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..0664ecd110 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": "20.3.1" } }, "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 93% rename from .devcontainer/compose.yml rename to .devcontainer/docker-compose.yml index d02d2a8f4a..2809cd2ca4 100644 --- a/.devcontainer/compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,3 +1,5 @@ +version: '3.8' + services: app: build: @@ -6,7 +8,6 @@ services: 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/.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..b889d96eb3 --- /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: 20.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..1aea8b5459 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 20.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..96e64c322e 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: [20.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..eef68aa0d1 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: [20.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: [20.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..213657ce1f 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: [20.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..8429465b5b 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: [20.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..a66e527db0 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 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..dd0fe95cce 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -22.15.0 +20.3.1 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..0e5c10a0b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1504 +1,20 @@ -## 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 + + +## 13.x.x (unreleased) ### General -- Enhance: 投稿者のロールに応じて、一つのノートに含むことのできるメンションとダイレクト投稿の宛先の人数に上限を設定できるように - * デフォルトのメンション上限は20アカウントに設定されます。(管理者はベースロールの設定で変更可能です。) - * 連合の問い合わせに応答しないサーバーのリモートユーザーへのメンションは、上限の人数に含めない実装になっています。 -- Enhance: 通知がミュート、凍結を考慮するようになりました -- Enhance: サーバーごとにモデレーションノートを残せるように -- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加 -- Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加 -- Enhance: 通知の履歴をリセットできるように -- Fix: ダイレクトなノートに対してはダイレクトでしか返信できないように - -### 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生成を無効にしてパフォーマンスを向上させることができるようになりました - サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました @@ -1506,25 +22,15 @@ - 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内にリンクを入れるとクリック不能になる問題の修正 @@ -1533,26 +39,14 @@ - Fix: フォルダーのページネーションが機能しない #11180 - Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正 - Fix: システムフォント設定が正しく反映されない問題を修正 -- Fix: アンケート終了時のプッシュ通知が正しく表示されない問題を修正 -- Fix: MasterVolumeが0の時だけでなく各通知音の音量設定が0のときも、HTMLAudioElement.playが実行されないように変更 ### Server - JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました - nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように - 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用) +- 全体的なDBクエリのパフォーマンスを向上 - 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: 無効化されたアンテナが再度有効化されないことがある問題を修正 +- リモートサーバーからのNSFW映像のキャッシュだけを無効化できるオプションを追加しました ## 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..896fb6b089 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..5431c28aad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=22.15.0-bookworm +ARG NODE_VERSION=20.3.1-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..ab4388c2eb 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 @@ -24,14 +22,45 @@ 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/ui-icons.afdesign b/assets/ui-icons.afdesign deleted file mode 100644 index 39abf1dd4f..0000000000 Binary files a/assets/ui-icons.afdesign and /dev/null differ diff --git a/chart/files/default.yml b/chart/files/default.yml index 8fa0b39eff..e62032abfd 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -77,17 +77,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 └───────────────────────────────────── @@ -116,22 +116,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 └───────────────────────────── @@ -151,7 +135,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 @@ -159,28 +142,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 └───────────────────────────────────── @@ -196,7 +158,7 @@ id: "aidx" # Job rate limiter # deliverJobPerSec: 128 -# inboxJobPerSec: 32 +# inboxJobPerSec: 16 # Job attempts # deliverJobMaxAttempts: 12 @@ -221,6 +183,9 @@ id: "aidx" # Media Proxy #mediaProxy: https://example.com/proxy +# Sign to ActivityPub GET request (default: true) +signToActivityPubGet: true + #allowedPrivateNetworks: [ # '127.0.0.1/32' #] diff --git a/chart/templates/Deployment.yml b/chart/templates/Deployment.yml index 3c73837801..d5dd14f59e 100644 --- a/chart/templates/Deployment.yml +++ b/chart/templates/Deployment.yml @@ -27,7 +27,7 @@ spec: ports: - containerPort: 3000 - name: postgres - image: postgres:15-alpine + image: postgres:14-alpine env: - name: POSTGRES_USER value: "example-misskey-user" @@ -38,7 +38,7 @@ spec: ports: - containerPort: 5432 - name: redis - image: redis:7-alpine + image: redis:alpine ports: - containerPort: 6379 volumes: diff --git a/compose.local-db.yml b/compose.local-db.yml deleted file mode 100644 index 3835cb23db..0000000000 --- a/compose.local-db.yml +++ /dev/null @@ -1,40 +0,0 @@ -# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します - -services: - redis: - restart: always - image: redis:7-alpine - ports: - - "6379:6379" - volumes: - - ./redis:/data - healthcheck: - test: "redis-cli ping" - interval: 5s - retries: 20 - - db: - restart: always - image: postgres:15-alpine - ports: - - "5432:5432" - env_file: - - .config/docker.env - volumes: - - ./db:/var/lib/postgresql/data - healthcheck: - test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" - interval: 5s - retries: 20 - -# meilisearch: -# restart: always -# image: getmeili/meilisearch:v1.3.4 -# environment: -# - MEILI_NO_ANALYTICS=true -# - MEILI_ENV=production -# env_file: -# - .config/meilisearch.env -# volumes: -# - ./meili_data:/meili_data - diff --git a/cypress.config.ts b/cypress.config.ts index 361acaf6e5..e390c41a54 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -2,6 +2,11 @@ import { defineConfig } from 'cypress' export default defineConfig({ e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + return require('./cypress/plugins/index.js')(on, config) + }, baseUrl: 'http://localhost:61812', }, }) diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.js similarity index 81% rename from cypress/e2e/basic.cy.ts rename to cypress/e2e/basic.cy.js index bd4021d2e3..2bf91cb009 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - describe('Before setup instance', () => { beforeEach(() => { cy.resetState(); @@ -23,7 +18,6 @@ describe('Before setup instance', () => { cy.intercept('POST', '/api/admin/accounts/create').as('signup'); - cy.get('[data-cy-admin-initial-password] input').type('example_password_please_change_this_or_you_will_get_hacked'); cy.get('[data-cy-admin-username] input').type('admin'); cy.get('[data-cy-admin-password] input').type('admin1234'); cy.get('[data-cy-admin-ok]').click(); @@ -31,15 +25,6 @@ describe('Before setup instance', () => { // なぜか動かない //cy.wait('@signup').should('have.property', 'response.statusCode'); cy.wait('@signup'); - - cy.intercept('POST', '/api/admin/update-meta').as('update-meta'); - - cy.get('[data-cy-next]').click(); - cy.get('[data-cy-next]').click(); - cy.get('[data-cy-server-name] input').type('Testskey'); - cy.get('[data-cy-server-setup-wizard-apply]').click(); - - cy.wait('@update-meta'); }); }); @@ -69,7 +54,6 @@ describe('After setup instance', () => { cy.get('[data-cy-signup]').click(); cy.get('[data-cy-signup-rules-continue]').should('be.disabled'); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); - cy.get('[data-cy-modal-dialog-ok]').click(); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').click(); @@ -94,7 +78,6 @@ describe('After setup instance', () => { cy.get('[data-cy-signup]').click(); cy.get('[data-cy-signup-rules-continue]').should('be.disabled'); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); - cy.get('[data-cy-modal-dialog-ok]').click(); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').click(); @@ -129,16 +112,11 @@ describe('After user signup', () => { it('signin', () => { cy.visitHome(); - cy.intercept('POST', '/api/signin-flow').as('signin'); + cy.intercept('POST', '/api/signin').as('signin'); cy.get('[data-cy-signin]').click(); - - cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); - // Enterキーで続行できるかの確認も兼ねる - cy.get('[data-cy-signin-username] input').type('alice{enter}'); - - cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 }); - // Enterキーで続行できるかの確認も兼ねる + cy.get('[data-cy-signin-username] input').type('alice'); + // Enterキーでサインインできるかの確認も兼ねる cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); cy.wait('@signin'); @@ -153,9 +131,8 @@ describe('After user signup', () => { cy.visitHome(); cy.get('[data-cy-signin]').click(); - - cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); - cy.get('[data-cy-signin-username] input').type('alice{enter}'); + cy.get('[data-cy-signin-username] input').type('alice'); + cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); // TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi); @@ -182,13 +159,11 @@ describe('After user signed in', () => { }); it('successfully loads', () => { - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).should('be.visible'); + cy.get('[data-cy-user-setup-continue]').should('be.visible'); }); it('account setup wizard', () => { - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).click(); + cy.get('[data-cy-user-setup-continue]').click(); cy.get('[data-cy-user-setup-user-name] input').type('ありす'); cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ'); @@ -225,8 +200,7 @@ describe('After user setup', () => { cy.login('alice', 'alice1234'); // アカウント初期設定ウィザード - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); + cy.get('[data-cy-user-setup] [data-cy-modal-window-close]').click(); cy.get('[data-cy-modal-dialog-ok]').click(); }); @@ -242,7 +216,7 @@ describe('After user setup', () => { cy.get('[data-cy-post-form-text]').type('Hello, Misskey!'); cy.get('[data-cy-open-post-form-submit]').click(); - cy.contains('Hello, Misskey!', { timeout: 15000 }); + cy.contains('Hello, Misskey!'); }); it('open note form with hotkey', () => { diff --git a/cypress/e2e/router.cy.ts b/cypress/e2e/router.cy.ts deleted file mode 100644 index 8d8fb3af31..0000000000 --- a/cypress/e2e/router.cy.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -describe('Router transition', () => { - describe('Redirect', () => { - // サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い) - before(() => { - cy.resetState(); - - // インスタンス初期セットアップ - cy.registerUser('admin', 'pass', true); - - // ユーザー作成 - cy.registerUser('alice', 'alice1234'); - - cy.login('alice', 'alice1234'); - - // アカウント初期設定ウィザード - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); - cy.wait(500); - cy.get('[data-cy-modal-dialog-ok]').click(); - }); - - it('redirect to user profile', () => { - // テストのためだけに用意されたリダイレクト用ルートに飛ぶ - cy.visit('/redirect-test'); - - // プロフィールページのURLであることを確認する - cy.url().should('include', '/@alice') - }); - }); -}); diff --git a/cypress/e2e/widgets.cy.ts b/cypress/e2e/widgets.cy.js similarity index 95% rename from cypress/e2e/widgets.cy.ts rename to cypress/e2e/widgets.cy.js index 847801a69f..f5a982eb0a 100644 --- a/cypress/e2e/widgets.cy.ts +++ b/cypress/e2e/widgets.cy.js @@ -1,9 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* flaky describe('After user signed in', () => { beforeEach(() => { cy.resetState(); @@ -73,4 +67,3 @@ describe('After user signed in', () => { buildWidgetTest('aiscript'); buildWidgetTest('aichan'); }); -*/ diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000000..59b2bab6e4 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,22 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line no-unused-vars +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.js similarity index 71% rename from cypress/support/commands.ts rename to cypress/support/commands.js index 197ff963ac..91a4d7abe6 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.js @@ -30,13 +30,9 @@ Cypress.Commands.add('visitHome', () => { }) Cypress.Commands.add('resetState', () => { - // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 - // see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123 - /* - cy.window().then(win => { + cy.window(win => { win.indexedDB.deleteDatabase('keyval-store'); }); - */ cy.request('POST', '/api/reset-db', {}).as('reset'); cy.get('@reset').its('status').should('equal', 204); cy.reload(true); @@ -48,19 +44,16 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => { cy.request('POST', route, { username: username, password: password, - ...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}), }).its('body').as(username); }); Cypress.Commands.add('login', (username, password) => { cy.visitHome(); - cy.intercept('POST', '/api/signin-flow').as('signin'); + cy.intercept('POST', '/api/signin').as('signin'); cy.get('[data-cy-signin]').click(); - cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); - cy.get('[data-cy-signin-username] input').type(`${username}{enter}`); - cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 }); + cy.get('[data-cy-signin-username] input').type(username); cy.get('[data-cy-signin-password] input').type(`${password}{enter}`); cy.wait('@signin').as('signedIn'); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.js similarity index 100% rename from cypress/support/e2e.ts rename to cypress/support/e2e.js diff --git a/cypress/support/index.ts b/cypress/support/index.ts deleted file mode 100644 index c1bed21979..0000000000 --- a/cypress/support/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare global { - namespace Cypress { - interface Chainable { - login(username: string, password: string): Chainable; - - registerUser( - username: string, - password: string, - isAdmin?: boolean - ): Chainable; - - resetState(): Chainable; - - visitHome(): Chainable; - } - } -} - -export {} diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json deleted file mode 100644 index 6fe7f32cc4..0000000000 --- a/cypress/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "es5"], - "target": "es5", - "types": ["cypress", "node"] - }, - "include": ["./**/*.ts"] -} diff --git a/compose_example.yml b/docker-compose.yml.example similarity index 62% rename from compose_example.yml rename to docker-compose.yml.example index 336bd814a7..a0061c5c20 100644 --- a/compose_example.yml +++ b/docker-compose.yml.example @@ -1,3 +1,5 @@ +version: "3" + services: web: build: . @@ -5,7 +7,6 @@ services: links: - db - redis -# - mcaptcha # - meilisearch depends_on: db: @@ -17,8 +18,6 @@ services: networks: - internal_network - external_network - # env_file: - # - .config/docker.env volumes: - ./files:/misskey/files - ./.config:/misskey/.config:ro @@ -49,39 +48,9 @@ services: interval: 5s retries: 20 -# mcaptcha: -# restart: always -# image: mcaptcha/mcaptcha:latest -# networks: -# internal_network: -# external_network: -# aliases: -# - localhost -# ports: -# - 7493:7493 -# env_file: -# - .config/docker.env -# environment: -# PORT: 7493 -# MCAPTCHA_redis_URL: "redis://mcaptcha_redis/" -# depends_on: -# db: -# condition: service_healthy -# mcaptcha_redis: -# condition: service_healthy -# -# mcaptcha_redis: -# image: mcaptcha/cache:latest -# networks: -# - internal_network -# healthcheck: -# test: "redis-cli ping" -# interval: 5s -# retries: 20 - # meilisearch: # restart: always -# image: getmeili/meilisearch:v1.3.4 +# image: getmeili/meilisearch:v1.1.1 # environment: # - MEILI_NO_ANALYTICS=true # - MEILI_ENV=production diff --git a/gulpfile.mjs b/gulpfile.mjs new file mode 100644 index 0000000000..9556eb795f --- /dev/null +++ b/gulpfile.mjs @@ -0,0 +1,65 @@ +/** + * Gulp tasks + */ + +import * as fs from 'node:fs'; +import gulp from 'gulp'; +import replace from 'gulp-replace'; +import terser from 'gulp-terser'; +import cssnano from 'gulp-cssnano'; + +import locales from './locales/index.js'; +import meta from './package.json' assert { type: "json" }; + +gulp.task('copy:backend:views', () => + gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views')) +); + +gulp.task('copy:frontend:fonts', () => + gulp.src('./packages/frontend/node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/_frontend_dist_/fonts/')) +); + +gulp.task('copy:frontend:tabler-icons', () => + gulp.src('./packages/frontend/node_modules/@tabler/icons-webfont/**/*').pipe(gulp.dest('./built/_frontend_dist_/tabler-icons/')) +); + +gulp.task('copy:frontend:locales', cb => { + fs.mkdirSync('./built/_frontend_dist_/locales', { recursive: true }); + + const v = { '_version_': meta.version }; + + for (const [lang, locale] of Object.entries(locales)) { + fs.writeFileSync(`./built/_frontend_dist_/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8'); + } + + cb(); +}); + +gulp.task('build:backend:script', () => { + return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js']) + .pipe(replace('LANGS', JSON.stringify(Object.keys(locales)))) + .pipe(terser({ + toplevel: true + })) + .pipe(gulp.dest('./packages/backend/built/server/web/')); +}); + +gulp.task('build:backend:style', () => { + return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css', './packages/backend/src/server/web/error.css']) + .pipe(cssnano({ + zindex: false + })) + .pipe(gulp.dest('./packages/backend/built/server/web/')); +}); + +gulp.task('build', gulp.parallel( + 'copy:frontend:locales', 'copy:backend:views', 'build:backend:script', 'build:backend:style', 'copy:frontend:fonts', 'copy:frontend:tabler-icons' +)); + +gulp.task('default', gulp.task('build')); + +gulp.task('watch', () => { + gulp.watch([ + './packages/*/src/**/*', + ], { ignoreInitial: false }, gulp.task('build')); +}); diff --git a/healthcheck.sh b/healthcheck.sh index dcfcf76786..e97a3f0636 100644 --- a/healthcheck.sh +++ b/healthcheck.sh @@ -1,7 +1,4 @@ #!/bin/bash -# SPDX-FileCopyrightText: syuilo and misskey-project -# SPDX-License-Identifier: AGPL-3.0-only - PORT=$(grep '^port:' /misskey/.config/default.yml | awk 'NR==1{print $2; exit}') -curl -Sfso/dev/null "http://localhost:${PORT}/healthz" +curl -s -S -o /dev/null "http://localhost:${PORT}" diff --git a/idea/README.md b/idea/README.md deleted file mode 100644 index f64d16800a..0000000000 --- a/idea/README.md +++ /dev/null @@ -1 +0,0 @@ -使われなくなったけど消すのは勿体ない(将来使えるかもしれない)コードを入れておくとこ diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 3675b17e53..4dc3f743f7 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -41,23 +41,19 @@ unfavorite: "إزالة من المفضلة" favorited: "أُضيف إلى المفضلة." alreadyFavorited: "تمت إضافته بالفعل إلى المفضلة." cantFavorite: "تعذرت الإضافة إلى المفضلة." -pin: "ثبتها على الصفحة الشخصية" -unpin: "فكها من ملفك الشخصي" +pin: "دبّسها على الصفحة الشخصية" +unpin: "ألغ تدبيسها من ملفك الشخصي" copyContent: "انسخ المحتوى" copyLink: "انسخ الرابط" delete: "حذف" deleteAndEdit: "إزالة وإعادة الصياغة" deleteAndEditConfirm: "أمتأكد من حذف الملاحظة؟ ستفقد كل مشاركاتها، والتفاعلات، والردود عليها." addToList: "أضفه إلى قائمة" -addToAntenna: "أضف إلى هوائي" sendMessage: "أرسل رسالة" copyRSS: "انسخ رابط RSS" copyUsername: "انسخ اسم المستخدم" copyUserId: "انسخ معرف المستخدم" copyNoteId: "انسخ معرف الملاحظة" -copyFileId: "انسخ معرّف الملف" -copyFolderId: "انسخ معرّف المجلد" -copyProfileUrl: "انسخ رابط الملف الشخصي" searchUser: "ابحث عن مستخدمين" reply: "رد" loadMore: "عرض المزيد" @@ -112,18 +108,18 @@ cantReRenote: "لا يمكنك إعادة نشر ملاحظة معاد نشره quote: "اقتبس" inChannelRenote: "إعادة نشر في قناة" inChannelQuote: "اقتباس في قناة" -pinnedNote: "ملاحظة مثبتة" -pinned: "ثبتها على الصفحة الشخصية" +pinnedNote: "ملاحظة مدبسة" +pinned: "دبّسها على الصفحة الشخصية" you: "أنت" clickToShow: "اضغط للعرض" sensitive: "محتوى حساس" add: "إضافة" reaction: "التفاعلات" reactions: "التفاعلات" +reactionSetting: "التفاعلات المراد عرضها في منتقي التفاعلات." reactionSettingDescription2: "اسحب لترتيب ، انقر للحذف ، استخدم \"+\" للإضافة." rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات" attachCancel: "أزل المرفق" -deleteFile: "حُذف الملف" markAsSensitive: "علّمه كمحتوى حساس" unmarkAsSensitive: "ألغ تعيينه كمحتوى حساس" enterFileName: "ادخل اسم الملف" @@ -140,10 +136,8 @@ unblockConfirm: "أمتأكد من إلغاء حجب هذا الحساب؟" suspendConfirm: "أمتأكد من تعليق الحساب؟" unsuspendConfirm: "أمتأكد من إلغاء تعليق؟" selectList: "اختر قائمة" -editList: "عدّل القائمة" selectChannel: "اختر قناة" selectAntenna: "اختر هوائيًا" -editAntenna: "عدّل الهوائي" selectWidget: "اختر ودجة" editWidgets: "عدّل الودجات" editWidgetsExit: "تم" @@ -214,7 +208,8 @@ blockedUsers: "الحسابات المحجوبة" noUsers: "ليس هناك مستخدمون" editProfile: "تعديل الملف التعريفي" noteDeleteConfirm: "هل تريد حذف هذه الملاحظة؟" -pinLimitExceeded: "لا يمكنك تثبيت الملاحظات بعد الآن." +pinLimitExceeded: "لا يمكنك تدبيس الملاحظات بعد الآن." +intro: "لقد انتهت عملية تنصيب Misskey. الرجاء إنشاء حساب إداري." done: "تمّ" processing: "المعالجة جارية" preview: "معاينة" @@ -250,6 +245,7 @@ removeAreYouSure: "متأكد من أنك تريد حذف {x}؟" deleteAreYouSure: "متأكد من أنك تريد حذف {x}؟" resetAreYouSure: "هل تريد إعادة التعيين؟" saved: "حُفظ" +messaging: "المحادثة" upload: "ارفع" keepOriginalUploading: "ابق الصورة الأصلية" keepOriginalUploadingDescription: "يحفظ الصور المرفوعة على حالتها الأصلية، وان عطّل ستولد نسخة مخصصة من الصورة." @@ -262,6 +258,7 @@ uploadFromUrlMayTakeTime: "سيستغرق بعض الوقت لاتمام الر explore: "استكشاف" messageRead: "مقروءة" noMoreHistory: "لا يوجد المزيد من التاريخ" +startMessaging: "ابدأ محادثة" nUsersRead: "قرأه {n}" agreeTo: "اوافق على {0}" agree: "أقبل" @@ -310,7 +307,6 @@ copyUrl: "انسخ الرابط" rename: "إعادة التسمية" avatar: "الصورة الرمزية" banner: "الصورة الرأسية" -displayOfSensitiveMedia: "عرض المحتوى الحساس" whenServerDisconnected: "عند فقدان الاتصال بالخادم" disconnectedFromServer: "قُطِع الإتصال بالخادم" reload: "انعش" @@ -340,25 +336,25 @@ enableLocalTimeline: "تفعيل الخيط المحلي" enableGlobalTimeline: "تفعيل الخيط الزمني الشامل" disablingTimelinesInfo: "سيتمكن المديرون والمشرفون من الوصول إلى كل الخيوط الزمنية حتى وإن لم تفعّل." registration: "إنشاء حساب" +enableRegistration: "تفعيل إنشاء الحسابات الجديدة" invite: "دعوة" driveCapacityPerLocalAccount: "حصة التخزين لكل مستخدم محلي" driveCapacityPerRemoteAccount: "حصة التخزين لكل مستخدم بعيد" inMb: "بالميغابايت" +iconUrl: "رابط الأيقونة" bannerUrl: "رابط صورة اللافتة" backgroundImageUrl: "رابط صورة الخلفية" basicInfo: "المعلومات الأساسية " -pinnedUsers: "المستخدمون المثبتون" -pinnedUsersDescription: "قائمة المستخدمين المثبتين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده." -pinnedPages: "الصفحات المثبتة" -pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تثبيتها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده." -pinnedClipId: "معرّف المشبك المثبت" -pinnedNotes: "ملاحظة مثبتة" +pinnedUsers: "المستخدمون المدبسون" +pinnedUsersDescription: "قائمة المستخدمين المدبسين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده." +pinnedPages: "الصفحات المدبسة" +pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تدبيسها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده." +pinnedClipId: "معرّف المشبك المدبس" +pinnedNotes: "ملاحظة مدبسة" hcaptcha: "hCaptcha" enableHcaptcha: "فعّل hCaptcha" hcaptchaSiteKey: "مفتاح الموقع" hcaptchaSecretKey: "المفتاح السري" -mcaptchaSiteKey: "مفتاح الموقع" -mcaptchaSecretKey: "المفتاح السري" recaptcha: "reCAPTCHA" enableRecaptcha: "تمكين reCAPTCHA" recaptchaSiteKey: "مفتاح الموقع" @@ -416,6 +412,7 @@ share: "شارِك" notFound: "غير موجود" notFoundDescription: "تعذر العثور على صفحة يقود إليها هذا الرابط." uploadFolder: "المجلد الافتراضي للرفع" +cacheClear: "مسح ذاكرة التخزين المؤقت" markAsReadAllNotifications: "وضع جميع الإشعارات كأنها مقروءة" markAsReadAllUnreadNotes: "علّم جميع الملاحظات كمقروءة" markAsReadAllTalkMessages: "علّم جميع الرسائل كمقروءة" @@ -433,6 +430,8 @@ retype: "أعد الكتابة" noteOf: "ملاحظات {user}" quoteAttached: "اِقتُبسَ" quoteQuestion: "أتريد تضمينها كاقتباس" +noMessagesYet: "ليس هناك رسائل بعد" +newMessageExists: "لقد تلقيت رسالة جديدة" onlyOneFileCanBeAttached: "يمكنك إرفاق ملف واحد بالرسالة" signinRequired: "رجاءً لِج" invitations: "دعوة" @@ -620,7 +619,10 @@ abuseReported: "أُرسل البلاغ، شكرًا لك" reporter: "المُبلّغ" reporteeOrigin: "أصل البلاغ" reporterOrigin: "أصل المُبلّغ" +forwardReport: "وجّه البلاغ إلى المثيل البعيد" +forwardReportIsAnonymous: "في المثيل البعيد سيظهر المبلّغ كحساب مجهول." send: "أرسل" +abuseMarkAsResolved: "علّم البلاغ كمحلول" openInNewTab: "افتح في لسان جديد" defaultNavigationBehaviour: "سلوك الملاحة الافتراضي" editTheseSettingsMayBreakAccount: "تعديل هذه الإعدادات قد يسبب عطبًا لحسابك" @@ -636,7 +638,6 @@ optional: "اختياري" createNewClip: "أنشئ مِشبكَا جديدًا" confirmToUnclipAlreadyClippedNote: "هذه الملاحظة تنتمي للمشبك {name} سلفًا، أتريد حذفها منه⸮" public: "علني" -private: "خاص" i18nInfo: "يترجم متطوعون ميسكي إلى عدة لغات، يمكنك المساعدة عبر {link}" manageAccessTokens: "إدارة رموز الوصول" accountInfo: "معلومات الحساب" @@ -675,6 +676,7 @@ experimental: "اختباري" developer: "المطور" makeExplorable: "أظهر الحساب في صفحة \"استكشاف\"" makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\"" +showGapBetweenNotesInTimeline: "أظهر فجوات بين المشاركات في الخيط الزمني" left: "يسار" center: "وسط" wide: "عريض" @@ -727,7 +729,7 @@ unlikeConfirm: "أتريد إلغاء إعجابك؟" fullView: "ملء الشاشة" quitFullView: "اخرج من وضع ملء للشاشة" addDescription: "أضف وصفًا" -userPagePinTip: "لعرض ملاحظة هنا اختر \"ثبتها على الصفحة الشخصية\" من قائمة تلك الملاحظة." +userPagePinTip: "لعرض ملاحظة هنا اختر \"دبسها على الصفحة الشخصية\" من قائمة تلك الملاحظة." notSpecifiedMentionWarning: "في الملاحظة ذكر لمستخدمين لن يستلموها." info: "عن" userInfo: "معلومات المستخدم" @@ -789,7 +791,6 @@ accountDeletionInProgress: "حذف الحساب جارٍ" usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله." devMode: "وضع المُطوّر" keepCw: "أبقِ على تحذيرات المحتوى" -pubSub: "حسابات Pub/Sub" lastCommunication: "آخر تواصل" resolved: "عولج" unresolved: "لم يعالج" @@ -798,7 +799,6 @@ breakFollowConfirm: "أمتأكد من إزالة المتابِع ؟" itsOn: "مفعّل" itsOff: "معطّل" on: "مفعل" -off: "معطل" emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل" unread: "غير مقروءة" filter: "رشّح" @@ -809,6 +809,8 @@ makeReactionsPublicDescription: "هذا سيجعل قائمة تفاعلاتك classic: "تقليدي" muteThread: "اكتم النقاش" unmuteThread: "ارفع الكتم عن النقاش" +ffVisibility: "مرئية المتابِعين/المتابَعين" +ffVisibilityDescription: "يسمح لك بتحديد من يمكنهم رؤية متابِعيك ومتابَعيك." continueThread: "اعرض بقية النقاش" deleteAccountConfirm: "سيحذف حسابك نهائيًا، أتريد المتابعة؟" incorrectPassword: "كلمة السر خاطئة." @@ -833,9 +835,6 @@ oneDay: "يوم" oneWeek: "أسبوع" oneMonth: "شهر" failedToFetchAccountInformation: "تعذر جلب معلومات الحساب" -cropImage: "اقتصاص الصورة" -cropImageAsk: "أتريد اقتصاص هذه الصورة" -cropYes: "اقتص" cropNo: "استخدمها كما هي" file: "الملفات" recentNHours: "آخر {n} ساعة" @@ -843,16 +842,13 @@ recentNDays: "آخر {n} أيام" noEmailServerWarning: "خادم البريد غير مضبوط." thereIsUnresolvedAbuseReportWarning: "توجد بلاغات غير معالجة." recommended: "مقترح" -check: "التحقق" driveCapOverrideLabel: "غيّر حجم قرص التخزين لهذا المستخدم" driveCapOverrideCaption: "أعد الحجم إلى القيمة الافتراضية بإدخال 0 أو أقل." requireAdminForView: "لاستعراض هذه الصفحة وجب عليك الولوج كمدير." -isSystemAccount: "حساب أنشأه النظام ويُدار من قِبله." typeToConfirm: "أدخل {x} للتأكيد" deleteAccount: "احذف الحساب" document: "التوثيق" numberOfPageCache: "عدد الصفحات المخزنة مؤقتًا" -numberOfPageCacheDescription: "رفع الرقم سيسحن تجربة المستخدم لكن سيرفع استهلاك الذاكرة." logoutConfirm: "أتريد الخروج؟" lastActiveDate: "آخر استخدام" statusbar: "شريط الحالة" @@ -867,7 +863,6 @@ slow: "بطيء" fast: "سريع" sensitiveMediaDetection: "التعرف على المحتوى الحساس" localOnly: "المحلي فقط" -remoteOnly: "بُعدي فقط" failedToUpload: "فشل الرفع" cannotUploadBecauseInappropriate: "تعذر رفع الملف لوجود محتوى حساس فيه." cannotUploadBecauseNoFreeSpace: "تعذر رفع الملف لنقص مساحة التخزين." @@ -887,7 +882,6 @@ pushNotificationAlreadySubscribed: "إرسال الإشعارات مفعل سل pushNotificationNotSupported: "متصفحك لا يدعم إرسال الإشعارات أو المثيل لا يدعمها." sendPushNotificationReadMessage: "احذف الإشعارات فور قراءتها" sendPushNotificationReadMessageCaption: "هذا قد يزيد من معدل استهلاك الطاقة لجهازك." -windowMaximize: "املأ الشاشة" windowRestore: "استرجاع" caption: "التعليق التوضيحي" loggedInAsBot: "والج كآلي" @@ -913,7 +907,6 @@ color: "اللون" manageCustomEmojis: "إدارة الإيموجي المخصصة" youCannotCreateAnymore: "وصلت لسقف الإنشاء." cannotPerformTemporary: "غير متاح مؤقتاً" -invalidParamError: "معاملات غير صالحة" permissionDeniedError: "رُفضة العملية" preset: "إعدادات مسبقة" selectFromPresets: "اختر من الإعدادات المسبقة" @@ -937,20 +930,14 @@ rolesAssignedToMe: "الأدوار المسندة إلي" resetPasswordConfirm: "هل تريد إعادة تعيين كلمة السر؟" license: "الرخصة" unfavoriteConfirm: "أتريد إزالتها من المفضلة؟" -reactionsDisplaySize: "حجم التفاعلات" -limitWidthOfReaction: "تصغير حجم التفاعلات" noteIdOrUrl: "معرف الملاحظة أو رابطها" video: "فيديو" videos: "فيديوهات" -dataSaver: "موفر البيانات" accountMigration: "ترحيل الحساب" accountMoved: "نقل هذا المستخدم حسابه:" accountMovedShort: "رُحل هذا الحساب." operationForbidden: "عملية ممنوعة" forceShowAds: "أظهر الإعلانات التجارية دائما" -reactionsList: "التفاعلات" -renotesList: "إعادات النشر" -notificationDisplay: "إشعارات" leftTop: "أعلى اليسار" rightTop: "أعلى اليمين" leftBottom: "أسفل اليسار" @@ -973,7 +960,6 @@ thisChannelArchived: "أُرشفت هذه القناة." displayOfNote: "عرض الملاحظة" initialAccountSetting: "إعداد الملف الشخصي" youFollowing: "متابَع" -preventAiLearning: "منع استخدام البيانات في تعليم الآلة" options: "خيارات" specifyUser: "مستخدم محدد" failedToPreviewUrl: "تتعذر المعاينة" @@ -987,35 +973,6 @@ later: "لاحقاً" goToMisskey: "لميسكي" additionalEmojiDictionary: "قواميس إيموجي إضافية" installed: "مُثبت" -enableServerMachineStats: "نشر إحصائيات عتاد الخادم" -turnOffToImprovePerformance: "تفعيله قد يزيد الأداء." -createInviteCode: "ولِّد دعوة" -inviteCodeCreated: "ولِّدت دعوة" -inviteLimitExceeded: "وصلتَ لحد عدد الدعوات المسموح لك توليدها." -createLimitRemaining: "حد عدد الدعوات: {limit} دعوة" -expirationDate: "تاريخ انتهاء الصلاحية" -noExpirationDate: "لا نهاية لصلاحيتها" -inviteCodeUsedAt: "اُستخدم رمز الدعوة في" -registeredUserUsingInviteCode: "اِستخدم رمز الدعوة" -unused: "غير مستعمَل" -expired: "منتهية صلاحيته" -icon: "الصورة الرمزية" -replies: "رد" -renotes: "أعد النشر" -sourceCode: "الشفرة المصدرية" -flip: "اقلب" -lastNDays: "آخر {n} أيام" -surrender: "ألغِ" -postForm: "أنشئ ملاحظة" -information: "عن" -_chat: - invitations: "دعوة" - noHistory: "السجل فارغ" - members: "الأعضاء" - home: "الرئيسي" - send: "أرسل" -_delivery: - stop: "مُعلّق" _initialAccountSetting: accountCreated: "نجح إنشاء حسابك!" letsStartAccountSetup: "إذا كنت جديدًا لنعدّ حسابك الشخصي." @@ -1023,10 +980,6 @@ _initialAccountSetting: profileSetting: "إعدادات الملف الشخصي" privacySetting: "إعدادات الخصوصية" theseSettingsCanEditLater: "يمكنك تغيير هذه الإعدادات لاحقًا." - skipAreYouSure: "أتريد تخطي إعداد الملف الشخصي؟" - laterAreYouSure: "أتريد إعداد الملف الشخصي لاحقًا؟" -_serverRules: - description: "مجموعة من القواعد لعرضها عند التسجيل، من المستحسن كتابة ملخصٍ للشروط الخدمة." _accountMigration: moveFrom: "انقل حسابًا آخر لهذا الحساب" moveFromLabel: "الحساب الأصلي #{n}" @@ -1101,7 +1054,6 @@ _role: description: "وصف الدور" permission: "أذونات الدور" assignTarget: "نوع الإسناد" - condition: "الشرط" options: "خيارات" policies: "السياسة العامة" priority: "الأولوية" @@ -1111,7 +1063,6 @@ _role: high: "عالية" _options: canManageCustomEmojis: "إدارة الإيموجي المخصصة" - pinMax: "حد عدد الملاحظات المثبتة" _condition: isLocal: "مستخدم محلي" isRemote: "مستخدم بعيد" @@ -1157,10 +1108,6 @@ _plugin: install: "ثبّت إضافات" installWarn: "رجاءً لا تثبت إضافات غير موثوقة." manage: "إدارة الإضافات" - viewSource: "اظهر المصدر" -_preferencesBackups: - createdAt: "تم إنشاؤه: {date} {time}" - updatedAt: "آخر تحديث: {date} {time}" _registry: scope: "الحيّز" key: "مفتاح" @@ -1201,6 +1148,11 @@ _wordMute: muteWords: "الكلمات المحظورة" muteWordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\"." muteWordsDescription2: "احصر الكلمات المفتاحية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية" + softDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني." + hardDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني.بالإضافة إلى أن هذه الملاحظات ستبقى مخفية حتى وإن تغيرت الشروط." + soft: "لينة" + hard: "قاسية" + mutedNotes: "الملاحظات المكتومة" _instanceMute: instanceMuteDescription: "هذه سيحجب كل ملاحظات الخوادم المحجوبة ومشاركاتها والردود على تلك الملاحظات حتى وإن كانت من خادم غير محجوب." instanceMuteDescription2: "مدخلة لكل سطر" @@ -1238,6 +1190,7 @@ _theme: shadow: "الظل" navBg: "خلفية الشريط الجانبي" navFg: "نص الشريط الجانبي" + navHoverFg: "نص الشريط الجانبي (عند التمرير فوقه)" link: "رابط" hashtag: "وسم" mention: "أشر الى" @@ -1252,11 +1205,17 @@ _theme: buttonBg: "خلفية الأزرار" buttonHoverBg: "خلفية الأزرار (عند التمرير فوقها)" inputBorder: "حواف حقل الإدخال" + listItemHoverBg: "خلفية عناصر القائمة (عند التمرير فوقها)" + driveFolderBg: "خلفية مجلد قرص التخزين" messageBg: "خلفية المحادثة" _sfx: note: "الملاحظات" noteMy: "ملاحظتي" notification: "الإشعارات" + chat: "المحادثة" + chatBg: "المحادثة (الخلفية)" + antenna: "الهوائيات" + channel: "إشعارات القنات" _ago: future: "المستقبَل" justNow: "اللحظة" @@ -1311,7 +1270,6 @@ _permissions: "read:gallery": "اعرض المعرض" "write:gallery": "عدّل المعرض" "read:gallery-likes": "يعرض ما أعجبك من مشاركات المعرض" - "write:chat": "اكتب أو احذف رسائل محادثة" _auth: shareAccess: "أتريد التفويض لـ \"{name}\" بالوصول لحسابك؟" shareAccessAsk: "هل تخول لهذا التطبيق الوصول لحسابك؟" @@ -1416,7 +1374,6 @@ _profile: _exportOrImport: allNotes: "كل الملاحظات" favoritedNotes: " الملاحظات المفضلة" - clips: "مِشبك" followingList: "المتابَعون" muteList: "المستخدمون المكتومون" blockingList: "المستخدمون المحجوبون" @@ -1461,6 +1418,9 @@ _pages: newPage: "أنشئ صفحة جديدة" editPage: "عدّل الصفحة" readPage: "نُشّط عرض المصدر" + created: "نجح إنشاء الصفحة" + updated: "نجح تعديل الصفحة" + deleted: "نجح حذف الصفحة" pageSetting: "إعدادات الصفحة" nameAlreadyExists: "رابط الصفحة موجود مسبقًا" invalidNameTitle: "رابط الصفحة ليس صالحًا" @@ -1479,7 +1439,7 @@ _pages: url: "رابط الصفحة" summary: "ملخص الصفحة" alignCenter: "توسيط العناصر" - hideTitleWhenPinned: "اخف عنوان الصفحة عند تثبيتها في ملف الشخصي" + hideTitleWhenPinned: "اخف عنوان الصفحة عند تدبيسها في ملف الشخصي" font: "الخط" fontSerif: "Serif" fontSansSerif: "Sans Serif" @@ -1509,83 +1469,49 @@ _notification: fileUploaded: "نجح رفع الملف" youGotMention: "{name} أشار إليك" youGotReply: "ردّ عليك {name}" - youGotQuote: "اقتبس {name} منشورك" - youRenoted: "أعاد {name} نشر منشورك" + youGotQuote: "اقتبس منك {name}" + youRenoted: "إعادت نشر من {name}" youWereFollowed: "يتابعك" youReceivedFollowRequest: "تلقيتَ طلب متابعة" yourFollowRequestAccepted: "قُبل طلب المتابعة" - pollEnded: "انتهى الاستطلاع" + pollEnded: "ظهرت نتائج الاستطلاع" unreadAntennaNote: "هوائي {name}" _types: all: "الكل" follow: "متابِعون جدد" mention: "الإشارات" reply: "الردود" - renote: "أعاد النشر" + renote: "أعد النشر" quote: "الاقتباسات" - reaction: "التفاعل" - receiveFollowRequest: "طلبات المتابعة" + reaction: "التفاعلات" + receiveFollowRequest: "طلبات المتابعة المتلقاة" followRequestAccepted: "طلبات المتابعة المقبولة" - login: "لِج" app: "إشعارات التطبيقات المرتبطة" _actions: followBack: "تابعك بالمثل" reply: "رد" renote: "أعد النشر" _deck: - alwaysShowMainColumn: "أظهر العمود الأساسي دائمًا" - columnAlign: "محاذاة الأعمدة" - addColumn: "إضافة عمود" - swapLeft: "التحريك إلى اليسار" - swapRight: "التحريك إلى اليمين" - swapUp: "التحريك إلى الأعلى" - swapDown: "التحريك إلى الأسفل" - profile: "حسابي الشخصي" - newProfile: "ملف تعريفي جديد" - deleteProfile: "حذف الملف التعريفي" + alwaysShowMainColumn: "أظهر العمود الرئيسي دائمًا" + columnAlign: "حاذِ الأعمدة" + addColumn: "أضف عمودًا" + swapLeft: "حرّك لليسار" + swapRight: "حرّك لليمين" + swapUp: "حرّك لأعلى" + swapDown: "حرّك لأسفل" + profile: "الملف الشخصي" _columns: - main: "الرئيسية" - widgets: "التطبيقات المُصغّرة" + main: "الرئيسي" + widgets: "الودجات" notifications: "الإشعارات" - tl: "الخط الزمني" + tl: "الخيط الزمني" antenna: "الهوائيات" list: "القوائم" channel: "القنوات" mentions: "الإشارات" direct: "مباشرة" _webhookSettings: - name: "الاسم" - active: "مُفعّل" + name: "الإسم" + active: "مفعّل" _events: - reaction: "عند التفاعل" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "البريد الإلكتروني " -_moderationLogTypes: - suspend: "علِق" - deleteDriveFile: "حُذف الملف" - deleteNote: "حُذفت الملاحظة" - createGlobalAnnouncement: "أُنشئ إعلان عام" - createUserAnnouncement: "أُنشئ إعلان مستخدم" - updateGlobalAnnouncement: "حُدث إعلان عام" - updateUserAnnouncement: "حُدث إعلان مستخدم" - resetPassword: "أعد تعيين كلمتك السرية" - createInvitation: "ولِّد دعوة" -_reversi: - total: "المجموع" - lookingForPlayer: "يبحث عن خصم..." - gameCanceled: "أُلغيت اللعبة." - opponentHasSettingsChanged: "غيَر الخصم إعدادته." - showBoardLabels: "اعرض ترقيم الصفوف والأعمدة على اللوح" - useAvatarAsStone: "حوَل الحجارة إلى صور مستخدمين" -_offlineScreen: - title: "غير متصل - يتعذر الاتصال بالخادم" - header: "يتعذر الاتصال بالخادم" -_remoteLookupErrors: - _noSuchObject: - title: "غير موجود" -_search: - searchScopeAll: "الكل" - searchScopeLocal: "المحلي" - searchScopeUser: "مستخدم محدد" + reaction: "عند تلقي تفاعل" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index f87818a5c8..78dbd77eb2 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -2,7 +2,6 @@ _lang_: "বাংলা" headlineMisskey: "নোট ব্যাবহার করে সংযুক্ত নেটওয়ার্ক" introMisskey: "স্বাগতম! মিসকি একটি ওপেন সোর্স, ডিসেন্ট্রালাইজড মাইক্রোব্লগিং পরিষেবা। \n\"নোট\" তৈরির মাধ্যমে যা ঘটছে তা সবার সাথে শেয়ার করুন 📡\n\"রিঅ্যাকশন\" গুলির মাধ্যমে যেকোনো নোট সম্পর্কে আপনার অনুভূতি ব্যাক্ত করতে পারেন 👍\nএকটি নতুন দুনিয়া ঘুরে দেখুন 🚀\n" -poweredByMisskeyDescription: "{name} হল ওপেন সোর্স প্ল্যাটফর্ম Misskey-এর সার্ভারগুলির একটি৷" monthAndDay: "{day}/{month}" search: "খুঁজুন" notifications: "বিজ্ঞপ্তি" @@ -13,14 +12,12 @@ fetchingAsApObject: "ফেডিভার্স থেকে খবর আন ok: "ঠিক" gotIt: "বুঝেছি" cancel: "বাতিল" -noThankYou: "না, ধন্যবাদ" enterUsername: "ইউজারনেম লিখুন" renotedBy: "{user} রিনোট করেছেন" noNotes: "কোন নোট নেই" noNotifications: "কোনো বিজ্ঞপ্তি নেই" instance: "ইন্সট্যান্স" settings: "সেটিংস" -notificationSettings: "বিজ্ঞপ্তির সেটিংস" basicSettings: "সাধারণ সেটিংস" otherSettings: "অন্যান্য সেটিংস" openInWindow: "নতুন উইন্ডোতে খুলা" @@ -45,20 +42,12 @@ pin: "পিন করা" unpin: "পিন সরান" copyContent: "বিষয়বস্তু কপি করুন" copyLink: "লিঙ্ক কপি করুন" -copyLinkRenote: "রিনোট লিঙ্ক কপি করুন" delete: "মুছুন" deleteAndEdit: "মুছুন এবং সম্পাদনা করুন" deleteAndEditConfirm: "আপনি কি এই নোটটি মুছে এটি সম্পাদনা করার বিষয়ে নিশ্চিত? আপনি এটির সমস্ত রিঅ্যাকশন, রিনোট এবং জবাব হারাবেন।" addToList: "লিস্ট এ যোগ করুন" -addToAntenna: "অ্যান্টেনা এ যোগ করুন" sendMessage: "একটি বার্তা পাঠান" -copyRSS: "RSS কপি করুন" copyUsername: "ব্যবহারকারীর নাম কপি করুন" -copyUserId: "ব্যবহারকারীর ID কপি করুন" -copyNoteId: "নোটের ID কপি করুন" -copyFileId: "ফাইল ID কপি করুন" -copyFolderId: "ফোল্ডার ID কপি করুন" -copyProfileUrl: "প্রোফাইল URL কপি করুন" searchUser: "ব্যবহারকারী খুঁজুন..." reply: "জবাব" loadMore: "আরও দেখুন" @@ -111,8 +100,6 @@ renoted: "রিনোট করা হয়েছে" cantRenote: "এই নোটটি রিনোট করা যাবে না।" cantReRenote: "রিনোটকে রিনোট করা যাবে না।" quote: "উদ্ধৃতি" -inChannelRenote: "চ্যানেলে রিনোট" -inChannelQuote: "চ্যানেলে উদ্ধৃতি" pinnedNote: "পিন করা নোট" pinned: "পিন করা" you: "আপনি" @@ -121,10 +108,7 @@ sensitive: "সংবেদনশীল বিষয়বস্তু" add: "যুক্ত করুন" reaction: "প্রতিক্রিয়া" reactions: "প্রতিক্রিয়া" -emojiPicker: "ইমোজি পিকার" -pinnedEmojisForReactionSettingDescription: "রিঅ্যাকশন দেয়ার সময় আপনি ইমোজিটিকে পিন করা এবং প্রদর্শিত হওয়ার জন্য সেট করতে পারেন।" -pinnedEmojisSettingDescription: "ইমোজি ইনপুট দেয়ার সময় আপনি ইমোজিটিকে পিন করা এবং প্রদর্শিত হওয়ার জন্য সেট করতে পারেন।" -emojiPickerDisplay: "পিকার ডিসপ্লে" +reactionSetting: "রিঅ্যাকশন পিকারে যেসকল প্রতিক্রিয়া দেখানো হবে" reactionSettingDescription2: "পুনরায় সাজাতে টেনে আনুন, মুছতে ক্লিক করুন, যোগ করতে + টিপুন।" rememberNoteVisibility: "নোটের দৃশ্যমান্যতার সেটিংস মনে রাখুন" attachCancel: "অ্যাটাচমেন্ট সরান " @@ -215,6 +199,7 @@ noUsers: "কোন ব্যাবহারকারী নেই" editProfile: "প্রোফাইল সম্পাদনা করুন" noteDeleteConfirm: "আপনি কি নোট ডিলিট করার ব্যাপারে নিশ্চিত?" pinLimitExceeded: "আপনি আর কোন নোট পিন করতে পারবেন না" +intro: "Misskey এর ইন্সটলেশন সম্পন্ন হয়েছে!দয়া করে অ্যাডমিন ইউজার তৈরি করুন।" done: "সম্পন্ন" processing: "প্রক্রিয়াধীন..." preview: "পূর্বরূপ দেখুন" @@ -251,6 +236,7 @@ removeAreYouSure: "আপনি কি \"{x}\" সরানোর ব্যা deleteAreYouSure: "আপনি কি \"{x}\" সরানোর ব্যাপারে নিশ্চিত?" resetAreYouSure: "রিসেট করার ব্যাপারে নিশ্চিত?" saved: "সংরক্ষিত হয়েছে" +messaging: "চ্যাট" upload: "আপলোড" keepOriginalUploading: "আসল ছবি রাখুন" keepOriginalUploadingDescription: "ছবিটি আপলোড করার সময় আসল সংস্করণটি রাখুন। অপশনটি বন্ধ থাকলে, আপলোডের সময় ওয়েব প্রকাশনার জন্য ছবি ব্রাউজারে তৈরি করা হবে।" @@ -263,6 +249,7 @@ uploadFromUrlMayTakeTime: "URL হতে আপলোড হতে কিছু explore: "ঘুরে দেখুন" messageRead: "পড়া" noMoreHistory: "আর কোন ইতিহাস নেই" +startMessaging: "চ্যাট শুরু করুন" nUsersRead: "{n} জন পড়েছেন" agreeTo: "{0} এর প্রতি আমি সম্মত" start: "শুরু করুন" @@ -336,10 +323,12 @@ enableLocalTimeline: "স্থানীয় টাইমলাইন চাল enableGlobalTimeline: "গ্লোবাল টাইমলাইন চালু করুন" disablingTimelinesInfo: "আপনি এই টাইমলাইনগুলি বন্ধ করলেও প্রশাসক এবং মডারেটররা এই টাইমলাইনগুলি ব্যাবহার করতে পারবে" registration: "নিবন্ধন" +enableRegistration: "নতুন ব্যাবহারকারী নিবন্ধন চালু করুন" invite: "আমন্ত্রণ" driveCapacityPerLocalAccount: "প্রত্যেক স্থানীয় ব্যাবহারকারীর জন্য ড্রাইভের জায়গা" driveCapacityPerRemoteAccount: "প্রত্যেক রিমোট ব্যাবহারকারীর জন্য ড্রাইভের জায়গা" inMb: "মেগাবাইটে লিখুন" +iconUrl: "আইকনের URL (ফ্যাভিকন, ইত্যাদি)" bannerUrl: "ব্যানার ছবির URL" backgroundImageUrl: "পটভূমির চিত্রের URL" basicInfo: "আপনার ব্যক্তিগত তথ্য" @@ -353,8 +342,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptcha চালু করুন" hcaptchaSiteKey: "সাইট কী" hcaptchaSecretKey: "সিক্রেট কী" -mcaptchaSiteKey: "সাইট কী" -mcaptchaSecretKey: "সিক্রেট কী" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHA চালু করুন" recaptchaSiteKey: "সাইট কী" @@ -407,6 +394,7 @@ share: "শেয়ার" notFound: "পাওয়া যায়নি" notFoundDescription: "এই URL-এর সাথে সম্পর্কিত কোনো পৃষ্ঠা নেই।" uploadFolder: "আপলোডের জন্য ডিফল্ট ফোল্ডার" +cacheClear: "ক্যাশ পরিষ্কার করুন" markAsReadAllNotifications: "সমস্ত বিজ্ঞপ্তিগুলি পঠিত হিসাবে চিহ্নিত করুন" markAsReadAllUnreadNotes: "সমস্ত নোটগুলি পঠিত হিসাবে চিহ্নিত করুন" markAsReadAllTalkMessages: "সমস্ত মেসেজ পঠিত হিসাবে চিহ্নিত করুন" @@ -424,6 +412,8 @@ retype: "পুনঃ প্রবেশ" noteOf: "{user} এর নোট" quoteAttached: "উদ্ধৃত" quoteQuestion: "উদ্ধৃতি হিসাবে সংযুক্ত করবেন?" +noMessagesYet: "কোন মেসেজ নেই" +newMessageExists: "নতুন মেসেজ পেয়েছেন" onlyOneFileCanBeAttached: "আপনি মেসেজের সাথে সর্বোচ্চ একটি ফাইল যুক্ত করতে পারবেন" signinRequired: "দয়া করে লগ ইন করুন" invitations: "আমন্ত্রণ" @@ -445,6 +435,7 @@ or: "অথবা" language: "ভাষা" uiLanguage: "UI এর ভাষা" aboutX: "{x} সম্পর্কে" +disableDrawer: "ড্রয়ার মেনু প্রদর্শন করবেন না" noHistory: "কোনো ইতিহাস নেই" signinHistory: "প্রবেশ করার ইতিহাস" doing: "প্রক্রিয়া করছে..." @@ -618,7 +609,10 @@ abuseReported: "আপনার অভিযোগটি দাখিল কর reporter: "অভিযোগকারী" reporteeOrigin: "অভিযোগটির উৎস" reporterOrigin: "অভিযোগকারীর উৎস" +forwardReport: "রিমোট ইন্সত্যান্সে অভিযোগটি পাঠান" +forwardReportIsAnonymous: "আপনার তথ্য রিমোট ইন্সত্যান্সে পাঠানো হবে না এবং একটি বেনামী সিস্টেম অ্যাকাউন্ট হিসাবে প্রদর্শিত হবে।" send: "পাঠান" +abuseMarkAsResolved: "অভিযোগটিকে সমাধাকৃত হিসাবে চিহ্নিত করুন" openInNewTab: "নতুন ট্যাবে খুলুন" openInSideView: "সাইড ভিউতে খুলুন" defaultNavigationBehaviour: "ডিফল্ট নেভিগেশন" @@ -634,7 +628,6 @@ createNew: "নতুন" optional: "প্রয়োজনীয় নয়" createNewClip: "নতুন ক্লিপ তৈরি করুন" public: "সর্বজনীন" -private: "ব্যাক্তিগত" i18nInfo: "Misskey স্বেচ্ছাসেবকদের দ্বারা বিভিন্ন ভাষায় অনুবাদ করা হচ্ছে। আপনি {link} এ গিয়ে অনুবাদে সহযোগিতা করতে পারেন।" manageAccessTokens: "অ্যাক্সেস টোকেন পরিচালনা করুন" accountInfo: "অ্যাকাউন্টের তথ্য" @@ -672,6 +665,7 @@ experimentalFeatures: "পরীক্ষামূলক বৈশিষ্ট developer: "ডেভেলপার" makeExplorable: "অ্যাকাউন্ট \"ঘুরে দেখুন\" পৃষ্ঠায় দেখান" makeExplorableDescription: "আপনি এটি বন্ধ করলে, আপনার অ্যাকাউন্ট \"ঘুরে দেখুন\" পৃষ্ঠায় প্রদর্শিত হবে না।" +showGapBetweenNotesInTimeline: "টাইমলাইন এবং নোটের মাঝে ফাকা জায়গা রাখুন" duplicate: "প্রতিরূপ" left: "বাম" center: "মাঝখান" @@ -801,6 +795,8 @@ makeReactionsPublicDescription: "আপনার পূর্ববর্তী classic: "ক্লাসিক" muteThread: "থ্রেড মিউট করুন" unmuteThread: "থ্রেড আনমিউট করুন" +ffVisibility: "অনুসরণ/অনুসরণকারীদের দৃশ্যমান্যতা" +ffVisibilityDescription: "আপনি কাকে অনুসরণ করেন এবং কে আপনাকে অনুসরণ করে, সেটা কারা দেখতে পাবে তা নির্ধারণ করে।" continueThread: "আরো থ্রেড দেখুন" deleteAccountConfirm: "আপনার অ্যাকাউন্ট মুছে ফেলা হবে। ঠিক আছে?" incorrectPassword: "আপনার দেওয়া পাসওয়ার্ডটি ভুল।" @@ -841,23 +837,6 @@ show: "প্রদর্শন" color: "রং" horizontal: "পাশে" youFollowing: "অনুসরণ করা হচ্ছে" -icon: "প্রোফাইল ছবি" -replies: "জবাব" -renotes: "রিনোট" -sourceCode: "সোর্স কোড" -flip: "উল্টান" -postForm: "নোট লিখুন" -information: "আপনার সম্পর্কে" -_chat: - invitations: "আমন্ত্রণ" - noHistory: "কোনো ইতিহাস নেই" - members: "সদস্যবৃন্দ" - home: "মূল পাতা" - send: "পাঠান" -_delivery: - stop: "স্থগিত করা হয়েছে" - _type: - none: "প্রকাশ করা হচ্ছে" _role: priority: "অগ্রাধিকার" _priority: @@ -907,7 +886,6 @@ _plugin: install: "প্লাগইন ইন্সটল করুন" installWarn: "অবিশ্বস্ত প্লাগইন ইনস্টল করবেন না।" manage: "প্লাগইন ম্যানেজ করুন" - viewSource: "উৎস দেখুন" _registry: scope: "স্কোপ" key: "কী" @@ -950,6 +928,11 @@ _wordMute: muteWords: "নিঃশব্দ করা শব্দগুলি" muteWordsDescription: "স্পেস দিয়ে আলাদা করলে AND শর্ত তৈরি হবে এবং আলাদা লাইনে লিখলে OR শর্ত তৈরি হবে।" muteWordsDescription2: "রেগুলার এক্সপ্রেশন ব্যবহার করতে স্ল্যাশ দিয়ে কীওয়ার্ডকে ঘিরে রাখুন।" + softDescription: "টাইমলাইন থেকে নির্দিষ্ট শর্তানুযায়ী নোট লুকিয়ে রাখে।" + hardDescription: "নির্দিষ্ট শর্তানুযায়ী নোটগুলিকে টাইমলাইন থেকে বাদ দেয়। আপনি শর্ত পরিবর্তন করলেও যে নোটগুলি যোগ করা হয়নি সেগুলি বাদ দেওয়া হবে।" + soft: "নমনীয়" + hard: "কঠোর" + mutedNotes: "মিউট করা নোটগুলি" _instanceMute: instanceMuteDescription: "কনফিগার করা ইন্সট্যান্সের সব নোট এবং রিনোট মিউট করুন, মিউট করা ইন্সট্যান্সের ব্যবহারকারীদের উত্তর সহ।" instanceMuteDescription2: "প্রতিটিকে আলাদা লাইনে লিখুন" @@ -996,6 +979,7 @@ _theme: header: "হেডার" navBg: "সাইডবারের পটভূমি" navFg: "সাইডবারের পাঠ্য" + navHoverFg: "সাইডবারের পাঠ্য (হভার)" navActive: "সাইডবারের পাঠ্য (অ্যাকটিভ)" navIndicator: "সাইডবারের ইনডিকেটর" link: "লিংক" @@ -1012,18 +996,30 @@ _theme: infoFg: "তথ্যের পাঠ্য" infoWarnBg: "ওয়ার্নিং এর পটভূমি" infoWarnFg: "ওয়ার্নিং এর পাঠ্য" + cwBg: "CW বাটনের পটভূমি" + cwFg: "CW বাটনের পাঠ্য" + cwHoverBg: "CW বাটনের পটভূমি (হভার)" toastBg: "বিজ্ঞপ্তির পটভূমি" toastFg: "বিজ্ঞপ্তির পাঠ্য" buttonBg: "বাটনের পটভূমি" buttonHoverBg: "বাটনের পটভূমি (হভার)" inputBorder: "ইনপুট ফিল্ডের বর্ডার" + listItemHoverBg: "লিস্ট আইটেমের পটভূমি (হোভার)" + driveFolderBg: "ড্রাইভ ফোল্ডারের পটভূমি" + wallpaperOverlay: "ওয়ালপেপার ওভারলে" badge: "ব্যাজ" messageBg: "চ্যাটের পটভূমি" + accentDarken: "অ্যাকসেন্ট (গাঢ়)" + accentLighten: "অ্যাকসেন্ট (হাল্কা)" fgHighlighted: "হাইলাইট করা পাঠ্য" _sfx: note: "নোটগুলি" noteMy: "নোট (আপনার)" notification: "বিজ্ঞপ্তি" + chat: "চ্যাট" + chatBg: "চ্যাট (ব্যাকগ্রাউন্ড)" + antenna: "অ্যান্টেনাগুলি" + channel: "চ্যানেলের বিজ্ঞপ্তি" _ago: future: "ভবিষ্যৎ" justNow: "এইমাত্র" @@ -1044,10 +1040,10 @@ _2fa: alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷" step1: "প্রথমে, আপনার ডিভাইসে {a} বা {b} এর মতো একটি অথেনটিকেশন অ্যাপ ইনস্টল করুন৷" step2: "এরপরে, অ্যাপের সাহায্যে প্রদর্শিত QR কোডটি স্ক্যান করুন।" + step2Url: "ডেস্কটপ অ্যাপে, নিম্নলিখিত URL লিখুন:" step3: "অ্যাপে প্রদর্শিত টোকেনটি লিখুন এবং আপনার কাজ শেষ।" step4: "আপনাকে এখন থেকে লগ ইন করার সময়, এইভাবে টোকেন লিখতে হবে।" securityKeyInfo: "আপনি একটি হার্ডওয়্যার সিকিউরিটি কী ব্যবহার করে লগ ইন করতে পারেন যা FIDO2 বা ডিভাইসের ফিঙ্গারপ্রিন্ট সেন্সর বা পিন সমর্থন করে৷" - renewTOTPCancel: "না, ধন্যবাদ" _permissions: "read:account": "অ্যাকাউন্টের তথ্য দেখুন" "write:account": "অ্যাকাউন্টের তথ্য সম্পাদন করুন" @@ -1081,7 +1077,6 @@ _permissions: "write:gallery": "গ্যালারী সম্পাদনা করুন" "read:gallery-likes": "গ্যালারীর পছন্দগুলি দেখুন" "write:gallery-likes": "গ্যালারীর পছন্দগুলি সম্পাদনা করুন" - "write:chat": "চ্যাটগুলি সম্পাদনা করুন" _auth: shareAccess: "\"{name}\" কে অ্যাকাউন্টের অ্যাক্সেস দিবেন?" shareAccessAsk: "অ্যাপ্লিকেশনটিকে অ্যাকাউন্টের অ্যাক্সেস দিবেন?" @@ -1187,7 +1182,6 @@ _profile: changeBanner: "ব্যানার পরিবর্তন করুন" _exportOrImport: allNotes: "সকল নোট" - clips: "ক্লিপ" followingList: "অনুসরণ করা হচ্ছে" muteList: "মিউট" blockingList: "ব্লক" @@ -1235,6 +1229,9 @@ _pages: newPage: "নতুন পৃষ্ঠা বানান" editPage: "পৃষ্ঠাটি সম্পাদনা করুন" readPage: "উৎস দেখছেন" + created: "পৃষ্ঠা তৈরি করা হয়েছে" + updated: "পৃষ্ঠা সম্পাদনা করা হয়েছে" + deleted: "পৃষ্ঠা মুছে ফেলা হয়েছে" pageSetting: "পৃষ্ঠার সেটিংস" nameAlreadyExists: "পৃষ্ঠার URLটি ইতিমধ্যেই ব্যাবহার করা হয়েছে" invalidNameTitle: "পৃষ্ঠার URL অবৈধ" @@ -1303,7 +1300,6 @@ _notification: pollEnded: "পোল শেষ" receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ" followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ" - login: "প্রবেশ করুন" app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি" _actions: followBack: "ফলো ব্যাক করেছে" @@ -1334,18 +1330,3 @@ _deck: _webhookSettings: name: "নাম" active: "চালু" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "ইমেইল" -_moderationLogTypes: - suspend: "স্থগিত করা" - resetPassword: "পাসওয়ার্ড রিসেট করুন" -_reversi: - total: "মোট" -_remoteLookupErrors: - _noSuchObject: - title: "পাওয়া যায়নি" -_search: - searchScopeAll: "সবগুলো" - searchScopeLocal: "স্থানীয়" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index c887397301..e3a6ec11b2 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -2,76 +2,60 @@ _lang_: "Català" headlineMisskey: "Una xarxa connectada per notes" introMisskey: "Benvingut! Misskey és un servei de microblogging descentralitzat de codi obert.\nCrea \"notes\" per compartir els teus pensaments amb tots els que t'envolten. 📡\nAmb \"reaccions\", també pots expressar ràpidament els teus sentiments sobre les notes de tothom. 👍\nExplorem un món nou! 🚀" -poweredByMisskeyDescription: "{name} És un dels serveis (anomenats instàncies de Misskey) que utilitzen la plataforma de codi obert Misskey." +poweredByMisskeyDescription: "{name} És un del serveis (anomenats instàncies de Misskey) que utilitzen la plataforma de codi obert Misskey." monthAndDay: "{day}/{month}" search: "Cercar" -reset: "Reiniciar" notifications: "Notificacions" username: "Nom d'usuari" password: "Contrasenya" -initialPasswordForSetup: "Contrasenya inicial per fer la primera configuració " -initialPasswordIsIncorrect: "La contrasenya no és correcta." -initialPasswordForSetupDescription: "Fes servir la contrasenya que has fet servir al fitxer de configuració, si tu mateix has instal·lat Misskey.\nSi fas servir una empresa d'allotjament de Misskey, fes servir la contrasenya que t'han donat.\nSi no has posat cap contrasenya deixar l'espai en blanc." -forgotPassword: "Restableix la contrasenya " -fetchingAsApObject: "Cercant al Fediverse..." +forgotPassword: "Contrasenya oblidada" +fetchingAsApObject: "Cercant en el Fediverse..." ok: "OK" -gotIt: "D'acord " +gotIt: "Ho he entès!" cancel: "Cancel·lar" -noThankYou: "No, gràcies" enterUsername: "Introdueix el teu nom d'usuari" -renotedBy: "Impulsat per {user}" +renotedBy: "Impulsat per {usuari}" noNotes: "Cap nota" noNotifications: "Cap notificació" -instance: "Instància " +instance: "Servidor" settings: "Preferències" -notificationSettings: "Configurar les notificacions" basicSettings: "Configuració bàsica" -otherSettings: "Altres configuracions" -openInWindow: "Obrir en una finestra nova" +otherSettings: "Configuració avançada" +openInWindow: "Obrir en una nova finestra" profile: "Perfil" timeline: "Línia de temps" noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia." login: "Iniciar sessió" -loggingIn: "Iniciar la sessió " +loggingIn: "Identificant-se" logout: "Tancar la sessió" signup: "Registrar-se" uploading: "Pujant..." save: "Desa" users: "Usuaris" addUser: "Afegir un usuari" -favorite: "Afegeix als preferits" +favorite: "Afegir a preferits" favorites: "Favorits" unfavorite: "Eliminar dels preferits" favorited: "Afegit als preferits." alreadyFavorited: "Ja s'ha afegit als preferits." -cantFavorite: "No es pot afegir als preferits." -pin: "Fixa al perfil" +cantFavorite: "No s'ha pogut afegir als preferits." +pin: "Fixar al perfil" unpin: "Para de fixar del perfil" -copyContent: "Copia el contingut" -copyLink: "Copia l'enllaç" -copyRemoteLink: "Copiar l'enllaç remot" -copyLinkRenote: "Copiar l'enllaç de la renota" +copyContent: "Copiar el contingut" +copyLink: "Copiar l'enllaç" delete: "Elimina" -deleteAndEdit: "Eliminar i editar" +deleteAndEdit: "Elimina i edita" deleteAndEditConfirm: "Segur que vols eliminar aquesta publicació i editar-la? Perdràs totes les reaccions, impulsos i respostes." addToList: "Afegir a una llista" -addToAntenna: "Afegir a una antena" sendMessage: "Enviar un missatge" -copyRSS: "Copiar RSS" copyUsername: "Copiar nom d'usuari" -copyUserId: "Copiar ID d'usuari" -copyNoteId: "Copiar ID de la nota" -copyFileId: "Copiar ID de l'arxiu" -copyFolderId: "Copiar ID de la carpeta" -copyProfileUrl: "Copiar adreça URL del perfil" searchUser: "Cercar un usuari" -searchThisUsersNotes: "Cercar les publicacions de l'usuari" -reply: "Respostes" +reply: "Respondre" loadMore: "Carregar més" showMore: "Veure més" -showLess: "Mostrar menys" +showLess: "Mostra menys" youGotNewFollower: "t'ha seguit" -receiveFollowRequest: "Has rebut una sol·licitud de seguiment" +receiveFollowRequest: "Sol·licitud de seguiment rebuda" followRequestAccepted: "Sol·licitud de seguiment acceptada" mention: "Menció" mentions: "Mencions" @@ -80,87 +64,70 @@ importAndExport: "Importar / Exportar" import: "Importar" export: "Exporta" files: "Fitxers" -download: "Descarregar" -driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer també seran esborrades." -unfollowConfirm: "Segur que vols deixar de seguir a {name}?" -exportRequested: "Has sol·licitat una exportació de dades. Això pot trigar una estona. S'afegirà a la teva unitat de disc un cop estigui completada." -importRequested: "Has sol·licitat una importació de dades. Això pot trigar una estona." +download: "Baixar" +driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer adjunt també se suprimiran." +unfollowConfirm: "Estàs segur que vols deixar de seguir {name}?" +exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona. S'afegirà a la teva unitat un cop completat." +importRequested: "Has sol·licitat una importació. Això pot trigar una estona." lists: "Llistes" noLists: "No tens cap llista" note: "Nota" notes: "Notes" -following: "Segueixes " +following: "Seguint" followers: "Seguidors" followsYou: "Et segueix" createList: "Crear llista" manageLists: "Gestionar les llistes" error: "Error" somethingHappened: "S'ha produït un error" -retry: "Torna-ho a provar" +retry: "Torna-ho a intentar" pageLoadError: "S'ha produït un error en carregar la pàgina" -pageLoadErrorDescription: "Això normalment és a causa d'errors a la xarxa o a la memòria cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després d'esperar un temps." +pageLoadErrorDescription: "Això normalment es deu a errors de xarxa o a la memòria cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després d'esperar una estona." serverIsDead: "Aquest servidor no respon. Espera una estona i torna-ho a provar." youShouldUpgradeClient: "Per veure aquesta pàgina, actualitzeu-la per actualitzar el vostre client." enterListName: "Introdueix un nom per a la llista" privacy: "Privadesa" makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació" defaultNoteVisibility: "Visibilitat per defecte" -follow: "Segueix" -followRequest: "Enviar sol·licitud de seguiment" -followRequests: "Peticions de seguiment" +follow: "Seguint" +followRequest: "Enviar la sol·licitud de seguiment" +followRequests: "Sol·licituds de seguiment" unfollow: "Deixar de seguir" followRequestPending: "Sol·licituds de seguiment pendents" enterEmoji: "Introduir un emoji" -renote: "Impulsar" +renote: "Impulsa" unrenote: "Anul·la l'impuls" renoted: "S'ha impulsat" -renotedToX: "Impulsat per {name}." cantRenote: "No es pot impulsar aquesta publicació" -cantReRenote: "No es pot impulsar un impuls." +cantReRenote: "No es pot impulsar l'impuls." quote: "Cita" -inChannelRenote: "Impulsar només a un canal" -inChannelQuote: "Citar només a un canal" -renoteToChannel: "Impulsar a un canal" -renoteToOtherChannel: "Impulsar a un altre canal" pinnedNote: "Nota fixada" pinned: "Fixar al perfil" you: "Tu" clickToShow: "Fes clic per mostrar" -sensitive: "Sensible" +sensitive: "NSFW" add: "Afegir" -reaction: "Reacció " +reaction: "Reaccions" reactions: "Reaccions" -emojiPicker: "Selector d'emojis" -pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb qui vols reaccionar" -pinnedEmojisSettingDescription: "Selecciona quins emojis vols deixar fixats i es mostrin en obrir el selector d'emojis" -emojiPickerDisplay: "Mostrar el selector d'emojis" -overwriteFromPinnedEmojisForReaction: "Reemplaça els emojis de la reacció" -overwriteFromPinnedEmojis: "Sobreescriu els emojis fixats al panel de reaccions" +reactionSetting: "Reaccions a mostrar al selector de reaccions" reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir." rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes" attachCancel: "Eliminar el fitxer adjunt" -deleteFile: "Esborrar l'arxiu " -markAsSensitive: "Marcar com a sensible" +markAsSensitive: "Marcar com a NSFW" unmarkAsSensitive: "Deixar de marcar com a sensible" enterFileName: "Defineix nom del fitxer" mute: "Silencia" unmute: "Deixa de silenciar" -renoteMute: "Silenciar impulsos" -renoteUnmute: "Treure el silenci dels impulsos" block: "Bloqueja" unblock: "Desbloqueja" suspend: "Suspèn" unsuspend: "Deixa de suspendre" -blockConfirm: "Vols bloquejar-lo?" +blockConfirm: "Vols bloquejar?" unblockConfirm: "Vols desbloquejar-lo?" suspendConfirm: "Estàs segur que vols suspendre aquest compte?" unsuspendConfirm: "Estàs segur que vols treure la suspensió d'aquest compte?" selectList: "Tria una llista" -editList: "Editar llista" -selectChannel: "Selecciona un canal" selectAntenna: "Tria una antena" -editAntenna: "Modificar antena" -createAntenna: "Crea una antena" selectWidget: "Triar un giny" editWidgets: "Editar ginys" editWidgetsExit: "Fet" @@ -173,40 +140,31 @@ addEmoji: "Afegeix un emoji" settingGuide: "Configuració recomanada" cacheRemoteFiles: "Emmagatzemar fitxers remots" cacheRemoteFilesDescription: "Quan aquesta opció està desactivada, els fitxers remots es carreguen directament des del servidor remot. Si desactiveu això, es reduirà l'ús d'emmagatzematge, però augmentarà el trànsit, ja que no es generaran miniatures." -youCanCleanRemoteFilesCache: "Pots netejar la memòria cau fent clic al botó de la paperera🗑️ a l'administrador d'arxius." -cacheRemoteSensitiveFiles: "Posar a la memòria cau arxius remots sensibles" -cacheRemoteSensitiveFilesDescription: "Quan aquesta opció és desactiva, els arxius remots sensibles es carregant directament del servidor d'origen sense que es guardin a la memòria cau." flagAsBot: "Marca aquest compte com a bot" -flagAsBotDescription: "Activa aquesta opció si el compte el controla un programa. Si s'activa, actuarà com un senyal per altres desenvolupadors per prevenir cadenes d'interacció sense fi i ajustar els paràmetres interns de Misskey pe tractar el compte com un bot." +flagAsBotDescription: "Marca aquest compte com a bot" flagAsCat: "Marca aquest compte com a gat" flagAsCatDescription: "Activeu aquesta opció per marcar aquest compte com a gat." flagShowTimelineReplies: "Mostra les respostes a la línia de temps" -flagShowTimelineRepliesDescription: "Mostra les respostes dels usuaris a les notes d'altres usuaris a la línia de temps." +flagShowTimelineRepliesDescription: "Mostra les respostes a la línia de temps" autoAcceptFollowed: "Aprova automàticament les sol·licituds de seguiment dels usuaris que segueixes" addAccount: "Afegeix un compte" -reloadAccountsList: "Recarregar la llista de contactes" loginFailed: "S'ha produït un error al accedir." showOnRemote: "Navega més en el perfil original" -continueOnRemote: "Veure perfil original" -chooseServerOnMisskeyHub: "Escull un servidor des del Hub de Misskey" -specifyServerHost: "Especifica un servidor directament" -inputHostName: "Introdueix el domini" general: "General" wallpaper: "Fons de Pantalla" setWallpaper: "Defineix el fons de pantalla" removeWallpaper: "Elimina el fons de pantalla" searchWith: "Cerca: {q}" youHaveNoLists: "No tens cap llista" -followConfirm: "Segur que vols seguir a {name}?" +followConfirm: "Estàs segur que vols deixar de seguir {name}?" proxyAccount: "Compte de proxy" proxyAccountDescription: "Un compte proxy és un compte que actua com a seguidor remot per als usuaris en determinades condicions. Per exemple, quan un usuari afegeix un usuari remot a la llista, l'activitat de l'usuari remot no es lliurarà al servidor si cap usuari local segueix aquest usuari, de manera que el compte proxy el seguirà." host: "Amfitrió" -selectSelf: "Escollir manualment" selectUser: "Selecciona usuari/a" recipient: "Destinatari" annotation: "Comentaris" federation: "Federació" -instances: "Instàncies " +instances: "Servidors" registeredAt: "Registrat a" latestRequestReceivedAt: "Última petició rebuda" latestStatus: "Últim estat" @@ -215,35 +173,25 @@ charts: "Gràfics" perHour: "Per hora" perDay: "Per dia" stopActivityDelivery: "Deixa d'enviar activitats" -blockThisInstance: "Bloca aquesta instància " -silenceThisInstance: "Silencia aquesta instància " -mediaSilenceThisInstance: "Silenciar els arxius d'aquesta instància " +blockThisInstance: "Deixa d'enviar activitats" operations: "Accions" software: "Programari" -softwareName: "Nom del programari" version: "Versió" metadata: "Metadades" withNFiles: "{n} fitxer(s)" monitor: "Monitor" -jobQueue: "Cua de feines" +jobQueue: "Cua de tasques" cpuAndMemory: "CPU i memòria" network: "Xarxa" disk: "Disc" instanceInfo: "Informació del fitxer d'instal·lació" statistics: "Estadístiques" -clearQueue: "Esborra la cua de feina" +clearQueue: "Esborrar la cua" clearQueueConfirmTitle: "Esteu segur que voleu esborrar la cua?" clearQueueConfirmText: "Les notes no lliurades que quedin a la cua no es federaran. Normalment aquesta operació no és necessària." clearCachedFiles: "Esborra la memòria cau" clearCachedFilesConfirm: "Segur que voleu eliminar tots els fitxers de la memòria cau?" blockedInstances: "Instàncies bloquejades" -blockedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols bloquejar separades per un salt de pàgina. Les instàncies llistades no podran comunicar-se amb aquesta instància." -silencedInstances: "Instàncies silenciades" -silencedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols silenciar. Tots els comptes de les instàncies llistades s'establiran com silenciades i només podran fer sol·licitacions de seguiment, i no podran mencionar als comptes locals si no els segueixen. Això no afectarà les instàncies bloquejades." -mediaSilencedInstances: "Instàncies amb els arxius silenciats" -mediaSilencedInstancesDescription: "Llista els noms dels servidors que vulguis silenciar els arxius, un servidor per línia. Tots els comptes que pertanyin als servidors llistats seran tractats com sensibles i no podran fer servir emojis personalitzats. Això no tindrà efecte sobre els servidors blocats." -federationAllowedHosts: "Llista de servidors federats" -federationAllowedHostsDescription: "Llista dels servidors amb els quals es federa." muteAndBlock: "Silencia i bloca" mutedUsers: "Usuaris silenciats" blockedUsers: "Usuaris bloquejats" @@ -251,18 +199,16 @@ noUsers: "No hi ha usuaris" editProfile: "Edita el perfil" noteDeleteConfirm: "Segur que voleu eliminar aquesta publicació?" pinLimitExceeded: "No podeu fixar més publicacions" +intro: "La instal·lació de Misskey ha acabat! Crea un usuari d'administrador." done: "Fet" processing: "S'està processant..." preview: "Vista prèvia" default: "Per defecte" defaultValueIs: "Per defecte: {value}" -noCustomEmojis: "No hi ha emojis personalitzats" -noJobs: "No hi ha feines" +noCustomEmojis: "Cap emoji personalitzat" federating: "Federant" blocked: "Bloquejat" -suspended: "Anul·lar subscripció " -all: "tot" -subscribing: "Subscrit a" +suspended: "Suspés" publishing: "S'està publicant" notResponding: "Sense resposta" instanceFollowing: "Seguits del servidor" @@ -270,51 +216,31 @@ instanceFollowers: "Seguidors del servidor" instanceUsers: "Usuaris del servidor" changePassword: "Canvia la contrasenya" security: "Seguretat" -retypedNotMatch: "Les entrades no coincideix" +retypedNotMatch: "L'entrada no coincideix" currentPassword: "Contrasenya actual" newPassword: "Contrasenya nova" -newPasswordRetype: "Contrasenya nova (repeteix-la)" -attachFile: "Afegeix un arxiu" +newPasswordRetype: "Contrasenya nou (repeteix-la)" +attachFile: "Adjunta fitxers" more: "Més" featured: "Destacat" usernameOrUserId: "Nom o ID d'usuari" noSuchUser: "No s'ha trobat l'usuari" lookup: "Cerca" -announcements: "Avisos" +announcements: "Anuncis" imageUrl: "URL de la imatge" remove: "Eliminar" removed: "Eliminat" -removeAreYouSure: "Segur que vols esborrar «{x}»?" -deleteAreYouSure: "Segur que vols esborrar «{x}»?" -resetAreYouSure: "Segur que vols restablir-ho?" -areYouSure: "Estàs segur?" +removeAreYouSure: "Segur que voleu retirar «{x}»?" +deleteAreYouSure: "Segur que voleu retirar «{x}»?" +resetAreYouSure: "Segur que voleu restablir-ho?" saved: "S'ha desat" +messaging: "Xat" upload: "Puja" -keepOriginalUploading: "Guarda la imatge original" -keepOriginalUploadingDescription: "Guarda la imatge pujada sense modificar. Si està desactivat, es generarà una versió per visualitzar a la web en pujar la imatge." -fromDrive: "Des del Disc" -fromUrl: "Des d'un enllaç" -uploadFromUrl: "Carrega des d'un enllaç" -uploadFromUrlDescription: "Enllaç del fitxer que vols carregar" -uploadFromUrlRequested: "Càrrega sol·licitada" -uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot trigar un temps" -uploadNFiles: "Pujar {n} arxius" -explore: "Explora" -messageRead: "Vist" -noMoreHistory: "No hi ha res més per veure" -startChat: "Comença a xatejar " -nUsersRead: "Vist per {n}" -agreeTo: "Accepto que {0}" -agree: "Hi estic d'acord" -agreeBelow: "Hi estic d'acord amb el següent" -basicNotesBeforeCreateAccount: "Notes importants" -termsOfService: "Condicions d'ús" start: "Comença" home: "Inici" -remoteUserCaution: "Ja que aquest usuari resideix a una instància remota, la informació mostrada es podria trobar incompleta." activity: "Activitat" images: "Imatges" -image: "Imatge" +image: "Imatges" birthday: "Aniversari" yearsOld: "{age} anys" registeredDate: "Data de registre" @@ -327,45 +253,20 @@ dark: "Fosc" lightThemes: "Temes clars" darkThemes: "Temes foscos" syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu" -switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" es troba activat. Vols desactivar la sincronització i canviar de mode manualment?" -drive: "Disc" -fileName: "Nom del Fitxer" -selectFile: "Selecciona un fitxer" -selectFiles: "Selecciona fitxers" -selectFolder: "Selecció de carpeta" -selectFolders: "Selecció de carpetes" -fileNotSelected: "Cap fitxer seleccionat" renameFile: "Canvia el nom del fitxer" folderName: "Nom de la carpeta" createFolder: "Crea una carpeta" renameFolder: "Canvia el nom de la carpeta" deleteFolder: "Elimina la carpeta" -folder: "Carpeta " addFile: "Afegeix un fitxer" -showFile: "Mostrar fitxer" -emptyDrive: "El teu Disc és buit" emptyFolder: "La carpeta està buida" unableToDelete: "No es pot eliminar" -inputNewFileName: "Introduïu el nom de fitxer nou" -inputNewDescription: "Escriu el peu de foto." -inputNewFolderName: "Introduïu el nom de la carpeta nova" -circularReferenceFolder: "La carpeta destinatària és una subcarpeta de la carpeta a la qual la desitges moure" -hasChildFilesOrFolders: "No és possible esborrar aquesta carpeta ja que no és buida" copyUrl: "Copia l'URL" rename: "Canvia el nom" -avatar: "Icona" -banner: "Bàner" -displayOfSensitiveMedia: "Visualització de contingut sensible" -whenServerDisconnected: "Quan es perdi la connexió al servidor" -disconnectedFromServer: "Desconnectat pel servidor" -reload: "Actualitzar" +reload: "Actualitza" doNothing: "Ignora" -reloadConfirm: "Vols recarregar?" -watch: "Veure" -unwatch: "Deixa de veure" -accept: "Acceptar" -reject: "Denega" -normal: "Normal" +accept: "Accepta" +normal: "Nomal" instanceName: "Nom del servidor" instanceDescription: "Descripció del servidor" maintainerName: "Nom de l'administrador" @@ -383,57 +284,23 @@ connectService: "Connecta" disconnectService: "Desconnecta" enableLocalTimeline: "Activa la línia de temps local" enableGlobalTimeline: "Activa la línia de temps global" -disablingTimelinesInfo: "Fins i tot si aquestes línies de temps són desactivades, els administradors i els moderadors poden continuar visualitzant per conveniència." registration: "Registre" invite: "Convida" -driveCapacityPerLocalAccount: "Capacitat del disc per usuaris locals" -driveCapacityPerRemoteAccount: "Capacitat del disc per usuaris remots" -inMb: "En megabytes" -bannerUrl: "Adreça URL del bàner" -backgroundImageUrl: "Adreça URL de la imatge de fons" basicInfo: "Informació bàsica" pinnedUsers: "Usuaris fixats" -pinnedUsersDescription: "Llista d'usuaris, separats per salts de línia, que seran fixats a la pestanya \"Explorar\"." -pinnedPages: "Pàgines fixades" -pinnedPagesDescription: "Escriu les adreces de les pàgines que vols fixar a la pàgina d'inici d'aquesta instància. Separades per salts de línia." -pinnedClipId: "ID del retall fixat" pinnedNotes: "Nota fixada" -hcaptcha: "hCaptcha" -enableHcaptcha: "Activa hCaptcha" -hcaptchaSiteKey: "Clau del lloc" -hcaptchaSecretKey: "Clau secreta" -mcaptcha: "mCaptcha" -enableMcaptcha: "Activa mCaptcha" -mcaptchaSiteKey: "Clau del lloc" -mcaptchaSecretKey: "Clau secreta" -mcaptchaInstanceUrl: "Adreça URL del servidor mCaptcha" -recaptcha: "reCAPTCHA" -enableRecaptcha: "Activa reCAPTCHA" -recaptchaSiteKey: "Clau del lloc" -recaptchaSecretKey: "Clau secreta" turnstile: "Turnstile" enableTurnstile: "Activar Turnstile" turnstileSiteKey: "Clau del lloc" turnstileSecretKey: "Clau secreta" -avoidMultiCaptchaConfirm: "Fer servir diferents sistemes de Captcha a la vegada pot causar problemes entre ells. Vols desactivar els altres sistemes de Captcha activats? Si els vols mantenir actius fes clic a cancel·lar." antennas: "Antena" manageAntennas: "Gestiona les antenes" -name: "Nom" antennaSource: "Font de l'antena" antennaKeywords: "Paraules clau a seguir" antennaExcludeKeywords: "Paraules clau a excloure" -antennaExcludeBots: "Exclou els bots" -antennaKeywordsDescription: "Separar amb espais per la condició AND o amb salts de línia per la condició OR." notifyAntenna: "Notifica'm les publicacions noves" withFileAntenna: "Només les publicacions amb fitxers" -excludeNotesInSensitiveChannel: "Excloure notes a canals sensibles" -enableServiceworker: "Activar les notificacions al navegador" -antennaUsersDescription: "Llistar un nom d'usuari per línia" -caseSensitive: "Sensible a majúscules i minúscules " -withReplies: "Inclou respostes" -connectedTo: "Aquests comptes hi són connectats" notesAndReplies: "Amb respostes" -withFiles: "Incloure arxius" silence: "Silencia" silenceConfirm: "Segur que vols silenciar aquest usuari?" unsilence: "Deixa de silenciar" @@ -449,2235 +316,142 @@ userList: "Llistes" about: "Informació" aboutMisskey: "Quant a Misskey" administrator: "Administrador/a" -token: "Codi de verificació" -2fa: "Autenticació de doble factor" -setupOf2fa: "Configura l'autenticació de doble factor" -totp: "Aplicació d'autenticació" -totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d'autenticació" moderator: "Moderador/a" moderation: "Moderació" -moderationNote: "Nota de moderació " -moderationNoteDescription: "Pots escriure notes que es compartiran entre els moderadors." -addModerationNote: "Afegeix una nota de moderació " -moderationLogs: "Registre de moderació " nUsersMentioned: "{n} usuaris mencionats" -securityKeyAndPasskey: "Clau de seguretat / Clau de pas" securityKey: "Clau de seguretat" -lastUsed: "Fet servir per última vegada" -lastUsedAt: "Fet servir per última vegada: {t}" unregister: "Cancel·la el registre" passwordLessLogin: "Inici de sessió sense contrasenya" -passwordLessLoginDescription: "Permet l'inici de sessió sense contrasenya fent servir només una Clau de seguretat/Clau de pas" resetPassword: "Restableix la contrasenya" newPasswordIs: "La contrasenya nova és «{password}»" reduceUiAnimation: "Redueix les animacions de la interfície" share: "Comparteix" notFound: "No s'ha trobat" -notFoundDescription: "No es troba cap pàgina que correspongui a aquesta adreça" -uploadFolder: "Carpeta per defecte on desar els arxius pujats" -markAsReadAllNotifications: "Marca totes les notificacions com a llegides" markAsReadAllUnreadNotes: "Marca-ho tot com a llegit" -markAsReadAllTalkMessages: "Marcar tots els missatges com llegits" help: "Ajuda" -inputMessageHere: "Escriu aquí el teu missatge " -close: "Tanca" invites: "Convida" -members: "Membres" -transfer: "Transferir" -title: "Títol" -text: "Text" -enable: "Habilita" next: "Següent" -retype: "Torneu a introduir-la" noteOf: "Publicació de: {user}" -quoteAttached: "Frase adjunta" -quoteQuestion: "Vols annexar-la com a cita?" -attachAsFileQuestion: "El text copiat és massa llarg. Vols adjuntar-lo com un fitxer de text?" -onlyOneFileCanBeAttached: "Només pots adjuntar un fitxer a un missatge" -signinRequired: "Si us plau, Registra't o inicia la sessió abans de continuar" -signinOrContinueOnRemote: "Per continuar necessites moure el teu servidor o registrar-te / iniciar sessió en aquest servidor." invitations: "Convida" -invitationCode: "Codi d'invitació" -checking: "Comprovació en curs..." -available: "Disponible" -unavailable: "No és disponible" -usernameInvalidFormat: "Pots fer servir lletres (majúscules i minúscules), números i barres baixes (\"_\")" -tooShort: "Massa curt" -tooLong: "Massa llarg" -weakPassword: "Contrasenya insegura" -normalPassword: "Bona contrasenya" -strongPassword: "Contrasenya segura" -passwordMatched: "Correcte!" -passwordNotMatched: "No coincideix" -signinWith: "Inicia sessió amb {x}" -signinFailed: "Autenticació sense èxit. Intenta-ho un altre cop utilitzant la contrasenya i el nom correctes." -or: "O" -language: "Idioma" -uiLanguage: "Idioma de l'interfície" -aboutX: "Respecte a {x}" -emojiStyle: "Estil d'emoji" -native: "Nadiu" -menuStyle: "Estil de menú" -style: "Estil" -drawer: "Calaix" -popup: "Emergent" -showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor" -showReactionsCount: "Mostra el nombre de reaccions a les publicacions" -noHistory: "No hi ha un registre previ" -signinHistory: "Historial d'autenticacions" -enableAdvancedMfm: "Habilitar l'MFM avançat" -enableAnimatedMfm: "Habilitar l'MFM amb moviment" -doing: "Processant..." -category: "Categoria" tags: "Etiquetes" docSource: "Font del document" createAccount: "Crea un compte" existingAccount: "Compte existent" regenerate: "Regenera" fontSize: "Mida del text" -mediaListWithOneImageAppearance: "Altura de la llista de fitxers amb una única imatge" -limitTo: "Limita a {x}" noFollowRequests: "No tens sol·licituds de seguiment" -openImageInNewTab: "Obre imatges a una nova pestanya" -dashboard: "Tauler de control" +dashboard: "Panell de control" local: "Local" remote: "Remot" total: "Total" -weekOverWeekChanges: "Canvis l'última setmana" -dayOverDayChanges: "Canvis ahir" appearance: "Aparença" clientSettings: "Configuració del client" accountSettings: "Configuració del compte" -promotion: "Promocionat" -promote: "Promoure" -numberOfDays: "Nombre de dies" hideThisNote: "Amaga la publicació" showFeaturedNotesInTimeline: "Mostra publicacions destacades en la línia de temps" -objectStorage: "Emmagatzematge d'objectes\n" -useObjectStorage: "Utilitzar l'emmagatzematge d'objectes" -objectStorageBaseUrl: "Base d'enllaç" -objectStorageBaseUrlDesc: "Prefix d'enllaç utilitzat per a fer referencia als fitxers. Especifica l'enllaç del teu CDN o Proxy si n'estàs utilitzant qualsevol, en cas contrari, especifica l'enllaç al que es pot accedir públicament segons la guia de servei que vosté utilitza.\nPer l'ús d'S3 utilitza 'https://.s3.amazonaws.com' I per a GCS o serveis equivalents utilitza 'https://storage.googleapis.com/'." -objectStorageBucket: "Dipòsit " -objectStorageBucketDesc: "Escriu el nom del dipòsit que fas servir al teu proveïdor d'emmagatzematge " -objectStoragePrefix: "Prefix" -objectStoragePrefixDesc: "Els fitxers es deixaren a directoris amb aquest prefix" -objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "Deixa'l buit si fas servir AWS S3, si no és així específica un punt d'entrada com '' o ':', depenent del servei que facis servir." -objectStorageRegion: "Regió " -objectStorageRegionDesc: "Especifica una regió com 'xx-east-1'. Si el teu servei no diferència regions has de posar 'us-east-1'. Deixa'l buit si fas servir variables d'entorn o un arxiu de configuració d'AWS." -objectStorageUseSSL: "Fes servir SSL" -objectStorageUseSSLDesc: "Desactiva'l si no tens pensat fer servir HTTPS per les connexions de l'API" -objectStorageUseProxy: "Connectar-se mitjançant un Proxy" -objectStorageUseProxyDesc: "Desactiva'l si no faràs servir un Proxy per les connexions de l'API" -objectStorageSetPublicRead: "Configurar les pujades com públiques " -s3ForcePathStyleDesc: "Si s3ForcePathStyle es troba activat el nom del cubell s'haurà d'especificar com a part de l'adreça URL en comptes del nom del servidor. Podria ser que necessitis activar aquesta opció quan facis servir serveis com ara l'allotjament a un servidor propi." -serverLogs: "Registres del servidor" -deleteAll: "Elimina-ho tot" -showFixedPostForm: "Mostrar el formulari per escriure a l'inici de la línia de temps" -showFixedPostFormInChannel: "Mostrar el formulari d'escriptura al principi de la línia de temps (Canals)" -withRepliesByDefaultForNewlyFollowed: "Inclou les respostes d'usuaris nous que segueixes a la línia de temps per defecte." newNoteRecived: "Hi ha publicacions noves" -newNote: "Notes noves" -sounds: "Sons" -sound: "So" -notificationSoundSettings: "Configuració del so de notificació" -listen: "Escoltar" -none: "Res" -showInPage: "Mostrar a la pàgina " -popout: "Finestra emergent" -volume: "Volum" -masterVolume: "Volum principal" -notUseSound: "Sense so" -useSoundOnlyWhenActive: "Reproduir sons només quan Misskey estigui actiu" -details: "Detalls" -renoteDetails: "Més informació sobre l'impuls " -chooseEmoji: "Tria un emoji" -unableToProcess: "L'operació no pot ser completada " -recentUsed: "Utilitzat recentment" -install: "Instal·lació " -uninstall: "Desinstal·la" -installedApps: "Aplicacions autoritzades " -nothing: "No hi ha res per veure aquí " installedDate: "Data d'instal·lació" -lastUsedDate: "Utilitzat per última vegada" state: "Estat" sort: "Ordena" ascendingOrder: "Ascendent" descendingOrder: "Descendent" -scratchpad: "Bloc de proves" -scratchpadDescription: "El bloc de proves proporciona un entorn experimental per AiScript. Pot escriure i verificar els resultats que interactuen amb Misskey." -uiInspector: "Inspector de la interfície" -uiInspectorDescription: "Podeu visualitzar una llista d'elements UI presents en la memòria. Els components de la interfície d'usuari són generats per les funcions Ui:C:." -output: "Sortida" -script: "Script" -disablePagesScript: "Desactivar AiScript a les pàgines " -updateRemoteUser: "Actualitzar la informació de l'usuari remot" -unsetUserAvatar: "Desactiva l'avatar " -unsetUserAvatarConfirm: "Segur que vols desactivar l'avatar?" -unsetUserBanner: "Desactiva el bàner " -unsetUserBannerConfirm: "Segur que vols desactivar el bàner?" -deleteAllFiles: "Esborra tots els arxius" -deleteAllFilesConfirm: "Segur que vols esborrar tots els arxius?" -removeAllFollowing: "Deixa de seguir tots els usuaris que segueixes" -removeAllFollowingDescription: "El fet d'executar això, et farà deixar de seguir a tots els usuaris de {host}. Si us plau, executa això si l'amfitrió, per exemple, ja no existeix." -userSuspended: "Aquest usuari ha sigut suspès" -userSilenced: "Aquest usuari està sent silenciat" -yourAccountSuspendedTitle: "Aquest compte és suspès" -yourAccountSuspendedDescription: "Aquest compte ha sigut suspès a causa de la violació de les condicions d'ús o similars. Contacta l'administrador si en vol saber més. Si us plau, no en faci un altre compte." -tokenRevoked: "Codi de seguretat no vàlid" -tokenRevokedDescription: "La petició més recent ha estat denegada perquè contenia un codi de seguretat no vàlid. Actualitza la pàgina i torna-ho a provar." -accountDeleted: "Compte eliminat amb èxit" -accountDeletedDescription: "Aquest compte ha sigut eliminat" -menu: "Menú" -divider: "Divisor" -addItem: "Afegir element" -rearrange: "Torna a ordenar" -relays: "Relés" -addRelay: "Afegeix relés" -inboxUrl: "Enllaç de la safata d'entrada" -addedRelays: "Relés afegits" -serviceworkerInfo: "És obligatòria l'activació per a obtenir notificacions push" deletedNote: "Publicacions eliminades" invisibleNote: "Publicacions amagades" -enableInfiniteScroll: "Carrega més automàticament\n" -visibility: "Visibilitat" -poll: "Enquesta" -useCw: "Amaga el contingut" -enablePlayer: "Obre el reproductor de vídeo" -disablePlayer: "Tanca el reproductor de vídeo" -expandTweet: "Expandir post" -themeEditor: "Editor de temes" -description: "Descripció" -describeFile: "Afegeix una descripció " -enterFileDescription: "Escriu un peu de foto" -author: "Autor" -leaveConfirm: "Hi ha canvis sense guardar. Els vols descartar?" -manage: "Administració" -plugins: "Extensions" -preferencesBackups: "Configuracions de les Còpies de seguretat" -deck: "Escriptori" -undeck: "Tanca el tauler" -useBlurEffectForModal: "Utilitzar l'efecte de difuminació a modals" -useFullReactionPicker: "Utilitza el cercador de reaccions d'escala sencera" -width: "Amplada" -height: "Alçària" -large: "Gran" -medium: "Mitjà" -small: "Petit" -generateAccessToken: "Genera codi d'accés" -permission: "Permisos" -adminPermission: "Permisos d'administrador " -enableAll: "Habilita tot" -disableAll: "Deshabilita tot" -tokenRequested: "Donar accés al compte" -pluginTokenRequestedDescription: "Aquest connector podrà fer servir tots els permisos configurats aquí." -notificationType: "Tipus de notificació " -edit: "Editar" -emailServer: "Servidor de correu electrònic " -enableEmail: "Activar l'enviament de correus electrònics " -emailConfigInfo: "Es fa servir per confirmar el teu correu quan et registres o oblides la contrasenya " -email: "Correu electrònic" -emailAddress: "Adreça de correu electrònic" -smtpConfig: "Configuració del servidor SMTP" smtpHost: "Amfitrió" -smtpPort: "Port" smtpUser: "Nom d'usuari" smtpPass: "Contrasenya" -emptyToDisableSmtpAuth: "No omplis el nom d'usuari i la contrasenya si vols deshabilitar l'autenticació SMTP" -smtpSecure: "Fes servir SSL/TLS per connexions SMTP" -smtpSecureInfo: "Desactiva això quan facis servir connexions STARTTLS" -testEmail: "Prova l'enviament de correu " -wordMute: "Silenciar paraules " -wordMuteDescription: "Minimitza les notes que contenen la paraula o frase especificada. Les notes minimitzades poden visualitzar-se fent clic sobre elles." -hardWordMute: "Silenciar paraules fortes" -showMutedWord: "Mostrar paraules silenciades" -hardWordMuteDescription: "Oculta les notes que contenen la paraula o frase especificada. A diferència de Silenciar paraula, la nota quedarà completament oculta a la vista." -regexpError: "Error de l'expressió regular " -regexpErrorDescription: "S'ha produït un error a l'expressió regular a la línia {line} de les paraules silenciades {tab}:" -instanceMute: "Silenciar servidor" -userSaysSomething: "{name} n'ha dit alguna cosa" -userSaysSomethingAbout: "{name} està parlant sobre \"{word}\"" -makeActive: "Activar" -display: "Veure" -copy: "Copiar" -copiedToClipboard: "Copiat al porta papers" -metrics: "Mètriques" -overview: "Visió General" -logs: "Registres" -delayed: "Endarrerits " -database: "Bases de dades" -channel: "Canals" -create: "Crear" -notificationSetting: "Paràmetres de notificacions" -notificationSettingDesc: "Selecciona els tipus de notificacions que es mostraran" -useGlobalSetting: "Fer servir la configuració global" -useGlobalSettingDesc: "Si s'activa, es farà servir la configuració de notificacions del teu comte. Si no s'activa es poden fer configuracions individuals." -other: "Altres" -regenerateLoginToken: "Regenerar clau de seguretat d'inici de sessió" -regenerateLoginTokenDescription: "Regenera la clau de seguretat que es fa servir internament durant l'inici de sessió. Normalment aquesta acció no és necessària. Si es regenera es tancarà la sessió a tots els dispositius amb una sessió activa." -theKeywordWhenSearchingForCustomEmoji: "Cercar un emoji personalitzat " -setMultipleBySeparatingWithSpace: "Separa múltiples entrades amb un espai" -fileIdOrUrl: "ID de l'arxiu o URL" -behavior: "Comportament" -sample: "Mostrar" -abuseReports: "Denúncies " -reportAbuse: "Denuncia un abús " -reportAbuseRenote: "Denuncia una renota" -reportAbuseOf: "Denuncia a {name}" -fillAbuseReportDescription: "Omple els detalls sobre aquesta denúncia. Si la denúncia és sobre una nota en concret inclou l'adreça URL." -abuseReported: "La teva denúncia s'ha enviat. Moltes gràcies." -reporter: "Denunciant " -reporteeOrigin: "Origen de la denúncia " -reporterOrigin: "Origen del denunciant" -send: "Envia" -openInNewTab: "Obre a una pestanya nova" -openInSideView: "Obre a una vista lateral" -defaultNavigationBehaviour: "Navegació per defecte" -editTheseSettingsMayBreakAccount: "Editar aquestes opcions pot deixar inoperatiu el teu compte" -instanceTicker: "Informació de notes de la instància " -waitingFor: "Esperant {x}" -random: "Aleatori " -system: "Sistema" -switchUi: "Canviar la interfície" -desktop: "Escriptori" -clip: "Retalls" -createNew: "Crear" -optional: "Opcional" -createNewClip: "Crear un nou Retall" -unclip: "Treure Retall" -confirmToUnclipAlreadyClippedNote: "Aquesta nota ja és inclosa al Retall \"{name}\". Vols treure-la d'aquest retall?" -public: "Públic " -private: "Privat" -i18nInfo: "Misskey està sent traduït a diferents idiomes per voluntaris. Pots ajudar aquí {link}." -manageAccessTokens: "Administrar claus de seguretat d'accés " -accountInfo: "Informació del compte" -notesCount: "Comptador de notes" -repliesCount: "Nombre de respostes" renotesCount: "Impulsos fets" -repliedCount: "Nombre de respostes rebudes" renotedCount: "Impulsos rebuts" -followingCount: "Nombre de comptes que segueixes" -followersCount: "Nombre de seguidors" -sentReactionsCount: "Nombre de reaccions enviades" -receivedReactionsCount: "Nombre de reaccions rebudes" -pollVotesCount: "Nombre de vots enviats a enquestes" -pollVotedCount: "Nombre de vots rebuts a les enquestes" -yes: "Sí " -no: "No" -driveFilesCount: "Nombre de fitxers al Disc" -driveUsage: "Utilització de l'espai del Disc" -noCrawle: "Rebutjar la indexació dels buscadors" -noCrawleDescription: "No permetis que els buscadors indexin el teu perfil, notes, pàgines, etc." -lockedAccountInfo: "Tret que establiu la visibilitat de la nota a \"Només seguidors\", les vostres notes seran visibles per qualsevol persona, fins i tot si heu d'aprovar els seguidors manualment" -alwaysMarkSensitive: "Marcar com a sensible per defecte" -loadRawImages: "Carregar les imatges originals en comptes de miniatures " -disableShowingAnimatedImages: "No reproduir imatges animades" -highlightSensitiveMedia: "Ressalta els medis marcats com a sensibles" -verificationEmailSent: "S'ha enviat un correu electrònic de verificació. Fes clic a l'enllaç per completar la verificació." -notSet: "Sense definir" -emailVerified: "El correu electrònic s'ha verificat" -noteFavoritesCount: "Nombre de notes favorites " -pageLikesCount: "Nombre de Pàgines que t'agraden " -pageLikedCount: "Nombre d'agraïments rebuts a les Pàgines " -contact: "Contacte" -useSystemFont: "Fes servir la font per defecte del sistema" -clips: "Retalls" -experimentalFeatures: "Característiques experimentals" -experimental: "Experimental" -thisIsExperimentalFeature: "Aquesta és una característica experimental. La seva funcionalitat pot canviar, i pot ser que no funcioni degudament." -developer: "Programador" -makeExplorable: "Fes que el compte sigui visible a la secció \"Explorar\"" -makeExplorableDescription: "Si desactives aquesta opció, el teu compte no sortirà a la secció \"Explorar\"" -duplicate: "Duplicat" -left: "Esquerra" -center: "Centre" -wide: "Gran" -narrow: "Estret" -reloadToApplySetting: "Aquest ajust només s'aplicarà després de recarregar la pàgina. Vols fer-ho ara?" -needReloadToApply: "Es requereix recarregar per reflectir aquesta opció " -needToRestartServerToApply: "És necessari reiniciar el servidor perquè tinguin efecte els canvis." -showTitlebar: "Mostra la barra del títol " clearCache: "Esborra la memòria cau" -onlineUsersCount: "{n} Usuaris es troben en línia " -nUsers: "{n} Usuaris" -nNotes: "{n} Notes" -sendErrorReports: "Enviar informes d'error " -sendErrorReportsDescription: "Quan s'activa, es compartirà amb Misskey informació detallada de l'error quan es trobi un problema això farà pujar la qualitat de Misskey.\nAixò inclourà informació com la versió del SO que fas servir, el navegador web que fas servir, la teva activitat a Misskey, etc." -myTheme: "El meu tema" -backgroundColor: "Color de fons" -accentColor: "Color principal" -textColor: "Color del text" -saveAs: "Desar com..." -advanced: "Avançat" -advancedSettings: "Configuració avançada" -value: "Valor" -createdAt: "Creat el" -updatedAt: "Actualitzat el" -saveConfirm: "Desar canvis?" -deleteConfirm: "Segur que vols esborrar?" -invalidValue: "Valor invàlid." -registry: "Registre " -closeAccount: "Tancar el compte" -currentVersion: "Versió actual" -latestVersion: "Versió nova" -youAreRunningUpToDateClient: "Ja estàs fent servir la versió més recent del client." -newVersionOfClientAvailable: "Tens disponible una versió del client més recent." -usageAmount: "Ús " -capacity: "Capacitat" -inUse: "Fet servir" -editCode: "Editar el codi" -apply: "Aplicar" -receiveAnnouncementFromInstance: "Rep notificacions d'aquesta instància " -emailNotification: "Notificacions per correu electrònic " -publish: "Publicar" -inChannelSearch: "Cerca al canal" -useReactionPickerForContextMenu: "Fes clic al botó dret del ratolí per obrir el menú de reaccions" -typingUsers: "{users} està/estàn Escrivint " -jumpToSpecifiedDate: "Ves a una data concreta" showingPastTimeline: "Estàs veient una línia de temps antiga" -clear: "Tornar" -markAllAsRead: "Marcar tot com llegit" -goBack: "Tornar" -unlikeConfirm: "Vols esborrar el teu m'agrada?" -fullView: "Vista completa." -quitFullView: "Sortir de la vista completa" -addDescription: "Afegeix una descripció " -userPagePinTip: "Podeu seleccionar \"Fixar al perfil\" del menú de notes individuals per mostrar les notes aquí." -notSpecifiedMentionWarning: "Aquesta nota esmenta usuaris que no es troben com a destinataris" info: "Informació" -userInfo: "Informació de l'usuari" -unknown: "Desconegut" -onlineStatus: "Connectat" -hideOnlineStatus: "Ocultar l'estat de connexió" -hideOnlineStatusDescription: "Ocultant el teu estat de connexió redueix les funcionalitats d'algunes funcions com la cerca." -online: "Connectat" -active: "Actiu" -offline: "Desconnectat" -notRecommended: "No recomanat" -botProtection: "Protecció contra bots" -instanceBlocking: "Instàncies blocades/silenciades" -selectAccount: "Seleccionar un compte" -switchAccount: "Canviar de compte" -enabled: "Activat" -disabled: "Desactivat" -quickAction: "Accions ràpides" user: "Usuaris" -administration: "Administració" -accounts: "Comptes" -switch: "Canvia" -noMaintainerInformationWarning: "La informació de l'administrador no s'ha configurat" -noInquiryUrlWarning: "No s'ha desat l'URL de consulta." -noBotProtectionWarning: "La protecció contra bots no s'ha configurat." -configure: "Configurar" -postToGallery: "Crear una nova publicació a la galeria" -postToHashtag: "Pública a aquesta etiqueta" -gallery: "Galeria" -recentPosts: "Articles recents" -popularPosts: "Articles populars" -shareWithNote: "Comparteix amb una nota" -ads: "Publicitat " -expiration: "" -startingperiod: "Inici" -memo: "Recordatori" -priority: "Prioritat" -high: "Alta" -middle: "Mitjà" -low: "Baixa" -emailNotConfiguredWarning: "Adreça de correu electrònic" -ratio: "Proporció" -previewNoteText: "Mostrar vista prèvia" -customCss: "CSS personalitzat" -customCssWarn: "Aquesta configuració només hauries de configurar-la si saps que fas. Si poses valors inadequats pots fer que el client deixi de funcionar correctament." global: "Global" -squareAvatars: "Mostrar avatars quadrats" -sent: "Envia" -received: "Rebut" -searchResult: "Resultats de la cerca" -hashtags: "Etiquetes" -troubleshooting: "Solucionar problemes" -useBlurEffect: "Fes servir efectes de desenfocament a la interfície" -learnMore: "Saber més " -misskeyUpdated: "Misskey s'ha actualitzat " -whatIsNew: "Mostra canvis" -translate: "Traduir " -translatedFrom: "Traduït del {x}" -accountDeletionInProgress: "S'està produint l'eliminació del compte" -usernameInfo: "Un nom que identifiqui el teu compte d'altres en aquest servidor. Pots fer servir lletres (a~z, A~Z), números (0~9) i guions baixos (_). Els noms d'usuari no es poden canviar després." -aiChanMode: "Mode IA" -devMode: "Mode desenvolupador" -keepCw: "Mantenir els avisos de contingut" -pubSub: "Comptes Pub/Sub" -lastCommunication: "Última comunicació " -resolved: "Resolt" -unresolved: "Sense resoldre" -breakFollow: "Deixar de seguir" -breakFollowConfirm: "Vols deixar de seguir?" -itsOn: "Activat" -itsOff: "Desactivat" -on: "Activar" -off: "Desactivar" -emailRequiredForSignup: "Demanar correu electrònic per registrar-se " -unread: "Sense llegir" -filter: "Filtrar" -controlPanel: "Tauler de control" -manageAccounts: "Gestionar comptes" -makeReactionsPublic: "Reaccions públiques " -makeReactionsPublicDescription: "Això fa que totes les teves reaccions siguin visibles públicament " -classic: "Clàssic " -muteThread: "Silenciar el fil" -unmuteThread: "Deixar de silenciar el fil" -followingVisibility: "Visibilitat dels seguiments" -followersVisibility: "Visibilitat dels seguidors" -continueThread: "Veure la continuació del fil" -deleteAccountConfirm: "Això eliminarà el teu compte irreversiblement. Procedir?" -incorrectPassword: "Contrasenya incorrecta." -incorrectTotp: "La contrasenya no és correcta, o ha caducat." -voteConfirm: "Confirma el teu vot \"{choice}\"" -hide: "Amagar" -useDrawerReactionPickerForMobile: "Mostrar el selector de reaccions com un calaix al mòbil " -welcomeBackWithName: "Benvingut de nou, {name}" -clickToFinishEmailVerification: "Si us plau, fes clic a [{ok}] per completar la verificació per correu electrònic " -overridedDeviceKind: "Tipus de dispositiu" -smartphone: "Mòbil " -tablet: "Tauleta" -auto: "Automàtic " -themeColor: "Color del tema" -size: "Mida" -numberOfColumn: "Nombre de columnes" searchByGoogle: "Cercar" -instanceDefaultLightTheme: "Tema clar per defecte de tota la instància " -instanceDefaultDarkTheme: "Tema fosc per defecte de tota la instància " -instanceDefaultThemeDescription: "Introdueix el codi del tema en format d'objecte" -mutePeriod: "Duració del silenci" -period: "Límit de temps" -indefinitely: "Permanent" -tenMinutes: "10 minuts" -oneHour: "1 hora" -oneDay: "Un dia" -oneWeek: "Una setmana" -oneMonth: "Un mes" -threeMonths: "3 mesos" -oneYear: "1 any" -threeDays: "3 dies" -reflectMayTakeTime: "Això pot trigar una estona a tenir efecte" -failedToFetchAccountInformation: "No es pot obtenir la informació del compte" -rateLimitExceeded: "S'ha arribat al màxim de peticions" -cropImage: "Retalla la imatge" -cropImageAsk: "Vols retallar la imatge?" -cropYes: "Retallar" -cropNo: "Fer servir tal qual" file: "Fitxers" -recentNHours: "Últimes {n} hores" -recentNDays: "Últims {n} dies" -noEmailServerWarning: "Correu electrònic del servidor sense configurar" -thereIsUnresolvedAbuseReportWarning: "Hi ha informes sense solucionar." -recommended: "Recomanat" -check: "Verificar" -driveCapOverrideLabel: "Canvia la capacitat del Disc per aquest usuari" -driveCapOverrideCaption: "Restableix la mida original posant un valor de 0 o menys." -requireAdminForView: "Has de ser administrador per poder veure això." -isSystemAccount: "Un compte creat i operat automàticament pel sistema." -typeToConfirm: "Si us plau, escriu {x} per confirmar" -deleteAccount: "Esborrar el compte" -document: "Documentació" -numberOfPageCache: "Nombre de pàgines a la memòria cau" -numberOfPageCacheDescription: "Incrementant aquest nombre farà que millori l'experiència de l'usuari, però es farà servir més memòria al dispositiu de l'usuari." -logoutConfirm: "Vols sortir?" -logoutWillClearClientData: "En tancar la sessió, la informació del client al navegador s'esborrarà. Per garantir que la informació de configuració es pugui restaurar en tornar a iniciar sessió activa la còpia de seguretat automàtica de la configuració." -lastActiveDate: "Fet servir per última vegada" -statusbar: "Barra d'estat" -pleaseSelect: "Selecciona una opció" -reverse: "Invertir" -colored: "Colorit" -refreshInterval: "Interval d'actualització " -label: "Etiqueta" -type: "Tipus" -speed: "Velocitat" -slow: "Lent" -fast: "Ràpid " -sensitiveMediaDetection: "Detecció de contingut sensible" -localOnly: "Només local" -remoteOnly: "Només remot" -failedToUpload: "Ha fallat la pujada" -cannotUploadBecauseInappropriate: "Aquest fitxer no es pot pujar perquè s'ha trobat que algunes parts són inapropiades." -cannotUploadBecauseNoFreeSpace: "Ha fallat la pujada del fitxer perquè no hi ha capacitat al Disc." -cannotUploadBecauseExceedsFileSizeLimit: "Aquest fitxer no es pot pujar perquè supera la mida permesa." -cannotUploadBecauseUnallowedFileType: "Impossible pujar l'arxiu no és un tipus de fitxer autoritzat." -beta: "Proves" -enableAutoSensitive: "Marcar com a sensible automàticament " -enableAutoSensitiveDescription: "Permet la detecció i el marcat automàtic dels mitjans sensibles fent servir aprenentatge automàtic quan sigui possible. Si aquesta opció es troba desactivada potser que estigui activada per a tota la instància. " -activeEmailValidationDescription: "Activa la validació estricta de comptes de correu electrònic, inclou la validació d'adreces d'un sol ús i si es possible comunicar-se amb aquestes. Quan es troba desactivada només es vàlida el format del correu electrònic." -navbar: "Barra de navegació " -shuffle: "Aleatori" -account: "Compte" -move: "Mou" -pushNotification: "Enviament de notificacions" -subscribePushNotification: "Activar l'enviament de notificacions" -unsubscribePushNotification: "Desactivar l'enviament de notificacions" -pushNotificationAlreadySubscribed: "L'enviament de notificacions ja és activat" -pushNotificationNotSupported: "El teu navegador o la teva instància no suporta l'enviament de notificacions " -sendPushNotificationReadMessage: "Esborrar les notificacions enviades quan s'hagin llegit" -sendPushNotificationReadMessageCaption: "Això pot fer que el teu dispositiu consumeixi més bateria" -windowMaximize: "Maximitzar " -windowMinimize: "Minimitzar" -windowRestore: "Restaurar" -caption: "Peu de foto" -loggedInAsBot: "Identificat com a bot" -tools: "Eines" -cannotLoad: "No es pot carregar" -numberOfProfileView: "Visualitzacions del perfil" -like: "M'agrada " -unlike: "Treure m'agrada " -numberOfLikes: "M'agraden " -show: "Veure" -neverShow: "No mostrar més " -remindMeLater: "Recorda-m'ho més tard" -didYouLikeMisskey: "T'està agradant Misskey?" -pleaseDonate: "A {host} fem servir el software lliure Misskey. Considera fer un donatiu a Misskey perquè pugui continuar el seu desenvolupament!" -correspondingSourceIsAvailable: "El codi font corresponent està disponible a {anchor}." -roles: "Rols" -role: "Rols" -noRole: "No s'han trobat rols" -normalUser: "Usuari normal" -undefined: "Sense definir" -assign: "Assignar " -unassign: "Treure" -color: "Color" -manageCustomEmojis: "Gestiona els emojis personalitzats" -manageAvatarDecorations: "Gestiona les decoracions dels avatars " -youCannotCreateAnymore: "Has arribat al màxim de creacions" -cannotPerformTemporary: "Temporalment no disponible" -cannotPerformTemporaryDescription: "Aquesta acció no es pot dur a terme temporalment per arribar al seu límit d'execució. Pots esperar una mica i tornar-ho a intentar." -invalidParamError: "Paràmetres incorrectes " -invalidParamErrorDescription: "Els paràmetres demanats no són correctes. Normalment això es deu a un error, però també pot ser a alguna entrada excedint els límits o similar." -permissionDeniedError: "Operació no permesa " -permissionDeniedErrorDescription: "Aquest compte no té suficients permisos per dur a terme aquesta acció " -preset: "Predefinit" -selectFromPresets: "Escull des dels predefinits" -achievements: "Assoliments" -gotInvalidResponseError: "Resposta del servidor invàlida " -gotInvalidResponseErrorDescription: "No es pot contactar amb el servidor o potser es troba fora de línia per manteniment. Provar-ho de nou més tard." -thisPostMayBeAnnoying: "Aquesta nota pot ser molesta per algú." -thisPostMayBeAnnoyingHome: "Publicar a la línia de temps d'Inici" -thisPostMayBeAnnoyingCancel: "Cancel·lar " -thisPostMayBeAnnoyingIgnore: "Publicar de totes maneres" -collapseRenotes: "Col·lapsar les renotes que ja has vist" -collapseRenotesDescription: "Col·lapse les notes a les quals ja has reaccionat o que ja has renotat" -internalServerError: "Error intern del servidor" -internalServerErrorDescription: "El servidor ha fallat de manera inexplicable." -copyErrorInfo: "Copiar la informació de l'error " -joinThisServer: "Registra't en aquesta instància " -exploreOtherServers: "Cerca una altra instància " -letsLookAtTimeline: "Dona una ullada a la línia de temps" -disableFederationConfirm: "Vols treure la federació?" -disableFederationConfirmWarn: "Fins i tot traient la federació, les publicacions continuaren sent públiques, a no ser que es digui el contrari. Normalment no has de tocar això." -disableFederationOk: "Desactivar" -invitationRequiredToRegister: "Aquesta instància només permet el registre per invitació. Per registrar-te has d'introduir el codi d'invitació." -emailNotSupported: "Aquesta instància no suporta l'enviament de correus electrònics " -postToTheChannel: "Publicar a un Canal" -cannotBeChangedLater: "Això ja no es podrà canviar." -reactionAcceptance: "Acceptació de reaccions " -likeOnly: "Només m'agraden " -likeOnlyForRemote: "Tot (només m'agraden d'instàncies remotes)" -nonSensitiveOnly: "Només sense contingut sensible" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Només contingut no sensible (Només m'agraden d'instàncies remotes)" -rolesAssignedToMe: "Rols assignats " -resetPasswordConfirm: "Vols canviar la teva contrasenya?" -sensitiveWords: "Paraules sensibles" -sensitiveWordsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves." -sensitiveWordsDescription2: "Fent servir espais crearà expressions AND si l'expressió s'envolta amb barres inclinades es converteix en una expressió regular." -prohibitedWords: "Paraules prohibides" -prohibitedWordsDescription: "Quan intenteu publicar una Nota que conté una paraula prohibida, feu que es converteixi en un error. Es poden dividir i establir múltiples línies." -prohibitedWordsDescription2: "Fent servir espais crearà expressions AND si l'expressió s'envolta amb barres inclinades es converteix en una expressió regular." -hiddenTags: "Etiquetes ocultes" -hiddenTagsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves." -notesSearchNotAvailable: "La cerca de notes no es troba disponible." -license: "Llicència" -unfavoriteConfirm: "Esborrar dels favorits?" -myClips: "Els meus retalls" -drivecleaner: "Netejador de Disc" -retryAllQueuesNow: "Prova de nou d'executar totes les cues" -retryAllQueuesConfirmTitle: "Tornar a intentar-ho tot?" -retryAllQueuesConfirmText: "Això farà que la càrrega del servidor augmenti temporalment." -enableChartsForRemoteUser: "Generar gràfiques d'usuaris remots" -enableChartsForFederatedInstances: "Generar gràfiques d'instàncies remotes" -enableStatsForFederatedInstances: "Activa les estadístiques de les instàncies remotes federades" -showClipButtonInNoteFooter: "Afegir \"Retall\" al menú d'acció de la nota" -reactionsDisplaySize: "Mida de les reaccions" -limitWidthOfReaction: "Limitar l'amplada màxima de la reacció i mostrar-les en una mida reduïda " -noteIdOrUrl: "ID o URL de la nota" -video: "Vídeo" -videos: "Vídeos " -audio: "So" -audioFiles: "So" -dataSaver: "Economitzador de dades" -accountMigration: "Migració del compte" -accountMoved: "Aquest usuari té un compte nou:" -accountMovedShort: "Aquest compte ha sigut migrat" -operationForbidden: "Operació no permesa " -forceShowAds: "Mostrar publicitat sempre " -addMemo: "Afegir recordatori" -editMemo: "Editar recordatori" -reactionsList: "Reaccions" -renotesList: "Llistat d'impulsos " -notificationDisplay: "Notificacions" -leftTop: "Dalt a l'esquerra " -rightTop: "Dalt a la dreta " -leftBottom: "A baix a l'esquerra" -rightBottom: "A baix a la dreta" -stackAxis: "Apilar en direcció " -vertical: "Vertical" -horizontal: "Horitzontal " -position: "Posició " -serverRules: "Regles del servidor" -pleaseConfirmBelowBeforeSignup: "Per obrir un compte en aquest servidor, has de llegir i acceptar el següent." -pleaseAgreeAllToContinue: "Has d'acceptar tots els camps de dalt per poder continuar." -continue: "Continuar" -preservedUsernames: "Noms d'usuaris reservats" -preservedUsernamesDescription: "Llistat de noms d'usuaris que no es poden fer servir separats per salts de linia. Aquests noms d'usuaris no estaran disponibles quan es creï un compte d'usuari normal, però els administradors els poden fer servir per crear comptes manualment. Per altre banda els comptes ja creats amb aquests noms d'usuari no es veure'n afectats." -createNoteFromTheFile: "Escriu una nota incloent aquest fitxer" -archive: "Arxiu" -archived: "Arxivat" -unarchive: "Desarxivar" -channelArchiveConfirmTitle: "Vols arxivar {name}?" -channelArchiveConfirmDescription: "Un Canal arxivat no apareixerà a la llista de canals o als resultats de cerca. Tampoc es poden afegir noves entrades." -thisChannelArchived: "Aquest Canal ha sigut arxivat." -displayOfNote: "Mostrar notes" -initialAccountSetting: "Configuració del perfil" -youFollowing: "Segueixes " -preventAiLearning: "Descartar l'ús d'aprenentatge automàtic (IA Generativa)" -preventAiLearningDescription: "Demanar els indexadors no fer servir els texts, imatges, etc. en cap conjunt de dades per alimentar l'aprenentatge automàtic (IA Predictiva/ Generativa). Això s'aconsegueix afegint la etiqueta \"noai\" com a resposta HTML al contingut corresponent. Prevenir aquest ús totalment pot ser que no sigui aconseguit, ja que molts indexadors poden obviar aquesta etiqueta." -options: "Opcions" -specifyUser: "Especificar usuari" -lookupConfirm: "Vols fer una cerca?" -openTagPageConfirm: "Vols obrir una pàgina d'etiquetes?" -specifyHost: "Especifica un servidor" -failedToPreviewUrl: "Vista prèvia no disponible" -update: "Actualitzar" -rolesThatCanBeUsedThisEmojiAsReaction: "Rols que poden fer servir aquest emoji com a reacció " -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Si cap rol es especificat tothom ho pot fer servir" -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Aquests rols han de ser públics " -cancelReactionConfirm: "Vols esborrar la teva reacció?" -changeReactionConfirm: "Vols canviar la teva reacció?" -later: "Més tard" -goToMisskey: "Ves a Misskey" -additionalEmojiDictionary: "Diccionari d'emojis adicionals" -installed: "Instal·lats " -branding: "Marca" -enableServerMachineStats: "Publicar estadístiques del maquinari del servidor" -enableIdenticonGeneration: "Activar la generació d'icones d'identificació " -turnOffToImprovePerformance: "Desactivant aquesta opció es pot millorar el rendiment." -createInviteCode: "Crear codi d'invitació " -createWithOptions: "Crear invitació amb opcions" -createCount: "Comptador d'invitacions " -inviteCodeCreated: "Invitació creada" -inviteLimitExceeded: "Has sobrepassat el límit d'invitacions que pots crear." -createLimitRemaining: "Et queden {limit} invitacions restants" -inviteLimitResetCycle: "Cada {time} {limit} invitacions." -expirationDate: "Data de venciment" -noExpirationDate: "Sense data de venciment" -inviteCodeUsedAt: "Codi d'invitació fet servir el" -registeredUserUsingInviteCode: "Codi d'invitació fet servir per l'usuari " -waitingForMailAuth: "Esperant la verificació per correu electrònic " -inviteCodeCreator: "Invitació creada per" -usedAt: "Utilitzada el" -unused: "Sense utilitzar" -used: "Utilitzada" -expired: "Caducat" -doYouAgree: "Estàs d'acord?" -beSureToReadThisAsItIsImportant: "Llegeix això perquè és molt important." -iHaveReadXCarefullyAndAgree: "He llegit {x} i estic d'acord." -dialog: "Diàleg " -icon: "Icona" -forYou: "Per a tu" -currentAnnouncements: "Avisos actuals" -pastAnnouncements: "Avisos passats" -youHaveUnreadAnnouncements: "Tens informes per llegir." -useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey." -replies: "Respostes" -renotes: "Impulsos" -loadReplies: "Mostrar les respostes" -loadConversation: "Mostrar la conversació " -pinnedList: "Llista fixada" -keepScreenOn: "Mantenir la pantalla encesa" -verifiedLink: "La propietat de l'enllaç ha sigut verificada" -notifyNotes: "Notificar quan hi hagi notes noves" -unnotifyNotes: "Deixar de notificar quan hi hagi notes noves" -authentication: "Autenticació " -authenticationRequiredToContinue: "Si us plau autentificat per continuar" -dateAndTime: "Data i hora" -showRenotes: "Mostrar impulsos" -edited: "Editat" -notificationRecieveConfig: "Paràmetres de notificacions" -mutualFollow: "Seguidor mutu" -followingOrFollower: "Seguint o seguidor" -fileAttachedOnly: "Només notes amb adjunts" -showRepliesToOthersInTimeline: "Mostrar les respostes a altres a la línia de temps" -hideRepliesToOthersInTimeline: "Amagar les respostes a altres a la línia de temps" -showRepliesToOthersInTimelineAll: "Mostrar les respostes a altres a usuaris que segueixes a la línia de temps" -hideRepliesToOthersInTimelineAll: "Ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps" -confirmShowRepliesAll: "Aquesta opció no té marxa enrere. Vols mostrar les teves respostes a tots els que segueixes a la teva línia de temps?" -confirmHideRepliesAll: "Aquesta opció no té marxa enrere. Vols ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps?" -externalServices: "Serveis externs" -sourceCode: "Codi font" -sourceCodeIsNotYetProvided: "El codi font encara no es troba disponible. Contacta amb l'administrador per solucionar aquest problema." -repositoryUrl: "URL del repositori" -repositoryUrlDescription: "Si estàs fent servir Misskey tal com és (sense cap canvi al codi font), introdueix https://github.com/misskey-dev/misskey" -repositoryUrlOrTarballRequired: "Si no ofereixes cap repositori, publica un fitxer tarball. Dona una ullada a .config/example.yml per a més informació." -feedback: "Opinió" -feedbackUrl: "URL per a opinar" -impressum: "Impressum" -impressumUrl: "Adreça URL impressum" -impressumDescription: "A països, com Alemanya, la inclusió de la informació de contacte de l'operador (un Impressum) és requereix de manera legal per llocs comercials." -privacyPolicy: "Política de privacitat" -privacyPolicyUrl: "Adreça URL de la política de privacitat" -tosAndPrivacyPolicy: "Termes d'ús i política de privacitat" -avatarDecorations: "Decoracions dels avatars" -attach: "Adjuntar" -detach: "Eliminar" -detachAll: "Treure tot" -angle: "Angle" -flip: "Girar" -showAvatarDecorations: "Mostrar les decoracions dels avatars" -releaseToRefresh: "Deixar anar per actualitzar" -refreshing: "Recarregant..." -pullDownToRefresh: "Llisca cap a baix per recarregar" -useGroupedNotifications: "Mostrar les notificacions agrupades " -signupPendingError: "Hi ha hagut un problema verificant l'adreça de correu electrònic. L'enllaç pot haver caducat." -cwNotationRequired: "Si està activat \"Amagar contingut\" s'ha d'escriure una descripció " -doReaction: "Afegeix una reacció " -code: "Codi" -reloadRequiredToApplySettings: "És necessari recarregar la pàgina per aplicar els canvis." -remainingN: "Queden: {n}" -overwriteContentConfirm: "Vols substituir el contingut actual?" -seasonalScreenEffect: "Efectes de pantalla segons les estacions" -decorate: "Decorar" -addMfmFunction: "Afegeix funcions MFM" -enableQuickAddMfmFunction: "Activar accés ràpid per afegir funcions MFM" -bubbleGame: "Bubble Game" -sfx: "Efectes de so" -soundWillBePlayed: "Es reproduiran efectes de so" -showReplay: "Veure reproducció" -replay: "Reproduir" -replaying: "Reproduint" -endReplay: "Tanca la redifusió" -copyReplayData: "Copia les dades de la resposta" -ranking: "Classificació" -lastNDays: "Últims {n} dies" -backToTitle: "Torna al títol" -hemisphere: "Geolocalització" -withSensitive: "Incloure notes amb fitxers sensibles" -userSaysSomethingSensitive: "La publicació de {name} conte material sensible" -enableHorizontalSwipe: "Lliscar per canviar de pestanya" -loading: "S’està carregant" -surrender: "Cancel·lar " -gameRetry: "Torna a provar" -notUsePleaseLeaveBlank: "Si no voleu usar-ho, deixeu-ho en blanc" -useTotp: "Usa una contrasenya d'un sol ús" -useBackupCode: "Usa un codi de recuperació" -launchApp: "Inicia l'aplicació " -useNativeUIForVideoAudioPlayer: "Fes servir la UI del navegador quan reprodueixis vídeo i àudio " -keepOriginalFilename: "Desa el nom del fitxer original" -keepOriginalFilenameDescription: "Si desactives aquesta opció els noms dels fitxers se substituiran per una cadena aleatòria quan carreguis nous fitxers de forma automàtica." -noDescription: "No hi ha una descripció " -alwaysConfirmFollow: "Confirma sempre els seguiments" -inquiry: "Contacte" -tryAgain: "Intenta-ho més tard." -confirmWhenRevealingSensitiveMedia: "Confirmació quan revelis contingut sensible " -sensitiveMediaRevealConfirm: "Aquest contingut potser sensible. Segur que ho vols revelar?" -createdLists: "Llistes creades " -createdAntennas: "Antenes creades" -fromX: "De {x}" -genEmbedCode: "Obtenir el codi per incrustar" -noteOfThisUser: "Notes d'aquest usuari" -clipNoteLimitExceeded: "No es poden afegir més notes a aquest clip." -performance: "Rendiment" -modified: "Modificat" -discard: "Descarta" -thereAreNChanges: "Hi ha(n) {n} canvi(s)" -signinWithPasskey: "Inicia sessió amb Passkey" -unknownWebAuthnKey: "Passkey desconeguda" -passkeyVerificationFailed: "La verificació a fallat" -passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya." -messageToFollower: "Missatge als meus seguidors" -target: "Assumpte " -testCaptchaWarning: "És una característica dissenyada per a la prova de CAPTCHA. No l'utilitzes en l'entorn real." -prohibitedWordsForNameOfUser: "Noms prohibits per escollir noms d'usuari " -prohibitedWordsForNameOfUserDescription: "Si qualsevol d'aquestes paraules es troben a un nom d'usuari la creació de l'usuari no es durà a terme. Als moderadors no els afecta aquesta restricció." -yourNameContainsProhibitedWords: "El nom conté paraules prohibides " -yourNameContainsProhibitedWordsDescription: "Si de veritat vols fer servir aquest nom posat en contacte amb l'administrador." -thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autor requereix l'inici de sessió per poder veure" -lockdown: "Bloquejat" -pleaseSelectAccount: "Seleccionar un compte" -availableRoles: "Roles disponibles " -acknowledgeNotesAndEnable: "Activa'l després de comprendre els possibles perills." -federationSpecified: "Aquest servidor treballa amb una federació de llistes blanques. No pot interactuar amb altres servidors que no siguin els especificats per l'administrador." -federationDisabled: "La unió es troba deshabilitada en aquest servidor. No es pot interactuar amb usuaris d'altres servidors." -confirmOnReact: "Confirmar en reaccionar" -reactAreYouSure: "Vols reaccionar amb \"{emoji}\"?" -markAsSensitiveConfirm: "Vols marcar aquest contingut com a sensible?" -unmarkAsSensitiveConfirm: "Vols deixar de marcar com a sensible aquest contingut?" -preferences: "Preferències " -accessibility: "Accessibilitat " -preferencesProfile: "Perfil de configuració " -copyPreferenceId: "Copiar l'ID de la configuració " -resetToDefaultValue: "Restaura al valor per defecte " -overrideByAccount: "Anul·lar per compte" -untitled: "Sense títol " -noName: "No hi ha un nom disponible " -skip: "Ometre " -restore: "Restaurar " -syncBetweenDevices: "Sincronització entre dispositius" -preferenceSyncConflictTitle: "Els valors de la configuració ja existeixen al dispositiu" -preferenceSyncConflictText: "Un element de la configuració amb sincronització activada desa els seus valors al servidor, però s'ha trobat un valor a la configuració desat al servidor per aquest element de la configuració. Quin valor us sobreescriure?" -preferenceSyncConflictChoiceMerge: "Integració " -preferenceSyncConflictChoiceServer: "Valors de configuració del servidor" -preferenceSyncConflictChoiceDevice: "Punts d'ajustos del dispositiu " -preferenceSyncConflictChoiceCancel: "Cancel·lar l'activació de la sincronització " -paste: "Pegar" -emojiPalette: "Calaix d'emojis" -postForm: "Formulari de publicació" -textCount: "Nombre de caràcters " -information: "Informació" -chat: "Xat" -migrateOldSettings: "Migrar la configuració anterior" -migrateOldSettings_description: "Normalment això es fa automàticament, però si la transició no es fa, el procés es pot iniciar manualment. S'esborrarà la configuració actual." -compress: "Comprimir " -right: "Dreta" -bottom: "A baix " -top: "A dalt " -embed: "Incrustar" -settingsMigrating: "Estem migrant la teva configuració. Si us plau espera un moment... (També pots fer la migració més tard, manualment, anant a Preferències → Altres → Migrar configuració antiga)" -readonly: "Només lectura" -goToDeck: "Tornar al tauler" -federationJobs: "Treballs sindicats " -driveAboutTip: "Al Disc veure's una llista de tots els arxius que has anat pujant.
\nPots tornar-los a fer servir adjuntant-los a notes noves o pots adelantar-te i pujar arxius per publicar-los més tard!
\nTingués en compte que si esborres un arxiu també desapareixerà de tots els llocs on l'has fet servir (notes, pàgines, avatars, imatges de capçalera, etc.)
\nTambé pots crear carpetes per organitzar les." -scrollToClose: "Desplaçar per tancar" -advice: "Consell" -realtimeMode: "Mode en temps real" -turnItOn: "Activar" -turnItOff: "Desactivar" -emojiMute: "Silenciar emojis" -emojiUnmute: "Deixar de silenciar emojis" -muteX: "Silenciar {x}" -unmuteX: "Deixar de silenciar {x}" -abort: "Cancel·lar" -tip: "Trucs i consells" -redisplayAllTips: "Torna ha mostrat tots els trucs i consells" -hideAllTips: "Amagar tots els trucs i consells" -_chat: - noMessagesYet: "Encara no tens missatges " - newMessage: "Missatge nou" - individualChat: "Xat individual " - individualChat_description: "Pots mantenir converses individuals amb usuaris concrets." - roomChat: "Sala de xat" - roomChat_description: "Pots xatejar amb diverses persones.\nTambé pots xatejar amb usuaris que no poden fer xats privats, si ells accepten." - createRoom: "Crear una sala" - inviteUserToChat: "Invita usuaris per començar a xatejar" - yourRooms: "Sales creades" - joiningRooms: "Sales a les quals participes" - invitations: "Convida" - noInvitations: "No tens cap invitació " - history: "Historial de converses " - noHistory: "No hi ha un registre previ" - noRooms: "No hi ha cap sala" - inviteUser: "Invitar usuaris" - sentInvitations: "Enviar invitacions" - join: "Afegir-se " - ignore: "Ignorar " - leave: "Marxar" - members: "Membres" - searchMessages: "Buscar missatges " - home: "Inici" - send: "Envia" - newline: "Línia nova " - muteThisRoom: "Silenciar aquesta sala" - deleteRoom: "Esborrar la sala" - chatNotAvailableForThisAccountOrServer: "El xat no està disponible per aquest servidor o aquest compte." - chatIsReadOnlyForThisAccountOrServer: "El xat és només de lectura en aquest servidor o compte. No es poden escriure nous missatges ni crear o unir-se a sales de xat." - chatNotAvailableInOtherAccount: "La funció de xat es troba desactivada al compte de l'altre usuari." - cannotChatWithTheUser: "No pots xatejar amb aquest usuari" - cannotChatWithTheUser_description: "El xat està desactivat o l'altra part encara no l'ha obert." - youAreNotAMemberOfThisRoomButInvited: "No participes en aquesta sala, però has rebut una invitació. Per participar accepta la invitació." - doYouAcceptInvitation: "Acceptes la invitació?" - chatWithThisUser: "Xateja amb aquest usuari" - thisUserAllowsChatOnlyFromFollowers: "Aquest usuari només accepta xats d'usuaris que el segueixen." - thisUserAllowsChatOnlyFromFollowing: "Aquest usuari només accepta xats d'usuaris que segueix." - thisUserAllowsChatOnlyFromMutualFollowing: "Aquest usuari només accepta xats d'usuaris que segueixes i et segueixen." - thisUserNotAllowedChatAnyone: "Aquest usuari no accepta xats de ningú." - chatAllowedUsers: "Usuaris que poden xatejar" - chatAllowedUsers_note: "Pots xatejar amb qualsevol usuari a qui hagis enviat un missatge de xat, independentment d'aquesta configuració." - _chatAllowedUsers: - everyone: "Tothom" - followers: "Només els teus seguidors" - following: "Només usuaris als que segueixes" - mutual: "Només seguidors mutus" - none: "Ningú " -_emojiPalette: - palettes: "Calaixos d'emojis" - enableSyncBetweenDevicesForPalettes: "Activa la sincronització dels calaixos d'emojis entre dispositius" - paletteForMain: "Calaix d'emojis principal" - paletteForReaction: "Calaix d'emojis per reaccions" -_settings: - driveBanner: "Pots gestionar i configurar el Disc, comprovar el seu ús i establir una configuració per a la càrrega d'arxius." - pluginBanner: "Els complements poden fer-se servir per ampliar les funcionalitats del client. Els complements poden instal·lar-se, configurar-se individualment i gestionar-se." - notificationsBanner: "Pots configurar el tipus i l'abast de les notificacions que es rebran del servidor, també les notificacions emergents." - api: "API" - webhook: "Webhook" - serviceConnection: "Relació entre serveis" - serviceConnectionBanner: "Pots configurar i gestionar tokens d'accés i webhooks per integrar serveis i aplicacions externes." - accountData: "Dades del compte" - accountDataBanner: "Exportació/Importació i gestió d'arxius amb dades del compte." - muteAndBlockBanner: "Pots configurar i gestionar els continguts que desitges amagar i restringir les accions de determinats usuaris." - accessibilityBanner: "Els clients poden personalitzar-se i configurar-se per un ús òptim en funció de la seva visió i comportament." - privacyBanner: "Pots establir la configuració de privacitat del compte, com el grau de visibilitat del teu contingut, la facilitat per trobar-ho i si es pot aprovar els seguidors." - securityBanner: "Configura les opcions relacionades amb la seguretat del teu compte com ara contrasenyes, mètodes per iniciar sessió, aplicacions d'autentificació i claus d'accés." - preferencesBanner: "Pots configurar el comportament general del client segons les teves preferències." - appearanceBanner: "Pots configurar les preferències relacionades amb la visualització i l'aspecte del client segons el teu parer." - soundsBanner: "Configuració dels sons que reproduirà el client." - timelineAndNote: "Línia de temps i nota" - makeEveryTextElementsSelectable: "Fes que tots els elements del text siguin seleccionables" - makeEveryTextElementsSelectable_description: "L'activació pot reduir la usabilitat en determinades ocasions." - useStickyIcons: "Utilitza icones fixes" - enableHighQualityImagePlaceholders: "Mostrar marcadors de posició per imatges d'alta qualitat" - uiAnimations: "Animacions de la interfície" - showNavbarSubButtons: "Mostrar sub botons a la barra de navegació " - ifOn: "Quan s'activa" - ifOff: "Quan es desactiva" - enableSyncThemesBetweenDevices: "Sincronitzar els temes instal·lats entre dispositius" - enablePullToRefresh: "Lliscar i actualitzar " - enablePullToRefresh_description: "Amb el ratolí, llisca mentre prems la roda." - realtimeMode_description: "Estableix una connexió amb el servidor i actualitza el contingut en temps real. Pot consumir més dades i bateria." - contentsUpdateFrequency: "Freqüència d'adquisició del contingut" - contentsUpdateFrequency_description: "Com més alt sigui l'adquisició de contingut en temps real, més baixa el rendiment i més consum de dades i bateria." - contentsUpdateFrequency_description2: "Quan s'activa el mode en temps real, el contingut s'actualitza en temps real, independentment d'aquesta configuració." - showUrlPreview: "Mostrar vista prèvia d'URL" - _chat: - showSenderName: "Mostrar el nom del remitent" - sendOnEnter: "Introdueix per enviar" -_preferencesProfile: - profileName: "Nom del perfil" - profileNameDescription: "Estableix un nom que identifiqui aquest dispositiu." - profileNameDescription2: "Per exemple: \"PC Principal\", \"Smartphone\", etc" - manageProfiles: "Gestionar perfils" -_preferencesBackup: - autoBackup: "Còpia de seguretat automàtica " - restoreFromBackup: "Restaurar des d'una còpia de seguretat" - noBackupsFoundTitle: "No s'ha trobat cap còpia de seguretat" - noBackupsFoundDescription: "No s'han trobat còpies de seguretat creades automàticament, però si has desat, manualment, un arxiu de còpia de seguretat, pots importar-lo i carregar-lo." - selectBackupToRestore: "Seleccionar la còpia de seguretat que vols restaurar" - youNeedToNameYourProfileToEnableAutoBackup: "Has de posar-li un nom al teu perfil per poder activar les còpies de seguretat automàtiques." - autoPreferencesBackupIsNotEnabledForThisDevice: "La còpia de seguretat automàtica no es troba activada en aquest dispositiu." - backupFound: "Còpia de seguretat de la configuració trobada" -_accountSettings: - requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut" - requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació." - requireSigninToViewContentsDescription2: "També es desactivaran les vistes prèvies d'URLS (OGP), la incrustació a pàgines web i la visualització des de servidors que no admetin la citació de notes." - requireSigninToViewContentsDescription3: "Aquestes restriccions pot ser que no s'apliquin als continguts federats en servidors remots." - makeNotesFollowersOnlyBefore: "Permetre que les notes antigues només es mostrin als seguidors." - makeNotesFollowersOnlyBeforeDescription: "Mentre aquesta funció estigui activada, les notes que hagin passat la data i hora fixada o hagi passat els temps establert seran visibles només per als teus seguidors. Quan es desactivi, també es restableix l'estat públic de la nota." - makeNotesHiddenBefore: "Fes que les notes antigues siguin privades" - makeNotesHiddenBeforeDescription: "Mentres aquesta funció estigui activada les notes que hagin superat una data i hora fixada o hagi passat el temps establert només seran visibles per a tu. Si la desactives es restablirà també l'estat públic de les notes." - mayNotEffectForFederatedNotes: "Això pot ser que no afecti les notes federades." - mayNotEffectSomeSituations: "Aquestes restriccions són simplificades. Pot ser que no s'apliquin en determinades situacions, com quan es modera o visualitza un servidor remot." - notesHavePassedSpecifiedPeriod: "Notes publicades durant un període de temps especificat." - notesOlderThanSpecifiedDateAndTime: "Notes més antigues de la data i temps especificat " -_abuseUserReport: - forward: "Reenviar " - forwardDescription: "Reenvia l'informe a una altra instància com un compte del sistema anònima." - resolve: "Solució " - accept: "Acceptar " - reject: "Rebutjar" - resolveTutorial: "Si l'informe és legítim selecciona \"Acceptar\" per resoldre'l positivament. Però si l'informe no és legítim selecciona \"Rebutjar\" per resoldre'l negativament." -_delivery: - status: "Estat d'entrega " - stop: "Anul·lar subscripció " - resume: "Torna a enviar" - _type: - none: "S'està publicant" - manuallySuspended: "Suspendre manualment" - goneSuspended: "Servidor suspès perquè el servidor s'ha esborrat" - autoSuspendedForNotResponding: "Servidor suspès perquè el servidor no respon" - softwareSuspended: "Suspès perquè el programari ha deixat de desenvolupar-se " -_bubbleGame: - howToPlay: "Com es juga" - hold: "Mantenir" - _score: - score: "Puntuació " - scoreYen: "Diners guanyats" - highScore: "Millor puntuació " - maxChain: "Nombre màxim de combos" - yen: "{yen}Ien" - estimatedQty: "{qty}peces" - scoreSweets: "{onigiriQtyWithUnit}ongiris" - _howToPlay: - section1: "Ajusta la posició i deixa caure l'objecte dintre la caixa." - section2: "Quan dos objectes del mateix tipus es toquen, canviaran en un objecte diferent i guanyares punts." - section3: "El joc s'acabarà quan els objectes sobresurtin de la caixa. Intenta aconseguir la puntuació més gran possible fusionant objectes mentre impedeixes que sobresurtin de la caixa!" -_announcement: - forExistingUsers: "Anunci per usuaris registrats" - forExistingUsersDescription: "Aquest avís només es mostrarà als usuaris existents fins al moment de la publicació. Si no també es mostrarà als usuaris que es registrin després de la publicació." - needConfirmationToRead: "Es necessita confirmació de lectura de la notificació " - needConfirmationToReadDescription: "Si s'activa es mostrarà un diàleg per confirmar la lectura d'aquesta notificació. A més aquesta notificació serà exclosa de qualsevol funcionalitat com \"Marcar tot com a llegit\"." - end: "Final de la notificació " - tooManyActiveAnnouncementDescription: "Tenir massa notificacions actives pot empitjorar l'experiència de l'usuari. Considera finalitzar els avisos que siguin antics." - readConfirmTitle: "Marcar com llegida?" - readConfirmText: "Això marcarà el contingut de \"{title}\" com llegit." - shouldNotBeUsedToPresentPermanentInfo: "Ja que l'ús de notificacions pot impactar l'experiència dels nous usuaris, és recomanable fer servir les notificacions amb el flux d'informació en comptes de fer-les servir en un únic bloc." - dialogAnnouncementUxWarn: "Tenir dues o més notificacions amb l'estil de finestres pot impactar l'experiència de l'usuari, és per això que és recomana fer-lo servir amb cura." - silence: "Sense notificacions" - silenceDescription: "Activant aquesta opció la notificació no es mostrarà ni l'usuari l'haurà de llegir." -_initialAccountSetting: - accountCreated: "S'ha completat la creació del compte!" - letsStartAccountSetup: "Posem ràpidament la configuració inicial del compte." - letsFillYourProfile: "Comencem establint el teu perfil." - profileSetting: "Configuració del perfil" - privacySetting: "Configuració de seguretat" - theseSettingsCanEditLater: "Aquests ajustos es poden canviar més tard." - youCanEditMoreSettingsInSettingsPageLater: "A més d'això, es poden fer diferents configuracions a través de la pàgina de configuració. Assegureu-vos de comprovar-ho més tard." - followUsers: "Prova de seguir usuaris que t'interessin per construir la teva línia de temps." - pushNotificationDescription: "Activant les notificacions emergents et permetrà rebre notificacions de {name} directament al teu dispositiu." - initialAccountSettingCompleted: "Configuració del perfil completada!" - haveFun: "Disfruta {name}!" - youCanContinueTutorial: "Pots continuar amb un tutorial per aprendre a Fer servir {name} (MissKey) o tu pots estalviar i començar a fer-lo servir ja." - startTutorial: "Començar el tutorial" - skipAreYouSure: "Et vols saltar la configuració del perfil?" - laterAreYouSure: "Vols continuar la configuració del perfil més tard?" -_initialTutorial: - launchTutorial: "Començar tutorial" - title: "Tutorial" - wellDone: "Ben fet!" - skipAreYouSure: "Sortir del tutorial?" - _landing: - title: "Benvingut al tutorial" - description: "Aquí aprendràs el bàsic per poder fer servir Misskey i les seves característiques." - _note: - title: "Què és una Nota?" - description: "Les publicacions a Misskey es diuen 'Notes'. Les Notes s'ordenen cronològicament a la línia de temps i s'actualitzen de forma automàtica." - reply: "Fes clic en aquest botó per contestar a un missatge. També és possible contestar a una contestació, continuant la conversació en forma de fil." - renote: "Pots compartir una Nota a la teva pròpia línia de temps. Inclús pots citar-les amb els teus comentaris." - reaction: "Pots afegir reaccions a les Notes. Entrarem més en detall a la pròxima pàgina." - menu: "Pots veure els detalls de les Notes, copiar enllaços i fer diferents accions." - _reaction: - title: "Què són les Reaccions?" - description: "Es poden reaccionar a les Notes amb diferents emoticones. Les reaccions et permeten expressar matisos que hi són més enllà d'un simple m'agrada." - letsTryReacting: "Es poden afegir reaccions fent clic al botó '+'. Prova reaccionant a aquesta nota!" - reactToContinue: "Afegeix una reacció per continuar." - reactNotification: "Rebràs notificacions en temps real quan un usuari reaccioni a les teves notes." - reactDone: "Pots desfer una reacció fent clic al botó '-'." - _timeline: - title: "El concepte de les línies de temps" - description1: "Misskey mostra diferents línies de temps basades en l'ús (algunes poden no estar disponibles depenent de la política del servidor)" - home: "Pots veure notes dels comptes que segueixes" - local: "Pots veure les notes dels usuaris del servidor." - social: "Es mostren les notes de les línies de temps d'Inici i Local." - global: "Pots veure les notes de tots els servidors connectats." - description2: "Pots canviar la línia de temps en qualsevol moment fent servir la barra de la pantalla superior." - description3: "A més hi ha línies de temps per llistes i per canals. Si vols saber més {link}." - _postNote: - title: "Configuració de la publicació de les notes" - description1: "Quan públiques una nota a Misskey hi ha diferents opcions disponibles. El formulari de publicació es veu així" - _visibility: - description: "Pots limitar qui pot veure les teves notes." - public: "La teva nota serà visible per a tots els usuaris." - home: "Publicar només a línia de temps d'Inici. La gent que visiti el teu perfil o mitjançant les remotes també la podran veure." - followers: "Només visible per a seguidors. Només els teus seguidors la podran veure i ningú més. Ningú més podrà fer renotes." - direct: "Només visible per a alguns seguidors, el destinatari rebre una notificació. Es pot fer servir com una alternativa als missatges directes." - doNotSendConfidencialOnDirect1: "Tingues cura quan enviïs informació sensible." - doNotSendConfidencialOnDirect2: "Els administradors del servidor poden veure tot el que escrius. Ves compte quan enviïs informació sensible en enviar notes directes a altres usuaris en servidors de poca confiança." - localOnly: "Publicar amb aquesta opció activada farà que la nota no federi amb altres servidors. Els usuaris d'altres servidors no podran veure la nota directament, sense importar les opcions de visualització." - _cw: - title: "Avís de Contingut (CW)" - description: "En comptes del cos de la nota es mostrarà el que s'escrigui al camp de 'comentaris'. Fent clic a 'Llegir més' es mostrarà el cos." - _exampleNote: - cw: "Això et farà venir gana!" - note: "Acabo de menjar-me un donut de xocolata 🍩😋" - useCases: "Això es fa servir per seguir normes del servidor sobre certes notes o per ocultar contingut sensible O revelador." - _howToMakeAttachmentsSensitive: - title: "Com marcar adjunts com a contingut sensible?" - description: "Per adjunts que sigui requerit per les normes del servidor o que puguin contenir material sensible, s'ha d'afegir l'opció 'sensible'." - tryThisFile: "Prova de marcar la imatge adjunta en aquest formulari com a sensible!" - _exampleNote: - note: "Oops! L'he fet bona en obrir la tapa de Nocilla..." - method: "Per marcar un adjunt com a sensible, fes clic a la miniatura de l'adjunt, obre el menú i fes clic a 'Marcar com a sensible'." - sensitiveSucceeded: "Quan adjuntis fitxers si us plau marca la sensibilitat seguint les normes del servidor." - doItToContinue: "Marca el fitxer adjunt com a sensible per poder continuar." - _done: - title: "Has completat el tutorial 🎉" - description: "Les funcions explicades aquí és una petita mostra. Per una explicació més detallada de com fer servir MissKey consulta {link}." -_timelineDescription: - home: "A la línia de temps d'Inici pots veure les notes dels usuaris que segueixes." - local: "A la línia de temps Local pots veure les notes de tots els usuaris d'aquest servidor." - social: "La línia de temps Social mostren les notes de les línies de temps d'Inici i Local." - global: "A la línia de temps Global pots veure les notes de tots els servidors connectats." -_serverRules: - description: "Un conjunt de regles que seran mostrades abans de registrar-se. Es recomanable configurar un resum dels termes d'ús." -_serverSettings: - iconUrl: "URL de la icona" - appIconDescription: "Especifica la icona que es mostrarà quan el {host} es mostri en una aplicació." - appIconUsageExample: "Per exemple com a PWA, o quan es mostri com un favorit a la pàgina d'inici del telèfon mòbil" - appIconStyleRecommendation: "Com la icona pot ser retallada com un cercle o un quadrat, es recomana fer servir una icona amb un marge acolorit que l'envolti." - appIconResolutionMustBe: "La resolució mínima és {resolution}." - manifestJsonOverride: "Sobreescriure manifest.json" - shortName: "Nom curt" - shortNameDescription: "Una abreviatura del nom de la instància que es poguí mostrar en cas que el nom oficial sigui massa llarg" - fanoutTimelineDescription: "Quan es troba activat millora bastant el rendiment quan es recuperen les línies de temps i redueix la carrega de la base de dades. Com a contrapunt, l'ús de memòria de Redis es veurà incrementada. Considera d'estabilitat aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes de inestabilitat." - fanoutTimelineDbFallback: "Carregar de la base de dades" - fanoutTimelineDbFallbackDescription: "Quan s'activa, la línia de temps fa servir la base de dades per consultes adicionals si la línia de temps no es troba a la memòria cau. Si és desactiva la càrrega del servidor és veure reduïda, però també és reduirà el nombre de línies de temps que és poden obtenir." - reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat." - inquiryUrl: "URL de consulta " - inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació." - openRegistration: "Registres oberts" - openRegistrationWarning: "Obrir els registres és arriscat. Es recomana obrir-los només si el servidor és monitorat constantment i per respondre immediatament davant qualsevol problema." - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no es detecta activitat per part del moderador durant un període de temps, aquesta opció es desactiva automàticament per evitar el correu brossa." - deliverSuspendedSoftware: "Programari que ja no es distribueix" - deliverSuspendedSoftwareDescription: "Pots especificar un rang de noms i versions del programari del servidor per detenir l'entrega, per exemple, degut a vulnerabilitats. Aquesta informació la proporciona el servidor i la seva fiabilitat no es garantitzada. Es pot fer servir una especificació de rang sencer per especificar una versió, però es recomana especificar una versió anterior, com >= 2024.3.1-0, perquè especificar >= 2024.3.1 no incloure versions personalitzades com 2024.3.1-custom.0." - singleUserMode: "Mode un usuari" - singleUserMode_description: "Si ets l'únic usuari d'aquesta instància, activant aquest mode optimitzaràs el funcionament." - signToActivityPubGet: "Formar sol·licituds GET" - signToActivityPubGet_description: " Això normalment hauria d'estar activat. Desactivar aquesta opció pot millorar els problemes de comunicació amb algunes de les instàncies federades, però també pot fer impossibles les comunicacions amb altres servidors." - proxyRemoteFiles: "Proxy d'arxius remots" - proxyRemoteFiles_description: "Quan està habilitat, fa de proxy i serveix arxius remots. Això ajuda a generar les miniatures de les imatges i a protegir la privacitat dels usuaris." - allowExternalApRedirect: "Permetre el reencaminament per consultes fent servir ActivityPub." - allowExternalApRedirect_description: "Si aquesta opció s'activa, altres servidors poden consultar continguts de tercers mitjançant aquest servidor, però això pot donar peu a la suplantació de continguts." - userGeneratedContentsVisibilityForVisitor: "L'abast de la publicació del contingut generat per l'usuari" - userGeneratedContentsVisibilityForVisitor_description: "Això ajuda a evitar problemes com que continguts remots inadequats que no hagin estat moderats correctament es publiquin a internet mitjançant el teu servidor." - userGeneratedContentsVisibilityForVisitor_description2: "La publicació incondicional de tots els continguts del servidor a internet, incloent-hi els continguts remots rebuts pel servidor, comporta riscos. Això és extremadament important per els espectadors que desconeixen el caràcter descentralitzat dels continguts, ja que poden percebre erroneament els continguts remots com contingut generat per el propi servidor." - _userGeneratedContentsVisibilityForVisitor: - all: "Tot obert al públic " - localOnly: "Només es publiquen els continguts locals, el contingut remot es manté privat" - none: "Tot privat" -_accountMigration: - moveFrom: "Migrar un altre compte a aquest" - moveFromSub: "Crear un àlies per un altre compte" - moveFromLabel: "Compte original #{n}" - moveFromDescription: "Has de crear un àlies del compte que vols migrar en aquest compte.\nFes servir aquest format per posar el compte que vols migrar: @nomusuari@servidor.exemple.com\nPer esborrar l'àlies deixa el camp en blanc (no és recomanable de fer)" - moveTo: "Migrar aquest compte a un altre" - moveToLabel: "Compte al qual es vol migrar:" - moveCannotBeUndone: "Les migracions dels comptes no es poden desfer." - moveAccountDescription: "Això migrarà el teu compte a un altre diferent.\n ・Els seguidors d'aquest compte és passaran al compte nou de forma automàtica\n ・Es deixaran de seguir a tots els usuaris que es segueixen actualment en aquest compte\n ・No es poden crear notes noves, etc. en aquest compte\n\nSi bé la migració de seguidors es automàtica, has de preparar alguns pasos manualment per migrar la llista d'usuaris que segueixes. Per fer això has d'exportar els seguidors que després importaraes al compte nou mitjançant el menú de configuració. El mateix procediment s'ha de seguir per less teves llistes i els teus usuaris silenciats i bloquejats.\n\n(Aquesta explicació s'aplica a Misskey v13.12.0 i posteriors. Altres aplicacions, com Mastodon, poden funcionar diferent.)" - moveAccountHowTo: "Per fer la migració, primer has de crear un àlies per aquest compte al compte al qual vols migrar.\nDesprés de crear l'àlies, introdueix el compte al qual vols migrar amb el format següent: @nomusuari@servidor.exemple.com" - startMigration: "Migrar" - migrationConfirm: "Vols migrar aquest compte a {account}? Una vegada comenci la migració no es podrà parar O fer marxa enrere i no podràs tornar a fer servir aquest compte mai més." - movedAndCannotBeUndone: "Aquest compte ha migrat.\nLes migracions no es poden desfer." - postMigrationNote: "Aquest compte deixarà de seguir tots els comptes que segueix 24 hores després de terminar la migració.\nEl nombre de seguidors i seguits passarà a ser de zero. Per evitar que els teus seguidors no puguin veure les publicacions marcades com a només seguidors continuaren seguint aquest compte." - movedTo: "Nou compte:" -_achievements: - earnedAt: "Desbloquejat el" - _types: - _notes1: - title: "Aquí, configurant el meu msky" - description: "Publica la teva primera Nota" - flavor: "Passa-t'ho bé fent servir Miskey!" - _notes10: - title: "Algunes notes" - description: "Publica 10 notes" - _notes100: - title: "Un piló de notes" - description: "Publica 100 notes" - _notes500: - title: "Cobert de notes" - description: "Publica 500 notes" - _notes1000: - title: "Un piló de notes" - description: "1 000 notes publicades" - _notes5000: - title: "Desbordament de notes" - description: "5 000 notes publicades" - _notes10000: - title: "Supernota" - description: "10 000 notes publicades" - _notes20000: - title: "Necessito... Més... Notes!" - description: "20 000 notes publicades" - _notes30000: - title: "Notes notes notes!" - description: "30 000 notes publicades" - _notes40000: - title: "Fàbrica de notes" - description: "40 000 notes publicades" - _notes50000: - title: "Planeta de notes" - description: "50 000 notes publicades" - _notes60000: - title: "Quàsar de notes" - description: "60 000 notes publicades" - _notes70000: - title: "Forat negre de notes" - description: "70 000 notes publicades" - _notes80000: - title: "Galàxia de notes" - description: "80 000 notes publicades" - _notes90000: - title: "Univers de notes" - description: "90 000 notes publicades" - _notes100000: - title: "ALL YOUR NOTE ARE BELONG TO US" - description: "100 000 notes publicades" - flavor: "Segur que tens moltes coses a dir?" - _login3: - title: "Principiant I" - description: "Vas iniciar sessió fa tres dies" - flavor: "Des d'avui diguem Misskist" - _login7: - title: "Principiant II" - description: "Vas iniciar sessió fa set dies" - flavor: "Ja saps com va funcionant tot?" - _login15: - title: "Principiant III" - description: "Vas iniciar sessió fa quinze dies" - _login30: - title: "Misskist I" - description: "Vas iniciar sessió fa trenta dies" - _login60: - title: "Misskist II" - description: "Vas iniciar sessió fa seixanta dies" - _login100: - title: "Misskist III" - description: "Vas iniciar sessió fa cent dies" - flavor: "Misskist violent" - _login200: - title: "Regular I" - description: "Vas iniciar sessió fa dos-cents dies" - _login300: - title: "Regular II" - description: "Vas iniciar sessió fa tres-cents dies" - _login400: - title: "Regular III" - description: "Vas iniciar sessió fa quatre-cents dies" - _login500: - title: "Expert I" - description: "Vas iniciar sessió fa cinc-cents dies" - flavor: "Amics, he dit massa vegades que soc un amant de les notes" - _login600: - title: "Expert II" - description: "Vas iniciar sessió fa sis-cents dies" - _login700: - title: "Expert III" - description: "Vas iniciar sessió fa set-cents dies" - _login800: - title: "Mestre de les Notes I" - description: "Vas iniciar sessió fa vuit-cents dies " - _login900: - title: "Mestre de les Notes II" - description: "Vas iniciar sessió fa nou-cents dies" - _login1000: - title: "Mestre de les Notes III" - description: "Vas iniciar sessió fa mil dies" - flavor: "Gràcies per fer servir MissKey!" - _noteClipped1: - title: "He de retallar-te!" - description: "Retalla la teva primera nota" - _noteFavorited1: - title: "Quan miro les estrelles" - description: "La primera vegada que vaig registrar el meu favorit" - _myNoteFavorited1: - title: "Vull una estrella" - description: "La meva nota va ser registrada com favorita per una de les altres persones" - _profileFilled: - title: "Estic a punt" - description: "Vaig fer la configuració de perfil" - _markedAsCat: - title: "Soc un gat" - description: "He establert el meu compte com si fos un Gat" - flavor: "Encara no tinc nom" - _following1: - title: "És el meu primer seguiment" - description: "És la primera vegada que et segueixo" - _following10: - title: "Segueix-me... Segueix-me..." - description: "Seguir 10 usuaris" - _following50: - title: "Molts amics" - description: "Seguir 50 comptes" - _following100: - title: "100 amics" - description: "Segueixes 100 comptes" - _following300: - title: "Sobrecàrrega d'amics" - description: "Segueixes 300 comptes" - _followers1: - title: "Primer seguidor" - description: "1 seguidor guanyat" - _followers10: - title: "Segueix-me!" - description: "10 seguidors guanyats" - _followers50: - title: "Venen en manada" - description: "50 seguidors guanyats" - _followers100: - title: "Popular" - description: "100 seguidors guanyats" - _followers300: - title: "Si us plau, d'un en un!" - description: "300 seguidors guanyats" - _followers500: - title: "Torre de ràdio" - description: "500 seguidors guanyats" - _followers1000: - title: "Influenciador" - description: "1 000 seguidors guanyats" - _collectAchievements30: - title: "Col·leccionista d'èxits " - description: "Desbloqueja 30 assoliments" - _viewAchievements3min: - title: "M'agraden els èxits " - description: "Mira la teva llista d'assoliments durant més de 3 minuts" - _iLoveMisskey: - title: "Estimo Misskey" - description: "Publica \"I ❤ #Misskey\"" - flavor: "L'equip de desenvolupament de Misskey agraeix el vostre suport!" - _foundTreasure: - title: "A la Recerca del Tresor" - description: "Has trobat el tresor amagat" - _client30min: - title: "Parem una estona" - description: "Mantingues obert Misskey per 30 minuts" - _client60min: - title: "A totes amb Misskey" - description: "Mantingues Misskey obert per 60 minuts" - _noteDeletedWithin1min: - title: "No et preocupis" - description: "Esborra una nota al minut de publicar-la" - _postedAtLateNight: - title: "Nocturn" - description: "Publica una nota a altes hores de la nit " - flavor: "És hora d'anar a dormir." - _postedAt0min0sec: - title: "Rellotge xerraire" - description: "Publica una nota a les 0:00" - flavor: "Tic tac, tic tac, tic tac, DING!" - _selfQuote: - title: "Autoreferència " - description: "Cita una nota teva" - _htl20npm: - title: "Línia de temps fluida" - description: "La teva línia de temps va a més de 20npm (notes per minut)" - _viewInstanceChart: - title: "Analista " - description: "Mira els gràfics de la teva instància " - _outputHelloWorldOnScratchpad: - title: "Hola, món!" - description: "Escriu \"hola, món\" al bloc de notes" - _open3windows: - title: "Multi finestres" - description: "I va obrir més de tres finestres" - _driveFolderCircularReference: - title: "Consulteu la secció de bucle" - description: "Intenta crear carpetes recursives al Disc" - _reactWithoutRead: - title: "De veritat has llegit això?" - description: "Reaccions a una nota de més de 100 caràcters publicada fa menys de 3 segons " - _clickedClickHere: - title: "Fer clic" - description: "Has fet clic aquí " - _justPlainLucky: - title: "Ha sigut sort" - description: "Oportunitat de guanyar-lo amb una probabilitat d'un 0.005% cada 10 segons" - _setNameToSyuilo: - title: "soc millor" - description: "Posat \"siuylo\" com a nom" - _passedSinceAccountCreated1: - title: "Primer aniversari" - description: "Ja ha passat un any d'ençà que vas crear el teu compte" - _passedSinceAccountCreated2: - title: "Segon aniversari" - description: "Ja han passat dos anys d'ençà que vas crear el teu compte" - _passedSinceAccountCreated3: - title: "Tres anys" - description: "Ja han passat tres anys d'ençà que vas crear el teu compte" - _loggedInOnBirthday: - title: "Felicitats!" - description: "T'has identificat el dia del teu aniversari" - _loggedInOnNewYearsDay: - title: "Bon any nou!" - description: "T'has identificat el primer dia de l'any " - flavor: "A per un altre any memorable a la teva instància " - _cookieClicked: - title: "Un joc en què fas clic a les galetes" - description: "Pica galetes" - flavor: "Espera, ets al lloc web correcte?" - _brainDiver: - title: "Busseja Ments" - description: "Publica un enllaç al Busseja Ments" - flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "Sobrecàrrega de proves" - description: "Envia moltes notificacions de prova en un període de temps molt curt" - _tutorialCompleted: - title: "Diploma del Curs Elemental de Misskey" - description: "Has completat el tutorial" - _bubbleGameExplodingHead: - title: "🤯" - description: "L'objecte més gran del joc de la bombolla " - _bubbleGameDoubleExplodingHead: - title: "Doble 🤯" - description: "Dos dels objectes més grans del joc de la bombolla al mateix temps" - flavor: "Pots emplenar una carmanyola com aquesta 🤯🤯 una mica" -_role: - new: "Nou rol" - edit: "Editar el rol" - name: "Nom del rol" - description: "Descripció del rol" - permission: "Permisos de rol" - descriptionOfPermission: "Els Moderadors poden fer operacions bàsiques de moderació.\nEls Administradors poden canviar tots els ajustos del servidor." - assignTarget: "Assignar " - descriptionOfAssignTarget: "Manual per canviar manualment qui és part d'aquest rol i qui no.\nCondicional per afegir o eliminar de manera automàtica els usuaris d'aquest rol basat en una determinada condició." - manual: "Manual" - manualRoles: "Rols manuals" - conditional: "Condicional" - conditionalRoles: "Rols condicionals" - condition: "Condició" - isConditionalRole: "Aquest és un rol condicional" - isPublic: "Rol públic" - descriptionOfIsPublic: "Aquest rol es mostrarà al perfil dels usuaris al que se'ls assigni." - options: "Opcions" - policies: "Polítiques" - baseRole: "Plantilla de rols" - useBaseValue: "Fer servir els valors de la plantilla de rols" - chooseRoleToAssign: "Selecciona els rols a assignar" - iconUrl: "URL de la icona " - asBadge: "Mostrar com a insígnia " - descriptionOfAsBadge: "La icona d'aquest rol es mostrarà al costat dels noms d'usuaris que tinguin assignats aquest rol." - isExplorable: "Fer el rol explorable" - descriptionOfIsExplorable: "La línia de temps d'aquest rol i la llista d'usuaris seran públics si s'activa." - displayOrder: "Posició " - descriptionOfDisplayOrder: "Com més gran és el número, més dalt la seva posició a la interfície." - preserveAssignmentOnMoveAccount: "L'estat de l'assignació també es trasllada amb el compte migrat" - preserveAssignmentOnMoveAccount_description: "Si s'activa quan es migra un compte amb aquest rol, el compte migrat també heretarà aquest rol." - canEditMembersByModerator: "Permetre que els moderadors editin la llista d'usuaris en aquest rol" - descriptionOfCanEditMembersByModerator: "Quan s'activa, els moderadors, així com els administradors, podran afegir i treure usuaris d'aquest rol. Si es troba desactivat, només els administradors poden assignar usuaris." - priority: "Prioritat" - _priority: - low: "Baixa" - middle: "Mitjà" - high: "Alta" - _options: - gtlAvailable: "Pot veure la línia de temps global" - ltlAvailable: "Pot veure la línia de temps local" - canPublicNote: "Pot enviar notes públiques" - mentionMax: "Nombre màxim de mencions a una nota" - canInvite: "Pot crear invitacions a la instància " - inviteLimit: "Límit d'invitacions " - inviteLimitCycle: "Temps de refresc de les invitacions" - inviteExpirationTime: "Interval de caducitat de les invitacions" - canManageCustomEmojis: "Gestiona els emojis personalitzats" - canManageAvatarDecorations: "Gestiona les decoracions dels avatars " - driveCapacity: "Capacitat del disc" - maxFileSize: "Mida màxima de l'arxiu que es pot carregar" - alwaysMarkNsfw: "Marca sempre els fitxers com a sensibles" - canUpdateBioMedia: "Permet l'edició d'una icona o un bàner" - pinMax: "Nombre màxim de notes fixades" - antennaMax: "Nombre màxim d'antenes" - wordMuteMax: "Nombre màxim de caràcters permesos a les paraules silenciades" - webhookMax: "Nombre màxim de Webhooks" - clipMax: "Nombre màxim de clips" - noteEachClipsMax: "Nombre màxim de notes dintre d'un clip" - userListMax: "Nombre màxim de llistes d'usuaris " - userEachUserListsMax: "Nombre màxim d'usuaris dintre d'una llista d'usuaris " - rateLimitFactor: "Limitador" - descriptionOfRateLimitFactor: "Límits baixos són menys restrictius, límits alts són més restrictius." - canHideAds: "Pot amagar la publicitat" - canSearchNotes: "Pot cercar notes" - canUseTranslator: "Pot fer servir el traductor" - avatarDecorationLimit: "Nombre màxim de decoracions que es poden aplicar els avatars" - canImportAntennas: "Autoritza la importació d'antenes " - canImportBlocking: "Autoritza la importació de bloquejats" - canImportFollowing: "Autoritza la importació de seguidors" - canImportMuting: "Autoritza la importació de silenciats" - canImportUserLists: "Autoritza la importació de llistes d'usuaris " - chatAvailability: "Es permet xatejar" - uploadableFileTypes: "Tipus de fitxers que en podeu pujar" - uploadableFileTypes_caption: "Especifica el tipus MIME. Es poden especificar diferents tipus MIME separats amb una nova línia, i es poden especificar comodins amb asteriscs (*). (Per exemple: image/*)" - uploadableFileTypes_caption2: "Pot que no sigui possible determinar el tipus MIME d'alguns arxius. Per permetre aquests tipus d'arxius afegeix {x} a les especificacions." - _condition: - roleAssignedTo: "Assignat a rols manuals" - isLocal: "Usuari local" - isRemote: "Usuari remot" - isCat: "Usuaris gats" - isBot: "Usuaris bots" - isSuspended: "Usuari suspès" - isLocked: "Comptes privats" - isExplorable: "Fes que el compte aparegui a les cerques" - createdLessThan: "Han passat menys de X a passat des de la creació del compte" - createdMoreThan: "Han passat més de X des de la creació del compte" - followersLessThanOrEq: "Té menys de X seguidors" - followersMoreThanOrEq: "Té X o més seguidors" - followingLessThanOrEq: "Segueix X o menys comptes" - followingMoreThanOrEq: "Segueix a X o més comptes" - notesLessThanOrEq: "Les publicacions són menys o igual a " - notesMoreThanOrEq: "Les publicacions són més o igual a " - and: "AND condicional " - or: "OR condicional" - not: "NOT condicional" -_sensitiveMediaDetection: - description: "Redueix els esforços de moderació gràcies al reconeixement automàtic dels fitxers amb contingut sensible mitjançant Machine Learing. Això augmentarà la càrrega del servidor." - sensitivity: "Sensibilitat de la detecció " - sensitivityDescription: "Reduint la sensibilitat provocarà menys falsos positius. D'altra banda incrementant-ho generarà més falsos negatius." - setSensitiveFlagAutomatically: "Marcar com a sensible" - setSensitiveFlagAutomaticallyDescription: "Els resultats de la detecció interna seran desats, inclòs si aquesta opció es troba desactivada." - analyzeVideos: "Activar anàlisis de vídeos " - analyzeVideosDescription: "Analitzar els vídeos a més de les imatges. Això incrementarà lleugerament la càrrega del servidor." -_emailUnavailable: - used: "Aquest correu electrònic ja s'està fent servir" - format: "El format del correu electrònic és invàlid " - disposable: "No es poden fer servir adreces de correu electrònic d'un sol ús " - mx: "Aquest servidor de correu electrònic no és vàlid " - smtp: "Aquest servidor de correu electrònic no respon" - banned: "No pots registrar-te amb aquesta adreça de correu electrònic " -_ffVisibility: - public: "Públic " - followers: "Visible només per a seguidors " - private: "Privat" -_signup: - almostThere: "Ja quasi estem" - emailAddressInfo: "Si us plau, escriu la teva adreça de correu electrònic. No es farà pública." - emailSent: "S'ha enviat un correu de confirmació a ({email}). Si us plau, fes clic a l'enllaç per completar el registre." -_accountDelete: - accountDelete: "Eliminar el compte" - mayTakeTime: "Com l'eliminació d'un compte consumeix bastants recursos, pot trigar un temps perquè es completi l'esborrat, depenent si tens molt contingut i la quantitat de fitxer que hagis pujat." - sendEmail: "Una vegada hagi finalitzat l'esborrat del compte rebràs un correu electrònic a l'adreça que tinguis registrada en aquest compte." - requestAccountDelete: "Demanar l'eliminació del compte" - started: "Ha començat l'esborrat del compte." - inProgress: "L'esborrat es troba en procés " -_ad: - back: "Tornar" - reduceFrequencyOfThisAd: "Mostrar menys aquest anunci" - hide: "No mostrar mai" - timezoneinfo: "El dia de la setmana ve determinat del fus horari del servidor." - adsSettings: "Configurar la publicitat" - notesPerOneAd: "Interval d'emplaçament publicitari en temps real (Notes per anuncis)" - setZeroToDisable: "Ajusta aquest valor a 0 per deshabilitar l'actualització d'anuncis en temps real" - adsTooClose: "L'interval actual pot fer que l'experiència de l'usuari sigui dolenta perquè l'interval és molt baix." -_forgotPassword: - enterEmail: "Escriu l'adreça de correu electrònic amb la que et vas registrar. S'enviarà un correu electrònic amb un enllaç perquè puguis canviar-la." - ifNoEmail: "Si no vas fer servir una adreça de correu electrònic per registrar-te, si us plau posa't en contacte amb l'administrador." - contactAdmin: "Aquesta instància no suporta registrar-se amb correu electrònic. Si us plau, contacta amb l'administrador del servidor." -_gallery: - my: "La meva Galeria " - liked: "Publicacions que t'han agradat" - like: "M'agrada " - unlike: "Ja no m'agrada" _email: _follow: - title: "Tens un nou seguidor" - _receiveFollowRequest: - title: "Has rebut una sol·licitud de seguiment" -_plugin: - install: "Instal·lar un afegit " - installWarn: "Si us plau, no instal·lis afegits que no siguin de confiança." - manage: "Gestionar els afegits" - viewSource: "Veure l'origen " - viewLog: "Mostra el registre" -_preferencesBackups: - list: "Llista de còpies de seguretat" - saveNew: "Fer una còpia de seguretat nova" - loadFile: "Carregar des d'un fitxer" - apply: "Aplicar en aquest dispositiu" - save: "Desar els canvis" - inputName: "Escriu un nom per aquesta còpia de seguretat" - cannotSave: "No s'ha pogut desar" - nameAlreadyExists: "Ja existeix una còpia de seguretat anomenada \"{name}\". Escriu un nom diferent." - applyConfirm: "Vols aplicar la còpia de seguretat \"{name}\" a aquest dispositiu? La configuració actual del dispositiu serà esborrada." - saveConfirm: "Desar còpia de seguretat com {name}?" - deleteConfirm: "Esborrar la còpia de seguretat {name}?" - renameConfirm: "Vols canvia el nom de la còpia de seguretat de \"{old}\" a \"{new}\"?" - noBackups: "No hi ha còpies de seguretat. Pots fer una còpia de seguretat de la configuració d'aquest dispositiu al servidor fent servir \"Crear nova còpia de seguretat\"" - createdAt: "Creat el: {date} {time}" - updatedAt: "Actualitzat el: {date} {time}" - cannotLoad: "Hi ha hagut un error al carregar" - invalidFile: "Format del fitxer no vàlid " -_registry: - scope: "Àmbit " - key: "Clau" - keys: "Claus" - domain: "Domini" - createKey: "Crear una clau" -_aboutMisskey: - about: "Misskey és un programa de codi obert desenvolupat des del 2014 per syuilo" - contributors: "Col·laboradors principals" - allContributors: "Tots els col·laboradors " - source: "Codi font" - original: "Original" - thisIsModifiedVersion: "En {name} fa servir una versió modificada de Misskey." - translation: "Tradueix Misskey" - donate: "Fes un donatiu a Misskey" - morePatrons: "També agraïm el suport d'altres col·laboradors que no surten en aquesta llista. Gràcies! 🥰" - patrons: "Patrocinadors" - projectMembers: "Membres del projecte" -_displayOfSensitiveMedia: - respect: "Ocultar imatges o vídeos marcats com a sensibles" - ignore: "Mostrar imatges o vídeos marcats com a sensibles" - force: "Ocultar totes les imatges o vídeos " -_instanceTicker: - none: "No mostrar mai" - remote: "Mostrar per usuaris remots" - always: "Mostrar sempre" -_serverDisconnectedBehavior: - reload: "Recarregar automàticament " - dialog: "Mostrar finestres de confirmació " - quiet: "Mostrar un avís que no molesti" -_channel: - create: "Crear un canal" - edit: "Editar canal" - setBanner: "Estableix el bàner " - removeBanner: "Eliminar el.bàner" - featured: "Popular" - owned: "Propietat" - following: "Seguin" - usersCount: "{n} Participants" - notesCount: "{n} Notes" - nameAndDescription: "Nom i descripció " - nameOnly: "Nom només " - allowRenoteToExternal: "Permet la citació i l'impuls fora del canal" -_menuDisplay: - sideFull: "Horitzontal " - sideIcon: "Horitzontal (icones)" - top: "A dalt" - hide: "Amagar" -_wordMute: - muteWords: "Paraules silenciades" - muteWordsDescription: "Separar amb espais per la condició AND o amb salts de línia per la condició OR." - muteWordsDescription2: "Envolta les paraules amb barres per fer servir expressions regulars." + title: "t'ha seguit" _instanceMute: instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat." - instanceMuteDescription2: "Separar amb salts de línia" - title: "Ocultar notes de les instàncies en la llista." - heading: "Llista d'instàncies a silenciar" _theme: - explore: "Explorar els temes " - install: "Instal·lar un tema" - manage: "Gestionar els temes " - code: "Codi del tema" - description: "Descripció" - installed: "{name} Instal·lat " - installedThemes: "Temes instal·lats " - builtinThemes: "Temes integrats" - instanceTheme: "Tema de la instància " - alreadyInstalled: "Aquest tema ja es troba instal·lat " - invalid: "El format d'aquest tema no és correcte" - make: "Crear un tema" - base: "Base" - addConstant: "Afegir constant " - constant: "Constant" - defaultValue: "Valor per defecte" - color: "Color" - refProp: "Referència a una propietat" - refConst: "Referència a una constant " - key: "Clau" - func: "Funcions" - funcKind: "Tipus de funció " - argument: "Argument" - basedProp: "Propietat referenciada" - alpha: "Opacitat" - darken: "Enfosquir " - lighten: "Brillantor" - inputConstantName: "Escriu un nom per aquesta constant" - importInfo: "Si escrius el codi del tema aquí, el podràs importar a l'editor del tema" - deleteConstantConfirm: "Vols esborrar la constant {const}?" keys: - accent: "Accent" - bg: "Fons" - fg: "Text" - focus: "Enfocament" - indicator: "Indicador" - panel: "Tauler" - shadow: "Ombra" - header: "Capçalera" - navBg: "Fons de la barra lateral" - navFg: "Text de la barra lateral" - navActive: "Text barra lateral (actiu)" - navIndicator: "Indicador barra lateral" - link: "Enllaç" - hashtag: "Etiqueta" mention: "Menció" - mentionMe: "Mencions (jo)" - renote: "Impulsar" - modalBg: "Fons del modal" - divider: "Divisor" - scrollbarHandle: "Maneta de la barra de desplaçament" - scrollbarHandleHover: "Maneta de la barra de desplaçament (en passar-hi per sobre)" - dateLabelFg: "Text de l'etiqueta de la data" - infoBg: "Fons d'informació " - infoFg: "Text d'informació " - infoWarnBg: "Fons avís " - infoWarnFg: "Text avís " - toastBg: "Fons notificació " - toastFg: "Text notificació " - buttonBg: "Fons botó " - buttonHoverBg: "Fons botó (en passar-hi per sobre)" - inputBorder: "Contorn del cap d'introducció " - badge: "Insígnia " - messageBg: "Fons del xat" - fgHighlighted: "Text ressaltat" + renote: "Renotar" _sfx: note: "Notes" - noteMy: "Nota (per mi)" notification: "Notificacions" - reaction: "Quan se selecciona una reacció " - chatMessage: "Missatges del xat" -_soundSettings: - driveFile: "Fer servir un fitxer d'àudio del disc" - driveFileWarn: "Seleccionar un fitxer d'àudio del disc" - driveFileTypeWarn: "Fitxer no suportat " - driveFileTypeWarnDescription: "Seleccionar un fitxer d'àudio " - driveFileDurationWarn: "L'àudio és massa llarg" - driveFileDurationWarnDescription: "Els àudios molt llargs pot interrompre l'ús de Misskey. Vols continuar?" - driveFileError: "El so no es pot carregar. Canvia la configuració" -_ago: - future: "Futur " - justNow: "Ara mateix" - secondsAgo: "Fa {n} segons" - minutesAgo: "Fa {n} minuts" - hoursAgo: "Fa {n} hores" - daysAgo: "Fa {n} dies" - weeksAgo: "Fa {n} setmanes" - monthsAgo: "Fa {n} mesos" - yearsAgo: "Fa {n} anys" - invalid: "Res" -_timeIn: - seconds: "En {n} segons" - minutes: "En {n} minuts" - hours: "En {n} hores" - days: "En {n} dies" - weeks: "En {n} setmanes" - months: "En {n} mesos" - years: "En {n} anys" -_time: - second: "Segon(s)" - minute: "Minut(s)" - hour: "Hor(a)(es)" - day: "Di(a)(es)" + chat: "Xat" + antenna: "Antenes" _2fa: - alreadyRegistered: "J has registrat un dispositiu d'autenticació de doble factor." - registerTOTP: "Registrar una aplicació autenticadora" - step1: "Primer instal·la una aplicació autenticadora (com {a} o {b}) al teu dispositiu." - step2: "Després escaneja el codi QR que es mostra en aquesta pantalla." - step2Uri: "Escriu la següent URI si estàs fent servir una aplicació d'escriptori " - step3Title: "Escriu un codi d'autenticació" - step3: "Escriu el codi d'autenticació (token) que es mostra a la teva aplicació per finalitzar la configuració." - setupCompleted: "Configuració terminada" - step4: "D'ara endavant quan accedeixis se't demanarà el token que has introduït." - securityKeyNotSupported: "El teu navegador no suporta claus de seguretat" - registerTOTPBeforeKey: "Configura una aplicació d'autenticació per registrar una clau de seguretat o una clau de pas." - securityKeyInfo: "A més de l'empremta digital o PIN per autenticar-te, pots configurar autenticació mitjançant maquinari que suporti claus de seguretat FIDO2, per protegir encara més el teu compte." - registerSecurityKey: "Registrar una clau de seguretat o clau de pas" - securityKeyName: "Escriu un nom per la clau" - tapSecurityKey: "Seguiu les instruccions del navegador i registrar les claus de seguretat o la clau de pas" - removeKey: "Esborrar la clau de seguretat" - removeKeyConfirm: "Esborrar la còpia de seguretat {name}?" - whyTOTPOnlyRenew: "L'aplicació d'autenticació no es pot eliminar mentre hi hagi una clau de seguretat registrada." - renewTOTP: "Reconfigurar l'aplicació d'autenticació " - renewTOTPConfirm: "Això farà que els codis de validació de l'antiga aplicació deixin de funcionar" - renewTOTPOk: "Reconfigurar" - renewTOTPCancel: "No, gràcies" - checkBackupCodesBeforeCloseThisWizard: "Abans de tancar aquesta finestra, comprova el següent codi de seguretat." - backupCodes: "Codi de seguretat." - backupCodesDescription: "Si l'aplicació d'autenticació no es pot utilitzar, es pot accedir al compte utilitzant els següents codis de còpia de seguretat. Assegura't de mantenir aquests codis en un lloc segur. Cada codi es pot utilitzar només una vegada." - backupCodeUsedWarning: "Es va utilitzar un codi de còpia de seguretat. Si l'aplicació de certificació està disponible, reconfigura l'aplicació d'autenticació tan aviat com sigui possible." - backupCodesExhaustedWarning: "Es van utilitzar tots els codis de còpia de seguretat. Si no es pot utilitzar l'aplicació d'autenticació, ja no es pot accedir al compte. Torna a registrar l'aplicació d'autenticació." - moreDetailedGuideHere: "Aquí tens una guia al detall" -_permissions: - "read:account": "Veure la informació del compte." - "write:account": "Editar la informació del compte." - "read:blocks": "Veure la llista d'usuaris bloquejats" - "write:blocks": "Editar la llista d'usuaris blocats" - "read:drive": "Accedeix als teus fitxers i carpetes del Disc" - "write:drive": "Editar o eliminar els teus fitxers i carpetes al Disc" - "read:favorites": "Veure la teva llista de favorits" - "write:favorites": "Editar la teva llista de favorits" - "read:following": "Veure informació de qui segueixes" - "write:following": "Segueix o deixa de seguir altres comptes" - "read:messaging": "Veure els teus xats" - "write:messaging": "Crear o esborrar missatges de xat" - "read:mutes": "Veure la teva llista d'usuaris silenciats" - "write:mutes": "Editar la teva llista d'usuaris silenciats" - "write:notes": "Crear o esborrar notes" - "read:notifications": "Veure les teves notificacions" - "write:notifications": "Gestionar les teves notificacions" - "read:reactions": "Veure les teves reaccions" - "write:reactions": "Editar les teves reaccions" - "write:votes": "Votar en una enquesta" - "read:pages": "Veure les teves pàgines " - "write:pages": "Editar o esborrar les teves pàgines " - "read:page-likes": "Veure la llista de les pàgines que t'han agradat" - "write:page-likes": "Editar la llista de les pàgines que t'han agradat" - "read:user-groups": "Veure els teus grups d'usuaris " - "write:user-groups": "Editar o esborrar els teus grups d'usuaris " - "read:channels": "Veure els teus canals" - "write:channels": "Editar els teus canals" - "read:gallery": "Veure la teva galeria " - "write:gallery": "Editar la teva galeria" - "read:gallery-likes": "Veure la llista de publicacions de galeries que t'han agradat" - "write:gallery-likes": "Editar la llista de publicacions de galeries que t'han agradat" - "read:flash": "Veure reproduccions" - "write:flash": "Editar reproduccions" - "read:flash-likes": "Veure la llista de reproduccions que t'han agradat" - "write:flash-likes": "Editar la llista de reproduccions que t'han agradat" - "read:admin:abuse-user-reports": "Veure informes d'usuaris " - "write:admin:delete-account": "Esborrar compte d'usuari " - "write:admin:delete-all-files-of-a-user": "Esborrar tots els fitxers d'un usuari" - "read:admin:index-stats": "Veure l'índex de la base de dades" - "read:admin:table-stats": "Veure la informació de les taules a la base de dades" - "read:admin:user-ips": "Veure adreça IP de l'usuari " - "read:admin:meta": "Veure meta-informació del servidor" - "write:admin:reset-password": "Reiniciar contrasenya d'usuari " - "write:admin:resolve-abuse-user-report": "Resoldre informes d'usuaris " - "write:admin:send-email": "Enviar correu electrònic " - "read:admin:server-info": "Veure informació del servidor" - "read:admin:show-moderation-log": "Veure registre de moderació " - "read:admin:show-user": "Veure informació privada de l'usuari " - "write:admin:suspend-user": "Suspendre usuari" - "write:admin:unset-user-avatar": "Esborrar avatar d'usuari " - "write:admin:unset-user-banner": "Esborrar bàner de l'usuari " - "write:admin:unsuspend-user": "Treure la suspensió d'un usuari" - "write:admin:meta": "Gestionar les metadades de la instància" - "write:admin:user-note": "Gestionar les notes de moderació " - "write:admin:roles": "Gestionar rols" - "read:admin:roles": "Veure rols" - "write:admin:relays": "Gestionar relé" - "read:admin:relays": "Veure relés" - "write:admin:invite-codes": "Gestionar codis d'invitació " - "read:admin:invite-codes": "Veure codis d'invitació " - "write:admin:announcements": "Gestionar anuncis" - "read:admin:announcements": "Veure anuncis" - "write:admin:avatar-decorations": "Gestionar la decoració dels avatars" - "read:admin:avatar-decorations": "Veure les decoracions dels avatars" - "write:admin:federation": "Gestionar la federació d'instàncies " - "write:admin:account": "Gestionar els comptes d'usuaris " - "read:admin:account": "Veure els comptes d'usuaris " - "write:admin:emoji": "Edició d'emojis" - "read:admin:emoji": "Veure emojis" - "write:admin:queue": "Gestionar la cua de feines" - "read:admin:queue": "Veure la cua de feines" - "write:admin:promo": "Gestiona les notes promocionals" - "write:admin:drive": "Gestiona el disc de l'usuari" - "read:admin:drive": "Veure la informació del disc de l'usuari" - "read:admin:stream": "Fes servir l'API sobre Websocket per l'administració" - "write:admin:ad": "Gestiona la publicitat" - "read:admin:ad": "Veure anuncis" - "write:invite-codes": "Crear codis d'invitació" - "read:invite-codes": "Obtenir codis d'invitació" - "write:clip-favorite": "Gestionar els clips favorits" - "read:clip-favorite": "Veure clips favorits" - "read:federation": "Veure dades de federació" - "write:report-abuse": "Informar d'un abús" - "write:chat": "Crear o esborrar missatges de xat" - "read:chat": "Explorar xats" -_auth: - shareAccessTitle: "Concedeix permisos a l'aplicació" - shareAccess: "Vols que {name} pugui accedir al vostre compte?" - shareAccessAsk: "Segur que vols que aquesta aplicació pugui accedir al vostre compte?" - permission: "{name} demana els següents permisos" - permissionAsk: "Aquesta aplicació demana els següents permisos" - pleaseGoBack: "Si us plau, torna a l'aplicació" - callback: "Tornant a l'aplicació" - accepted: "Accés garantit" - denied: "Accés denegat" - scopeUser: "Opera com si fossis aquest usuari" - pleaseLogin: "Si us plau, identificat per autoritzar l'aplicació." - byClickingYouWillBeRedirectedToThisUrl: "Si es garanteix l'accés, seràs redirigit automàticament a la següent adreça URL" + step2Url: "També pots inserir aquest enllaç i utilitzes una aplicació d'escriptori:" _antennaSources: all: "Totes les publicacions" homeTimeline: "Publicacions dels usuaris seguits" users: "Publicacions d'usuaris específics" userList: "Publicacions d'una llista d'usuaris" - userBlacklist: "Totes les notes excepte les d'un o alguns usuaris especificats" -_weekday: - sunday: "Diumenge" - monday: "Dilluns" - tuesday: "Dimarts" - wednesday: "Dimecres" - thursday: "Dijous" - friday: "Divendres" - saturday: "Dissabte" _widgets: profile: "Perfil" instanceInfo: "Informació del fitxer d'instal·lació" - memo: "Notes adhesives" notifications: "Notificacions" timeline: "Línia de temps" - calendar: "Calendari" - trends: "Tendència" - clock: "Rellotge" - rss: "Lector RSS" - rssTicker: "RSS ticker" activity: "Activitat" - photos: "Fotografies" - digitalClock: "Rellotge digital" - unixClock: "Rellotge UNIX" federation: "Federació" - instanceCloud: "Núvol d'instàncies" - postForm: "Formulari de publicació" - slideshow: "Presentació" - button: "Botó " - onlineUsers: "Usuaris actius" - jobQueue: "Cua de feines" - serverMetric: "Mètriques del servidor" - aiscript: "Consola AiScript" - aiscriptApp: "Aplicació AiScript" - aichan: "Ai" - userList: "Llistat d'usuaris" + jobQueue: "Cua de tasques" _userList: chooseList: "Tria una llista" - clicker: "Clicker" - birthdayFollowings: "Usuaris que fan l'aniversari avui" - chat: "Xat" _cw: - hide: "Amagar" show: "Carregar més" - chars: "{count} caràcters " - files: "{count} fitxer(s)" -_poll: - noOnlyOneChoice: "Es necessita escollir dues opcions com a mínim " - choiceN: "Opció {n}" - noMore: "No pots afegir més opcions" - canMultipleVote: "Permetre escollir diferents opcions" - expiration: "Finalitza el" - infinite: "Mai" - at: "Finalitza en..." - after: "Finalitza després..." - deadlineDate: "Data de finalització " - deadlineTime: "Hor(a)(es)" - duration: "Duració " - votesCount: "{n} vots" - totalVotes: "{n} vots en total" - vote: "Votar en una enquesta" - showResult: "Veure resultats" - voted: "Has votat" - closed: "Finalitzada" - remainingDays: "Queden {d} dies i {h} hores per finalitzar" - remainingHours: "Queden {h} hores i {m} minuts" - remainingMinutes: "Queden {m} minuts i {s} segons" - remainingSeconds: "Queden {s} segons" _visibility: - public: "Públic " - publicDescription: "La teva nota la podrà veure tothom " home: "Inici" - homeDescription: "Publicar només a la línia de temps d'Inici " followers: "Seguidors" - followersDescription: "Fes només visible per als teus seguidors" - specified: "Directe" - specifiedDescription: "Fer visible només per alguns usuaris" - disableFederation: "Sense federar" - disableFederationDescription: "No enviar a altres servidors" -_postForm: - replyPlaceholder: "Contestar..." - quotePlaceholder: "Citar..." - channelPlaceholder: "Publicar a un canal..." - _placeholders: - a: "Que vols dir?..." - b: "Alguna cosa interessant al teu voltant?..." - c: "Què et passa pel cap?..." - d: "Què vols dir?..." - e: "Escriu alguna cosa..." - f: "Esperant que escriguis qualsevol cosa..." _profile: - name: "Nom" username: "Nom d'usuari" - description: "Biografia " - youCanIncludeHashtags: "Pots posar etiquetes a la teva biografia " - metadata: "Informació adicional " - metadataEdit: "Editar la informació adicional " - metadataDescription: "Amb això podràs mostrar camps d'informació adicional al teu perfil." - metadataLabel: "Etiqueta " - metadataContent: "Contingut" - changeAvatar: "Canviar l'avatar " - changeBanner: "Canviar el bàner " - verifiedLinkDescription: "Escrivint una adreça URL que enllaci a aquest perfil, una icona de propietat verificada es mostrarà al costat del camp." - avatarDecorationMax: "Pot afegir un màxim de {max} decoracions." - followedMessage: "Missatge als nous seguidors" - followedMessageDescription: "Es pot configurar un missatge curt que es mostra a l'altra persona quan comença a seguir-te." - followedMessageDescriptionForLockedAccount: "Si comencen a seguir-te es mostra un missatge de quan es permet aquesta sol·licitud. " _exportOrImport: allNotes: "Totes les publicacions" - favoritedNotes: "Notes preferides" - clips: "Retalls" - followingList: "Seguint " + followingList: "Seguint" muteList: "Silencia" blockingList: "Bloqueja" userLists: "Llistes" - excludeMutingUsers: "Exclou usuaris silenciats" - excludeInactiveUsers: "Exclou usuaris inactius" - withReplies: "Inclou a la línia de temps les respostes d'usuaris importats" _charts: federation: "Federació" - apRequest: "Peticions" - usersIncDec: "Diferència entre el nombre d'usuaris" - usersTotal: "Nombre total d'usuaris" - activeUsers: "Usuaris actius" - notesIncDec: "Diferència entre el nombre de notes" - localNotesIncDec: "Diferencia en el nombre de notes locals" - remoteNotesIncDec: "Diferencia en el nombre de notes remotes" - notesTotal: "Nombre total de notes" - filesIncDec: "Diferencia en el nombre de fitxers" - filesTotal: "Nombre total de fitxers" - storageUsageIncDec: "Diferencia en l'emmagatzematge usat" - storageUsageTotal: "Emmagatzematge usat" -_instanceCharts: - requests: "Peticions" - users: "Diferència entre el nombre d'usuaris" - usersTotal: "Usuaris totals acumulats" - notes: "Diferència entre el nombre de notes" - notesTotal: "Notes totals acumulades" - ff: "Diferència en nombre d'usuaris seguits / seguidors" - ffTotal: "Nombre total acumulat d'usuaris seguits / seguidors" - cacheSize: "Diferència a la mida de la memòria cau" - cacheSizeTotal: "Total acumulat de la mida de la memòria cau" - files: "Diferència al nombre d'arxius" - filesTotal: "Nombre acumulatiu de fitxers" _timelines: home: "Inici" local: "Local" social: "Social" global: "Global" -_play: - new: "Crear un guió" - edit: "Editar guió" - created: "Guió creat" - updated: "Guió editat" - deleted: "Guió esborrat" - pageSetting: "Configuració del guió" - editThisPage: "Edita aquest guió" - viewSource: "Veure l'origen " - my: "Els meus guions" - liked: "Guions que m'han agradat" - featured: "Popular" - title: "Títol " - script: "Script" - summary: "Descripció" - visibilityDescription: "" _pages: - newPage: "pa" - editPage: "Editar la pàgina" - readPage: "Veure el codi font d'aquesta pàgina" - pageSetting: "Configuració de la pàgina" - nameAlreadyExists: "L'adreça URL de la pàgina ja existeix" - invalidNameTitle: "L'adreça URL de la pàgina no és vàlida" - invalidNameText: "Assegurat que el títol de la pàgina no és buit" - editThisPage: "Editar la pàgina" - viewSource: "Veure l'origen " - viewPage: "Veure les teves pàgines " - like: "M'agrada " - unlike: "Treure m'agrada " - my: "Les meves pàgines " - liked: "Pàgines que m'agraden " - featured: "Popular" - inspector: "Inspeccionar" contents: "Contingut" - content: "Bloquejar la pàgina " - variables: "Variables" - title: "Títol " - url: "URL de la pàgina " - summary: "Resum de la pàgina " - alignCenter: "Centrar elements" - hideTitleWhenPinned: "Amagar el títol de la pàgina quan estigui fixada al perfil" - font: "Lletra tipogràfica" - fontSerif: "Serif" - fontSansSerif: "Sans Serif" - eyeCatchingImageSet: "Escull una miniatura" - eyeCatchingImageRemove: "Esborrar la miniatura" - chooseBlock: "Afegeix un bloc" - enterSectionTitle: "Escriu el títol de la secció" - selectType: "Seleccionar tipus" - contentBlocks: "Contingut" - inputBlocks: "Entrada " - specialBlocks: "Especial" blocks: - text: "Text" - textarea: "Àrea de text" - section: "Secció " image: "Imatges" - button: "Botó " - dynamic: "Blocs dinàmics" - dynamicDescription: "Aquest bloc és antic. Ara en endavant fes servir {play}" - note: "Incorporar una Nota" _note: id: "ID de la publicació" - idDescription: "Alternativament pots enganxar l'adreça URL de la nota aquí." detailed: "Mostra els detalls" -_relayStatus: - requesting: "Pendent" - accepted: "Acceptat" - rejected: "Rebutjat" _notification: - fileUploaded: "Fitxer pujat sense cap problema" - youGotMention: "{name} t'ha mencionat" - youGotReply: "{name} t'ha contestat" - youGotQuote: "{name} t'ha citat" youRenoted: "Impulsat per {name}" youWereFollowed: "t'ha seguit" - youReceivedFollowRequest: "Has rebut una petició de seguiment" - yourFollowRequestAccepted: "La teva petició de seguiment ha sigut acceptada" - pollEnded: "Ja pots veure els resultats de l'enquesta " - newNote: "Nota nova" - unreadAntennaNote: "Antena {name}" - roleAssigned: "Rol assignat " - chatRoomInvitationReceived: "T'han invitat a una sala de xat" - emptyPushNotificationMessage: "Les notificacions han sigut actualitzades" - achievementEarned: "Aconseguiment desblocat" - testNotification: "Notificació de prova" - checkNotificationBehavior: "Comprova el comportament de la notificació " - sendTestNotification: "Enviar notificació de prova" - notificationWillBeDisplayedLikeThis: "Les notificacions és veure'n així " - reactedBySomeUsers: "Han reaccionat {n} usuaris" - likedBySomeUsers: "A {n} usuaris els hi agrada la teva nota" - renotedBySomeUsers: "L'han impulsat {n} usuaris" - followedBySomeUsers: "Et segueixen {n} usuaris" - flushNotification: "Netejar notificacions" - exportOfXCompleted: "Completada l'exportació de {x}" - login: "Algú ha iniciat sessió " - createToken: "Token d'accés generat" - createTokenDescription: "Si no saps què és, esborra el token des de {text}." _types: all: "Tots" - note: "Notes noves" - follow: "Segueix-me" + follow: "Seguint" mention: "Menció" - reply: "Respostes" - renote: "Impulsos" + renote: "Renotar" quote: "Citar" reaction: "Reaccions" - pollEnded: "Enquesta terminada" - receiveFollowRequest: "Rebuda una petició de seguiment" - followRequestAccepted: "Petició de seguiment acceptada" - roleAssigned: "Rol donat" - chatRoomInvitationReceived: "Invitat a la sala de xat" - achievementEarned: "Assoliment desbloquejat" - exportCompleted: "Exportació completada" - login: "Iniciar sessió" - createToken: "Creació de tokens d'accés " - test: "Prova la notificació" - app: "Notificacions d'aplicacions" _actions: - followBack: "També et segueix" + followBack: "t'ha seguit també" reply: "Respondre" - renote: "Impulsar" + renote: "Renotar" _deck: - alwaysShowMainColumn: "Mostrar sempre la columna principal" columnAlign: "Alinea les columnes" - columnGap: "Espai entre columnes" - deckMenuPosition: "Posició del menú del tauler" - navbarPosition: "Posició de la barra de navegació " - addColumn: "Afegeix una columna" - newNoteNotificationSettings: "Configuració de notificacions per a notes noves" - configureColumn: "Configuració de columnes" + addColumn: "Afig una columna" swapLeft: "Mou a l’esquerra" swapRight: "Mou a la dreta" swapUp: "Mou cap amunt" swapDown: "Mou cap avall" - stackLeft: "Pila a la columna esquerra" popRight: "Col·loca a la dreta" profile: "Perfil" newProfile: "Perfil nou" deleteProfile: "Elimina el perfil" - introduction: "Crea la interfície perfecta posant les columnes allà on vulguis!" - introduction2: "Fes clic al botó + de la dreta per afegir noves columnes sempre que vulguis." - widgetsIntroduction: "Selecciona \"Editar ginys\" a la columna del menú i afegeix un." - useSimpleUiForNonRootPages: "Usa una interfície senzilla per a les pàgines navegades" - usedAsMinWidthWhenFlexible: "L'amplada mínima es farà servir quan \"Ajust automàtic de l'amplada\" estigui activat" - flexible: "Ajust automàtic de l'amplada" - enableSyncBetweenDevicesForProfiles: "Activar la sincronització de la informació de perfils de dispositiu a dispositiu" _columns: main: "Principal" widgets: "Ginys" @@ -2685,435 +459,5 @@ _deck: tl: "Línia de temps" antenna: "Antena" list: "Llistes" - channel: "Canals" mentions: "Mencions" direct: "Publicacions directes" - roleTimeline: "Línia de temps dels rols" - chat: "Xat" -_dialog: - charactersExceeded: "Has arribat al màxim de caràcters! Actualment és {current} de {max}" - charactersBelow: "Ets per sota del mínim de caràcters! Actualment és {current} de {min}" -_disabledTimeline: - title: "Línia de tems desactivada" - description: "No pots fer servir aquesta línia de temps amb els teus rols actuals." -_drivecleaner: - orderBySizeDesc: "Mida del fitxer descendent" - orderByCreatedAtAsc: "Data ascendent" -_webhookSettings: - createWebhook: "Crear un Webhook" - modifyWebhook: "Modificar un Webhook" - name: "Nom" - secret: "Secret" - trigger: "Activador" - active: "Activat" - _events: - follow: "Quan se segueix a un usuari" - followed: "Quan et segueixen" - note: "Quan es publica una nota" - reply: "Quan es rep una resposta" - renote: "Quan es renoti" - reaction: "Quan es rep una reacció " - mention: "Quan et mencionen" - _systemEvents: - abuseReport: "Quan reps un nou informe de moderació " - abuseReportResolved: "Quan resols un informe de moderació " - userCreated: "Quan es crea un usuari" - inactiveModeratorsWarning: "Quan el compte d'un moderador no té activitat durant un temps" - inactiveModeratorsInvitationOnlyChanged: "Quan el compte d'un moderador no té activitat durant un temps, i el servidor es canvia a registre per invitacions" - deleteConfirm: "Segur que vols esborrar el webhook?" - testRemarks: "Si feu clic al botó a la dreta de l'interruptor, podeu enviar un webhook de prova amb dades dummy." -_abuseReport: - _notificationRecipient: - createRecipient: "Afegeix un destinatari a l'informe de moderació " - modifyRecipient: "Editar un destinatari en l'informe de moderació " - recipientType: "Tipus de notificació " - _recipientType: - mail: "Correu electrònic" - webhook: "Webhook" - _captions: - mail: "Enviar un correu electrònic a tots els moderadors quan es rep un informe de moderació " - webhook: "Enviar una notificació al SystemWebhook quan es rebi o es resolgui un informe de moderació " - keywords: "Paraules clau" - notifiedUser: "Usuaris que s'han de notificar " - notifiedWebhook: "Webhook que s'ha de fer servir" - deleteConfirm: "Segur que vols esborrar el destinatari de l'informe de moderació?" -_moderationLogTypes: - createRole: "Rol creat" - deleteRole: "Rol esborrat" - updateRole: "Rol actualitzat" - assignRole: "Assignat al rol" - unassignRole: "Esborrat del rol" - suspend: "Suspèn" - unsuspend: "Suspensió treta" - addCustomEmoji: "Afegit emoji personalitzat" - updateCustomEmoji: "Actualitzat emoji personalitzat" - deleteCustomEmoji: "Esborrat emoji personalitzat" - updateServerSettings: "Configuració del servidor actualitzada" - updateUserNote: "Nota de moderació actualitzada" - deleteDriveFile: "Fitxer esborrat" - deleteNote: "Nota esborrada" - createGlobalAnnouncement: "Anunci global creat" - createUserAnnouncement: "Anunci individual creat" - updateGlobalAnnouncement: "Anunci global actualitzat" - updateUserAnnouncement: "Anunci individual actualitzat " - deleteGlobalAnnouncement: "Anunci global esborrat" - deleteUserAnnouncement: "Anunci individual esborrat " - resetPassword: "Restableix la contrasenya" - suspendRemoteInstance: "Servidor remot suspès " - unsuspendRemoteInstance: "S'ha tret la suspensió del servidor remot" - updateRemoteInstanceNote: "Nota de moderació de la instància remota actualitzada" - markSensitiveDriveFile: "Fitxer marcat com a sensible" - unmarkSensitiveDriveFile: "S'ha tret la marca de sensible del fitxer" - resolveAbuseReport: "Informe resolt" - forwardAbuseReport: "Informe reenviat" - updateAbuseReportNote: "Nota de moderació d'un informe actualitzat" - createInvitation: "Crear codi d'invitació " - createAd: "Anunci creat" - deleteAd: "Anunci esborrat" - updateAd: "Anunci actualitzat" - createAvatarDecoration: "Decoració de l'avatar creada" - updateAvatarDecoration: "S'ha actualitzat la decoració de l'avatar " - deleteAvatarDecoration: "S'ha esborrat la decoració de l'avatar " - unsetUserAvatar: "Esborrar l'avatar d'aquest usuari" - unsetUserBanner: "Esborrar el bàner d'aquest usuari" - createSystemWebhook: "Crear un SystemWebhook" - updateSystemWebhook: "Actualitzar SystemWebhook" - deleteSystemWebhook: "Esborrar SystemWebhook" - createAbuseReportNotificationRecipient: "Crear un destinatari per l'informe de moderació " - updateAbuseReportNotificationRecipient: "Actualitzar destinatari per l'informe de moderació " - deleteAbuseReportNotificationRecipient: "Esborrar destinatari de l'informe de moderació " - deleteAccount: "Esborrar el compte " - deletePage: "Esborrar la pàgina" - deleteFlash: "Esborrar el guió" - deleteGalleryPost: "Esborrar la publicació de la galeria" - deleteChatRoom: "Esborra la sala de xat" - updateProxyAccountDescription: "Actualitzar descripció del compte proxy" -_fileViewer: - title: "Detall del fitxer" - type: "Tipus de fitxer" - size: "Mida" - url: "URL" - uploadedAt: "Pujat el" - attachedNotes: "Notes amb aquest fitxer" - thisPageCanBeSeenFromTheAuthor: "Aquesta pàgina només la pot veure l'usuari que ha pujat aquest fitxer." -_externalResourceInstaller: - title: "Instal·lar des d'un lloc extern" - checkVendorBeforeInstall: "Assegura't que qui distribueix aquest recurs és fiable abans d'instal·lar-ho." - _plugin: - title: "Vols instal·lar aquest afegit?" - _theme: - title: "Vols instal·lar aquest tema?" - _meta: - base: "Paleta de colors base" - _vendorInfo: - title: "Informació del distribuïdor " - endpoint: "Punt final referenciat" - hashVerify: "Verificació d'integritat " - _errors: - _invalidParams: - title: "Paràmetres no vàlids " - description: "No hi ha suficient informació per carregar les dades del lloc extern. Confirma l'URL que hi ha escrita." - _resourceTypeNotSupported: - title: "El recurs extern no està suportat." - description: "Aquesta mena de recurs no està suportat. Contacta amb l'administrador." - _failedToFetch: - title: "Ha fallat l'obtenció de dades" - fetchErrorDescription: "Ha aparegut un error comunicant-se amb el lloc extern. Si després d'intentar-ho un altre cop no es resol, contacta amb l'administrador." - parseErrorDescription: "Ha aparegut un error processant les dades carregades del lloc extern. Contacta amb l'administrador." - _hashUnmatched: - title: "Ha fallat la verificació de les dades" - description: "Ha aparegut un error verificant les dades obtingudes. Com a mesura de seguretat la instal·lació no pot continuar. Contacta amb l'administrador." - _pluginParseFailed: - title: "Error d'AiScript" - description: "Les dades sol·licitades s'han obtingut correctament, però hem trobat un error durant el processament d'AiScript. Contacta amb l'autor de l'afegit. Detalls de l'error es pot veure a la consola JavaScript." - _pluginInstallFailed: - title: "La instal·lació de l'afegit a fallat" - description: "Ha aparegut un error durant la instal·lació de l'afegit. Intenta-ho una altra vegada. El detall de l'error es pot veure a la consola JavaScript." - _themeParseFailed: - title: "Ha fallat el processament del tema" - description: "Les dades sol·licitades s'han obtingut correctament, però hem trobat un error durant el processament del tema. Contacta amb l'autor de l'afegit. Detalls de l'error es pot veure a la consola JavaScript." - _themeInstallFailed: - title: "La instal·lació del tema a fallat" - description: "Ha aparegut un error durant la instal·lació del tema. Intenta-ho una altra vegada. El detall de l'error es pot veure a la consola JavaScript." -_dataSaver: - _media: - title: "Carregant multimèdia " - description: "Desactiva la càrrega automàtica d'imatges i vídeos. Les imatges i els vídeos amagats es carregaran quan es faci clic a sobre." - _avatar: - title: "Avatars animats" - description: "Detenir l'animació dels avatars animats. Les imatges animades solen tenir un pes més gran que les imatges normals, reduint el tràfic disponible." - _urlPreviewThumbnail: - title: "Amagar les miniatures de la vista prèvia d'URL" - description: "Les imatges en miniatura de la vista prèvia d'URL ja no es carreguen" - _disableUrlPreview: - title: "Desactivar la vista prèvia d'URL" - description: "Desactiva la funció de previsualització d'URL. A diferència de les imatges en miniatura soles, això redueix la càrrega de la mateixa informació vinculada." - _code: - title: "Ressaltat del codi " - description: "Quan s'utilitza codi MFM, no es llegeix fins que es copiï. En els punts destacats del codi s'han de llegir els fitxers definits per a cada llengua que resulti alt, però no es poden llegir automàticament, per la qual cosa es poden reduir les quantitats de comunicació." -_hemisphere: - N: "Hemisferi Nord " - S: "Hemisferi Sud" - caption: "El fan servir alguns clients per determinar l'estació de l'any." -_reversi: - reversi: "Reversi" - gameSettings: "Opcions del joc" - chooseBoard: "Escull un tauler" - blackOrWhite: "Negres/Blanques" - blackIs: "{name} juga amb negres " - rules: "Regles" - thisGameIsStartedSoon: "El joc començarà en breu" - waitingForOther: "Esperant la tirada de l'oponent " - waitingForMe: "Esperant el teu torn" - waitingBoth: "Prepara't " - ready: "Preparat " - cancelReady: " No preparat " - opponentTurn: "Torn de l'oponent " - myTurn: "El teu torn" - turnOf: "Li toca a {name}" - pastTurnOf: "Torn de {name}" - surrender: "Rendeix-te" - surrendered: "T'has rendit" - timeout: "Temps esgotat" - drawn: "Empat" - won: "{name} ha guanyat" - black: "Negres" - white: "Blanques" - total: "Total" - turnCount: "Torn {count}" - myGames: "Jugades" - allGames: "Totes les jugades" - ended: "Acabat" - playing: "Jugant" - isLlotheo: "Qui tingui menys pedres guanya (Llotheo)" - loopedMap: "Mapa de recursiu" - canPutEverywhere: "Les fitxes es poden posar a qualsevol lloc" - timeLimitForEachTurn: "Temps límit per jugada" - freeMatch: "Partida lliure" - lookingForPlayer: "Buscant contrincant..." - gameCanceled: "La partida s'ha cancel·lat " - shareToTlTheGameWhenStart: "Compartir la partida a la línia de temps quan comenci" - iStartedAGame: "La partida ha començat! #MisskeyReversi" - opponentHasSettingsChanged: "L'oponent h canviat la seva configuració " - allowIrregularRules: "Regles irregulars (totalment lliure)" - disallowIrregularRules: "Sense regles irregulars" - showBoardLabels: "Mostrar el número de línia i columna al tauler de joc" - useAvatarAsStone: "Fer servir els avatars dels usuaris com a fitxes" -_offlineScreen: - title: "Fora de línia - No es pot connectar amb el servidor" - header: "Impossible connectar amb el servidor" -_urlPreviewSetting: - title: "Configuració per a la previsualització de l'URL" - enable: "Activa la previsualització de l'URL" - allowRedirect: "Permet la redirecció de la visualització prèvia " - allowRedirectDescription: "Estableix si es mostra o no la redirecció a la vista prèvia quan l'adreça URL introduïda té una redirecció. Si es desactiva s'estalvien recursos del servidor, però no es mostrarà el contingut de la redirecció." - timeout: "Temps màxim per carregar la previsualització de l'URL (ms)" - timeoutDescription: "Si l'obtenció de la previsualització triga més que el temps establert, no es generarà la vista prèvia." - maximumContentLength: "Longitud màxima del contingut (bytes)" - maximumContentLengthDescription: "Si la màxima longitud és més gran que aquest valor, la previsualització no es generarà." - requireContentLength: "Generar la previsualització només si es pot obtenir la longitud màxima " - requireContentLengthDescription: "Si l'altre servidor no proporciona la longitud màxima, la previsualització no es generarà." - userAgent: "User-Agent" - userAgentDescription: "Estableix l'User-Agent que és farà servir per a la recuperació de la vista prèvia. Si és deixa en blanc es farà servir l'User-Agent per defecte." - summaryProxy: "Proxy endpoints per generar vistes prèvies" - summaryProxyDescription: "La vista prèvia es genera fent servir Summaly proxy, no la genera el mateix Misskey." - summaryProxyDescription2: "Els següents paràmetres són passats al proxy com cadenes de consulta. Si el proxy no els admet, s'ignoren els valors configurats." -_mediaControls: - pip: "Imatge sobre impressionada " - playbackRate: "Velocitat de reproducció " - loop: "Reproducció en bucle" -_contextMenu: - title: "Menú contextual" - app: "Aplicació " - appWithShift: "Aplicació amb la tecla shift" - native: "Interfície del navegador" -_gridComponent: - _error: - requiredValue: "Aquest camp és obligatori" - columnTypeNotSupport: "La validació d'expressions regulars només s'admet per columnes de tipus text." - patternNotMatch: "Aquest valor no coincideix amb {pattern}" - notUnique: "Aquest valor ha de ser únic " -_roleSelectDialog: - notSelected: "No seleccionat" -_customEmojisManager: - _gridCommon: - copySelectionRows: "Copiar línies seleccionades " - copySelectionRanges: "Copiar selecció " - deleteSelectionRows: "Esborrar línies seleccionades" - deleteSelectionRanges: "Esborrar files de la selecció " - searchSettings: "Configuració del cercador" - searchSettingCaption: "Defineix criteris de cerca detallats." - searchLimit: "Nombre de pantalles" - sortOrder: "Ordenar" - registrationLogs: "Registres d'inscripcions " - registrationLogsCaption: "Quan s'actualitzin o s'esborrin emojis es mostrarà un registre. Desapareixeran quan s'actualitzin, s'esborrin, visitis una nova pàgina o la recarreguis." - alertEmojisRegisterFailedDescription: "No s'ha pogut actualitzar o esborrar l'emoji. Si us plau, dona una ullada al registre per més detalls." - _logs: - showSuccessLogSwitch: "Mostrar el registre d'èxit " - failureLogNothing: "No hi ha registres de fallades." - logNothing: "No hi ha registres." - _remote: - selectionRowDetail: "Detall de la línia seleccionada" - importSelectionRows: "Importar les files seleccionades" - importSelectionRangesRows: "Importar les files de la selecció " - importEmojisButton: "Importar els Emojis marcats" - confirmImportEmojisTitle: "Importar Emojis" - confirmImportEmojisDescription: "Importar {count} Emojis d'una adreça remota. Tingues cura de les llicències dels Emojis. Vols importar-los?" - _local: - tabTitleList: "Llistar els Emojis registrats" - tabTitleRegister: "Registre d'Emojis" - _list: - emojisNothing: "No hi ha Emojis registrats" - markAsDeleteTargetRows: "Files seleccionades que s'han d'esborrar " - markAsDeleteTargetRanges: "Selecció de files per la seva eliminació " - alertUpdateEmojisNothingDescription: "No hi ha Emojis actualitzats." - alertDeleteEmojisNothingDescription: "No hi ha Emoji per esborrar." - confirmMovePage: "Vols canviar de pàgina?" - confirmChangeView: "Vols canviar la pantalla?" - confirmUpdateEmojisDescription: "Actualitzar {count} Emojis. Vols executar-ho?" - confirmDeleteEmojisDescription: "Esborrar {count} Emojis marcats. Vols continuar?" - confirmResetDescription: "Es restabliran tots els canvis fets fins ara." - confirmMovePageDesciption: "S'han fet canvis als Emojis d'aquesta pàgina. Si continues navegant sense guardar els canvis, es perdran tots els canvis fets en aquesta pàgina." - dialogSelectRoleTitle: "Buscar Emojis per rol" - _register: - uploadSettingTitle: "Actualitza la configuració " - uploadSettingDescription: "En aquesta pantalla pots configurar el que s'ha de fer quan es puja un Emoji." - directoryToCategoryLabel: "Escriu el nom del directori al camp de \"categoria\"" - directoryToCategoryCaption: "Quan arrossegues un directori, escriu el nom del directori al camp categoria." - confirmRegisterEmojisDescription: "Registrar els Emojis de la llista com a nous Emojis personalitzats. Vols continuar? (Per evitar una sobrecàrrega només {count} Emojis es poden registrar d'una sola vegada)" - confirmClearEmojisDescription: "Descartar els canvis i esborrar els Emojis de la llista. Vols continuar?" - confirmUploadEmojisDescription: "Pujar els {count} fitxers que has arrossegat al disc. Vols continuar?" -_embedCodeGen: - title: "Personalitza el codi per incrustar" - header: "Mostrar la capçalera" - autoload: "Carregar automàticament (no recomanat)" - maxHeight: "Alçada màxima" - maxHeightDescription: "0 anul·la la configuració màxima. Per evitar que continuï creixent verticalment, especifiqui qualsevol valor." - maxHeightWarn: "El límit màxim d'alçada és nul (0). Si això no és un canvi previst, estableix el màxim d'alçada a un cert valor." - previewIsNotActual: "La visualització és diferent de la que es mostra quan s'implanta." - rounded: "Angle recte" - border: "Afegeix un marc al contenidor" - applyToPreview: "Aplica a la vista prèvia" - generateCode: "Crea el codi per incrustar" - codeGenerated: "Codi generat" - codeGeneratedDescription: "Si us plau, enganxeu el codi generat al lloc web." -_selfXssPrevention: - warning: "Advertència " - title: "\"Enganxa qualsevol cosa en aquesta finestra\" És tot un engany." - description1: "Si posa alguna cosa al seu compte, un usuari malintencionat podria segrestar-la o robar-li les dades." - description2: "Si no entens que estàs fent %cpara ara mateix i tanca la finestra." - description3: "Per obtenir més informació. {link}" -_followRequest: - recieved: "Sol·licituds rebudes" - sent: "Sol·licituds enviades" -_remoteLookupErrors: - _federationNotAllowed: - title: "No es pot establir connexió amb aquest servidor" - description: "És possible que s'hagi desactivat la comunicació amb aquest servidor o que hagi estat bloquejat.\nPosa't en contacte amb l'administrador del servidor." - _uriInvalid: - title: "L'adreça és incorrecte" - description: "Hi ha un problema amb l'adreça introduïda; comprova que no hagis escrit caràcters que no es puguin fer servir." - _requestFailed: - title: "La sol·licitud a fallat" - description: "La comunicació amb aquest servidor a fallat. És possible que l'altre servidor no funcioni. Comprova també que no has posat una adreça no vàlida o inexistent." - _responseInvalid: - title: "La resposta no és correcta " - description: "Hem pogut comunicar-nos amb aquest servidor, però les dades rebudes no són correctes." - _noSuchObject: - title: "No s'ha trobat" - description: "No es pot trobar el recurs sol·licitat, si us plau comprova l'adreça una altra vegada." -_captcha: - verify: "Passar pel CAPTCHA" - testSiteKeyMessage: "Pots comprovar una vista prèvia introduïnt valors de prova per la clau del lloc i la clau secreta. Si vols més informació consulteu la següent pàgina." - _error: - _requestFailed: - title: "Ha fallat la sol·licitud del CAPTCHA" - text: "Si us plau, torna a intentar-ho d'aquí una estona o comprova els ajustos de nou." - _verificationFailed: - title: "Ha fallat la validació CAPTCHA" - text: "Comprova que els ajustos són els correctes." - _unknown: - title: "Error CAPTCHA" - text: "S'ha produït un error inesperat." -_bootErrors: - title: "Hi ha hagut en error en carregar" - serverError: "Si el problema persisteix després d'esperar una mica i recarregar, posa't en contacte amb l'administrador del servidor amb el següent codi d'error." - solution: "Per intentar resoldre el problema pots fer el següent." - solution1: "Actualitza el navegador i el sistema operatiu a l'última versió " - solution2: "Desactiva els adblockers" - solution3: "Esborra la memòria cau del navegador" - solution4: "(Navegador Tor) configura dom.webaudio.enabled a true" - otherOption: "Altres opcions" - otherOption1: "Esborrar la configuració i la memòria cau del client" - otherOption2: "Iniciar client senzill" - otherOption3: "Iniciar l'eina de reparació " -_search: - searchScopeAll: "Tot" - searchScopeLocal: "Local" - searchScopeServer: "Instància " - searchScopeUser: "Especificar usuari" - pleaseEnterServerHost: "Introdueix l'adreça de la instància " - pleaseSelectUser: "Selecciona un usuari" - serverHostPlaceholder: "Ex: misskey.example.com" -_serverSetupWizard: - installCompleted: "La instal·lació de Misskey ha finalitzat!" - firstCreateAccount: "Primer crea un compte d'administrador." - accountCreated: "Compte d'administrador creat." - serverSetting: "Configuració del servidor" - youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Aquest assistent t'ajuda a fer una configuració òptima del servidor." - settingsYouMakeHereCanBeChangedLater: "Els canvis que facis ara poden modificar-se més tard." - howWillYouUseMisskey: "Com es fa servir Misskey?" - _use: - single: "Servidor per una sola persona" - single_description: "Fes-ho servir com el teu propi servidor dedicat" - single_youCanCreateMultipleAccounts: "Es poden crear diferents comptes segons siguin les teves necessitats, inclús quan es fa servir com a servidor unipersonal." - group: "Servidor per a grups" - group_description: "Invita altres usuaris de la teva confiança i fes-ho servir amb més d'una persona." - open: "Servidor obert" - open_description: "Operar per donar cabuda a un nombre no determinat d'usuaris." - openServerAdvice: "Acceptar un nombre no determinat d'usuaris comporta alguns riscos. Es recomana operar amb un sistema de moderació fiable per fer front als problemes." - openServerAntiSpamAdvice: "També s'ha de tenir molta cura amb la seguretat, per exemple habilitant funcions anti-bot com reCAPTCHA, per assegurar-te que el teu servidor no es converteix en un trampolí per contingut brossa." - howManyUsersDoYouExpect: "Quantes persones preveus?" - _scale: - small: "Menys de 100 (petita escala)" - medium: "Més de 100 i menys de 1000 (mida mitjana)" - large: "Més de 1000 persones (gran escala)" - largeScaleServerAdvice: "Els grans servidors poden requerir coneixements avançats d'infraestructures, com balanceig de càrregues i replicació de base de dades." - doYouConnectToFediverse: "Desitges connectar-te amb el Fedivers?" - doYouConnectToFediverse_description1: "Quan es connecta amb una xarxa de servidors distribuïts (Fedivers), els continguts poden intercanviar-se amb altres servidors i entre ells." - doYouConnectToFediverse_description2: "La connexió amb el Fedivers també es coneix com a \"federació\"." - youCanConfigureMoreFederationSettingsLater: "Les configuracions avançades, com especificar els servidors amb els quals es pot federar, es poden fer més tard." - adminInfo: "Informació de l'administrador " - adminInfo_description: "Estableix la informació de l'administrador que es farà servir per rebre consultes." - adminInfo_mustBeFilled: "Aquesta informació ha de ser omplerta si el servidor té els registres oberts o la federació es troba activada." - followingSettingsAreRecommended: "Es recomana la següent configuració " - applyTheseSettings: "Aplicar aquesta configuració " - skipSettings: "Saltar la configuració " - settingsCompleted: "Configuració finalitzada " - settingsCompleted_description: "Gràcies per la teva ajuda. Ara que ja està tot llest, pots començar a fer servir el servidor immediatament." - settingsCompleted_description2: "La configuració avançada del servidor també poden fer-se des del \"Tauler de control\"." - donationRequest: "Una donació, si us plau" - _donationRequest: - text1: "Misskey és un programari gratuït fet per voluntaris." - text2: "Si ho desitges, agrairíem molt la teva donació per poder seguir desenvolupant el projecte." - text3: "També hi ha privilegis especials per als donants!" -_uploader: - compressedToX: "Comprimit a {x}" - savedXPercent: "{x}% d'estalvi " - abortConfirm: "Hi ha un arxiu que no s'ha pujat, vols cancel·lar?" - doneConfirm: "Hi han fitxers no pujats, vols completar-los?" - maxFileSizeIsX: "La mida màxima d'arxiu que es pot pujar és {x}." - allowedTypes: "Tipus de fitxers que en podeu pujar" - tip: "L'arxiu encara no s'ha carregat. En aquest quadre de diàleg, pots comprovar, canviar el nom, comprimir i retallar l'arxiu abans de pujar-lo. Quan estigui llest pots iniciar la càrrega polsant el boto \"Pujar\"" -_clientPerformanceIssueTip: - title: "Si creus que el consum de bateria és molt alt" - makeSureDisabledAdBlocker: "Desactiva els bloquejadors de publicitat" - makeSureDisabledAdBlocker_description: "Els bloquejadors d'anuncis pot afectar el rendiment, comprova que no estiguin activats per característiques del sistema operatiu o del navegador." - makeSureDisabledCustomCss: "Desactiva CSS personalitzat" - makeSureDisabledCustomCss_description: "L'anul·lació dels estils pot afectar el rendiment. Comprova que el CSS personalitzat o les extensions que reescriuen estils no estiguin activats." - makeSureDisabledAddons: "Desactiva extensions" - makeSureDisabledAddons_description: "Algunes extensions poden interferir en el comportament del client i afectar el rendiment. Desactiva les extensions del navegador i comprovar-ho." -_clip: - tip: "Clip és una funció que permet organitzar les teves notes." -_userLists: - tip: "Es poden crear llistes amb qualsevol usuari. La llista creada es pot mostrar com una línia de temps." diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 8ac43ab6d9..ceab50075a 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -1,17 +1,12 @@ --- _lang_: "Čeština" headlineMisskey: "Síť propojená poznámkami" -introMisskey: "Vítejte! Misskey je otevřená a decentralizovaná microblogovací služba.\n\"Poznámkami\" můžete sdílet co se zrovna děje se všemi ve Vašem okolí. 📡\nPomocí \"reakcí\" můžete sdílet své názory a pocity na ostatní poznámky. 👍\nPojďte objevovat nový svět! 🚀" -poweredByMisskeyDescription: "{name} je jeden ze serverů využívající open source platformu Misskey (nazývaná \"Misskey instance\")." +introMisskey: "Vítejte! Misskey je otevřený a decentralizovaný microblogový servis.\n\"Poznámkami\" můžete sdílet co se zrovna děje se všemi ve Vašem okolí. 📡\nPomocí \"reakcí\" můžete sdílet své názory a pocity na ostatní poznámky. 👍\nPojďte objevovat nový svět! 🚀" monthAndDay: "{day}. {month}." search: "Vyhledávání" -reset: "Obnovit" notifications: "Oznámení" username: "Uživatelské jméno" password: "Heslo" -initialPasswordForSetup: "Počáteční heslo pro nastavení" -initialPasswordIsIncorrect: "Počáteční heslo pro nastavení je nesprávné" -initialPasswordForSetupDescription: "Použijte heslo, které jste nastavili v konfiguračním souboru, pokud jste Misskey instalovali ručně.\nPokud užíváte Misskey hostovací službu, použijte poskytnuté heslo.\nPokud jste heslo nenastavovali, zanechte prázdné." forgotPassword: "Zapomenuté heslo" fetchingAsApObject: "Načítám data z Fediversu..." ok: "Potvrdit" @@ -19,7 +14,7 @@ gotIt: "Rozumím!" cancel: "Zrušit" noThankYou: "Ne děkuji" enterUsername: "Zadej uživatelské jméno" -renotedBy: "{user} přeposlal*a" +renotedBy: "{user} přeposla/a" noNotes: "Žádné poznámky" noNotifications: "Žádná oznámení" instance: "Instance" @@ -49,23 +44,13 @@ pin: "Připnout" unpin: "Odepnout" copyContent: "Zkopírovat obsah" copyLink: "Kopírovat odkaz" -copyRemoteLink: "Zkoprírovat vzdálený odkaz" -copyLinkRenote: "Zkopírovat odkaz renotu" delete: "Smazat" deleteAndEdit: "Smazat a upravit" deleteAndEditConfirm: "Jste si jistí že chcete smazat tuto poznámku a editovat ji? Ztratíte tím všechny reakce, sdílení a odpovědi na ni." addToList: "Přidat do seznamu" -addToAntenna: "Přidat do antény" sendMessage: "Odeslat zprávu" -copyRSS: "Kopírovat RSS" copyUsername: "Kopírovat uživatelské jméno" -copyUserId: "Kopírovat ID uživatele" -copyNoteId: "Kopírovat ID poznámky" -copyFileId: "Kopírovat ID souboru" -copyFolderId: "Kopírovat ID složky" -copyProfileUrl: "Kopírovat URL profilu" searchUser: "Vyhledat uživatele" -searchThisUsersNotes: "Prohledat poznámky uživatele" reply: "Odpovědět" loadMore: "Zobrazit více" showMore: "Zobrazit více" @@ -75,7 +60,6 @@ receiveFollowRequest: "Žádost o sledování přijata" followRequestAccepted: "Žádost o sledování přijata" mention: "Zmínění" mentions: "Zmínění" -directNotes: "Přímé poznámky" importAndExport: "Import a export" import: "Importovat" export: "Exportovat" @@ -98,7 +82,6 @@ error: "Chyba" somethingHappened: "Jejda. Něco se nepovedlo." retry: "Opakovat" pageLoadError: "Nepodařilo se načíst stránku" -pageLoadErrorDescription: "Tohle je obvykle způsobeno chybou sítě nebo mezipaměti prohlížeče. Zkuste vymazat mezipaměť a po chvíli čekání to zkuste znovu." serverIsDead: "Server neodpovídá. Počkejte chvíli a zkuste to znovu." youShouldUpgradeClient: "Pro zobrazení této stránky obnovte stránku pro aktualizaci klienta." enterListName: "Jméno seznamu" @@ -117,8 +100,6 @@ renoted: "Přeposláno" cantRenote: "Tento příspěvek nelze přeposlat." cantReRenote: "Odpověď nemůže být odstraněna." quote: "Citovat" -inChannelRenote: "Přeposlání v kanálu" -inChannelQuote: "Citace v kanálu" pinnedNote: "Připnutá poznámka" pinned: "Připnout" you: "Vy" @@ -135,8 +116,6 @@ unmarkAsSensitive: "Odznačit jako NSFW" enterFileName: "Zadejte název souboru" mute: "Ztlumit" unmute: "Odmlčet" -renoteMute: "Ztlumit poznámky" -renoteUnmute: "Zrušit ztlumení poznámek" block: "Zablokovat" unblock: "Odblokovat" suspend: "Zmrazit" @@ -146,10 +125,7 @@ unblockConfirm: "Jste si jistí že chcete odblokovat tento účet?" suspendConfirm: "Jste si jistí že chcete suspendovat tenhle účet?" unsuspendConfirm: "Jste si jistí že chcete obnovit tenhle účet?" selectList: "Vybrat seznam" -editList: "Upravit seznam" -selectChannel: "Vybrat kanál" selectAntenna: "Vyberte Anténu" -editAntenna: "Upravit anténu" selectWidget: "Zvolte widget" editWidgets: "Upravit widget" editWidgetsExit: "Hotovo" @@ -162,8 +138,6 @@ addEmoji: "Přidat emoji" settingGuide: "Doporučené nastavení" cacheRemoteFiles: "Ukládání vzdálených souborů do mezipaměti" cacheRemoteFilesDescription: "Zakázání tohoto nastavení způsobí, že vzdálené soubory budou odkazovány přímo, místo aby byly ukládány do mezipaměti. Tím se ušetří úložiště na serveru, ale zvýší se provoz, protože se negenerují miniatury." -cacheRemoteSensitiveFiles: "Uložit do mezipaměti vzdálené citlivé soubory" -cacheRemoteSensitiveFilesDescription: "Když je tohle nastavení zrušeno, tak jsou vzdálené citlivé soubory načítány přímo ze vzdálených instancí bez uložení do mezipaměti." flagAsBot: "Tento účet je bot" flagAsBotDescription: "Pokud je tento účet kontrolován programem zaškrtněte tuto možnost. To označí tento účet jako bot pro ostatní vývojáře a zabrání tak nekonečným interakcím s ostatními boty a upraví Misskey systém aby se choval k tomuhle účtu jako bot." flagAsCat: "Tenhle účet je kočka" @@ -172,12 +146,8 @@ flagShowTimelineReplies: "Zobrazovat odpovědi na časové ose" flagShowTimelineRepliesDescription: "Je-li zapnuto, zobrazí odpovědi uživatelů na poznámky jiných uživatelů na vaší časové ose." autoAcceptFollowed: "Automaticky akceptovat následování od účtů které sledujete" addAccount: "Přidat účet" -reloadAccountsList: "Obnovit list účtů" loginFailed: "Přihlášení se nezdařilo." showOnRemote: "Více na původním profilu" -continueOnRemote: "Pokračujte na původní profil" -chooseServerOnMisskeyHub: "Vyberete si server z Misskey Hubu" -inputHostName: "Zadejte doménu" general: "Obecně" wallpaper: "Obrázek na pozadí" setWallpaper: "Nastavení obrázku na pozadí" @@ -202,7 +172,6 @@ perHour: "za hodinu" perDay: "za den" stopActivityDelivery: "Přestat zasílat aktivitu" blockThisInstance: "Blokovat tuto instanci" -silenceThisInstance: "Utišit tuto instanci" operations: "Operace" software: "Software" version: "Verze" @@ -217,26 +186,17 @@ instanceInfo: "Informace o instanci" statistics: "Statistiky" clearQueue: "Vyčistit frontu" clearQueueConfirmTitle: "Jste si jisti že zrušit všechny úlohy ve frontě?" -clearQueueConfirmText: "Jakékoliv nedoručené poznámky ve frontě nebudou sdružovány. Většinou tahle operace není zapotřebí." clearCachedFiles: "Vyprázdnit mezipaměť" -clearCachedFilesConfirm: "Jste jistí že chcete smazat všechny vzdálené soubory v mezipaměti?" blockedInstances: "Blokované instance" -blockedInstancesDescription: "Vypište názvy hostitelů instancí, které chcete blokovat odděleně řádkovými zlomky. Uvedené instance již nebudou moci s touto instancí komunikovat." -muteAndBlock: "Ztlumení a blokování" -mutedUsers: "Zltumení uživatelé" -blockedUsers: "Blokovaní uživatelé" noUsers: "Žádní uživatelé" editProfile: "Upravit můj profil" -noteDeleteConfirm: "Jste si jistí že chcete smazat tuhle poznámku?" pinLimitExceeded: "Nemůžete připnout další poznámky." +intro: "Instalace Misskey byla dokončena! Prosím vytvořte admina." done: "Hotovo" processing: "Zpracovávám" preview: "Náhled" default: "Výchozí" -defaultValueIs: "Základní hodnota: {value}" noCustomEmojis: "Bez Emoji" -noJobs: "Žádné úlohy" -federating: "Sdružování" blocked: "Blokováno" suspended: "Suspendováno" all: "Vše" @@ -257,7 +217,6 @@ more: "Více!" featured: "Oblíbené poznámky" usernameOrUserId: "Uživatelské jméno nebo uživatelské id" noSuchUser: "Uživatel nebyl nalezen" -lookup: "Vyhledat" announcements: "Oznámení" imageUrl: "URL obrázku" remove: "Smazat" @@ -266,24 +225,19 @@ removeAreYouSure: "Jste si jistí že chcete smazat \"{x}\"?" deleteAreYouSure: "Jste si jistí že chcete smazat \"{x}\"?" resetAreYouSure: "Opravdu resetovat?" saved: "Uloženo" +messaging: "Zprávy" upload: "Nahrát soubory" -keepOriginalUploading: "Ponechat originální obrázek" -keepOriginalUploadingDescription: "Uloží původní nahraný obrázek jak je. Pokud je to vypnuté, vygeneruje se zobrazení verze na webu při nahrátí." fromDrive: "Z disku" fromUrl: "Z URL" uploadFromUrl: "Nahrát z URL adresy" uploadFromUrlDescription: "URL adresa souboru, který chcete nahrát" -uploadFromUrlRequested: "Upload zažádán" uploadFromUrlMayTakeTime: "Může trvat nějakou dobu, dokud nebude dokončeno nahrávání." explore: "Objevovat" messageRead: "Přečtené" noMoreHistory: "To je vše" +startMessaging: "Zahájit chat" nUsersRead: "přečteno {n} uživateli" agreeTo: "Souhlasím s {0}" -agree: "Souhlasím" -agreeBelow: "Souhlasím s následným" -basicNotesBeforeCreateAccount: "Důležité poznámky" -termsOfService: "Podmínky užívání" start: "Začít" home: "Domů" remoteUserCaution: "Tyto informace nemusí být aktuální jelikož uživatel je ze vzdálené instance." @@ -314,24 +268,17 @@ createFolder: "Vytvořit složku" renameFolder: "Přejmenovat složku" deleteFolder: "Odstranit složku" addFile: "Přidat soubor" -emptyDrive: "Váš disk je prázdný" emptyFolder: "Tato složka je prázdná" unableToDelete: "Nelze smazat" inputNewFileName: "Zadejte nový název" -inputNewDescription: "Zadejte nový popisek" inputNewFolderName: "Zadejte název nové složky" -circularReferenceFolder: "Koncová složka je podsložka složky, kterou chcete přesunout." -hasChildFilesOrFolders: "Nemůžete odstranit složku, která není prázdná." copyUrl: "Kopírovat URL" rename: "Přejmenovat" avatar: "Avatar" banner: "Baner" -displayOfSensitiveMedia: "Zobrazit citlivé média" -whenServerDisconnected: "Když ztratíte spojení se serverem" disconnectedFromServer: "Spojení bylo přerušeno" reload: "Aktualizovat" doNothing: "Ignorovat" -reloadConfirm: "Chcete obnovit časovou osu?" watch: "Sledovat" unwatch: "Přestat sledovat" accept: "Souhlasím" @@ -354,84 +301,48 @@ connectService: "Připojit" disconnectService: "Odpojit" enableLocalTimeline: "Povolit lokální čas" enableGlobalTimeline: "Povolit globální čas" -disablingTimelinesInfo: "Administrátoři a Moderátoři budou mít stálý přístup ke všem časovým osám i přes to že nejsou zapnuté." registration: "Registrace" +enableRegistration: "Povolit registraci novým uživatelům" invite: "Pozvat" -driveCapacityPerLocalAccount: "Kapacita disku na lokálního uživatele" -driveCapacityPerRemoteAccount: "Kapacita disku na vzdáleného uživatele" inMb: "V megabajtech" +iconUrl: "Favicon URL" bannerUrl: "Baner URL" backgroundImageUrl: "Adresa URL obrázku pozadí" basicInfo: "Základní informace" pinnedUsers: "Připnutí uživatelé" -pinnedUsersDescription: "Seznam uživatelských přezdívek oddělených řádkami bude připnutý v záložce \"Objevit\"." -pinnedPages: "Připnutý stránky" -pinnedPagesDescription: "Zadejte cesty stránek oddělené řádkami, které si přejete mít přípnutý na vrcholu téhle instance." -pinnedClipId: "ID připnutého klipu" pinnedNotes: "Připnutá poznámka" hcaptcha: "hCaptcha" enableHcaptcha: "Aktivovat hCaptchu" hcaptchaSiteKey: "Klíč stránky" hcaptchaSecretKey: "Tajný Klíč (Secret Key)" -mcaptcha: "mCaptcha" -enableMcaptcha: "Aktivovat mCaptchu" -mcaptchaSiteKey: "Klíč stránky" -mcaptchaSecretKey: "Tajný Klíč (Secret Key)" -mcaptchaInstanceUrl: "URL mCaptcha serveru" recaptcha: "reCAPTCHA" enableRecaptcha: "Zapnout ReCAPTCHu" recaptchaSiteKey: "Klíč stránky" recaptchaSecretKey: "Tajný Klíč (Secret Key)" -turnstile: "Turnstile" -enableTurnstile: "Povolit Turnstile" turnstileSiteKey: "Klíč stránky" turnstileSecretKey: "Tajný Klíč (Secret Key)" -avoidMultiCaptchaConfirm: "Používání několik Captcha systému může způsobit konflikt mezi nimi. Chtěli byste vypnout ostatní aktivní Captcha systémy? Pokud je chcete nechat zapnuté, stiskněte zrušit." antennas: "Antény" manageAntennas: "Spravovat Antény" name: "Jméno" antennaSource: "Zdroj Antény" -antennaKeywords: "Klíčová slova na poslech" -antennaExcludeKeywords: "Vyloučená klíčová slova" -antennaKeywordsDescription: "Oddělte mezerami pro AND kondice nebo řádkami pro OR kondice." -notifyAntenna: "Upozornit na nové poznámky" -withFileAntenna: "Poznámky jenom se souborama" enableServiceworker: "Povolit ServiceWorker" -antennaUsersDescription: "Vypsat jednoho uživatele na řádek" caseSensitive: "Rozlišuje malá a velká písmena" -withReplies: "Zahrnout odpovědi" connectedTo: "Následující účty jsou připojeny" notesAndReplies: "Poznámky a odpovědi" withFiles: "Včetně souborů" -silence: "Ztlumení" -silenceConfirm: "Jste si jistí že chcete ztlumit tohoto uživatele?" -unsilence: "Zrušit ztlumení" -unsilenceConfirm: "Jste jistí že chcete vrátit zltumení tohoto uživatele?" popularUsers: "Populární uživatelé" recentlyUpdatedUsers: "Nedávno aktívni uživatelé" -recentlyRegisteredUsers: "Nově připojený uživatelé" -recentlyDiscoveredUsers: "Nově objevený uživatelé" -exploreUsersCount: "Existuje {count} uživatelů" -exploreFediverse: "Objevovat Fediverse" popularTags: "Populární tagy" userList: "Seznamy" about: "Informace" aboutMisskey: "O Misskey" administrator: "Administrátor" token: "Token" -2fa: "Dvoufázové ověření" -totp: "Ověřovací aplikace" -totpDescription: "Použít ověřovací aplikaci pro použití jednorázových hesel" moderator: "Moderátor" -moderation: "Moderování" nUsersMentioned: "{n} uživatelů zmínilo" -securityKeyAndPasskey: "Bezpečnostní klíče a tokeny" securityKey: "Bezpečnostní klíč" lastUsed: "Naposledy použito" -lastUsedAt: "Naposledy použito: {t}" unregister: "Odstranit" -passwordLessLogin: "Přihlášení bez hesla" -passwordLessLoginDescription: "Umožní bez-heslové přihlášení pomocí bezpečnostního klíče či tokenu" resetPassword: "Resetovat heslo" newPasswordIs: "Nové heslo je \"{password}\"" reduceUiAnimation: "Snížit UI animace" @@ -439,6 +350,7 @@ share: "Sdílet" notFound: "Nenalezeno" notFoundDescription: "Nebyla nalezená žádná stránka korespondující se zadanou URL." uploadFolder: "Výchozí lokace pro upload" +cacheClear: "Vymazat cache" markAsReadAllNotifications: "Označit všechna oznámení za přečtená" markAsReadAllUnreadNotes: "Označit všechny příspěvky za přečtené" markAsReadAllTalkMessages: "Označit všechny zprávy za přečtené" @@ -456,6 +368,8 @@ retype: "Zadejte znovu" noteOf: "{user} poznámky" quoteAttached: "Citace" quoteQuestion: "Přiložit jako citaci?" +noMessagesYet: "Zatím tu nejsou žádné zprávy" +newMessageExists: "Máte novou zprávu" onlyOneFileCanBeAttached: "Ke zprávě můžete přiložit jenom jeden soubor" signinRequired: "Přihlašte se, prosím" invitations: "Pozvat" @@ -477,26 +391,14 @@ or: "Nebo" language: "Jazyk" uiLanguage: "Jazyk uživatelského rozhraní" aboutX: "O {x}" -emojiStyle: "Styl emoji" -native: "Výchozí" -style: "Vzhled" -popup: "Vyskakovací okno" -showNoteActionsOnlyHover: "Zobrazit akce poznámky jenom při naběhnutí myši" noHistory: "Žádná historie" signinHistory: "Historie přihlášení" -enableAdvancedMfm: "Zapnout pokročilé MFM" -enableAnimatedMfm: "Zapnout animované MFM" -doing: "Procesuju..." category: "Kategorie" tags: "Štítky" -docSource: "Zdroj tohoto dokumentu" createAccount: "Vytvořit účet" existingAccount: "Existující účet" regenerate: "Obnovit" fontSize: "Velikost písma" -mediaListWithOneImageAppearance: "Výška seznamu médií s jedním obrázkem" -limitTo: "Omezeno na {x}" -noFollowRequests: "Nemáte žádné žádosti o sledování" openImageInNewTab: "Otevřít obrázek v novém panelu" dashboard: "Přehled" local: "Lokální" @@ -510,40 +412,19 @@ accountSettings: "Nastavení účtu" promotion: "Propagace" promote: "Propagovat" numberOfDays: "Počet dní" -hideThisNote: "Skrýt tuto poznámku" -showFeaturedNotesInTimeline: "Zobrazit významné poznámky v časové ose" -objectStorage: "Úložiště objektů" -useObjectStorage: "Použít úložiště objektů" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "URL použitá jako reference. Upřesněte URL vlastní CDN nebo Proxy pokud používáte jeden z nich. Pro S3 použijte 'https://.s3.amazonaws.com' a pro GCS nebo ekvivalentní služby použijte 'https://storage.googleapis.com/', apd." objectStorageBucket: "Bucket" -objectStorageBucketDesc: "Prosím upřesněte název bucketu používaný poskytovatelem." objectStoragePrefix: "Předpona" -objectStoragePrefixDesc: "Soubory budou ukládány pod složkama s tímhle prefixem." objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "Ponechte tohle prázdné pokud používáte AWS S3, jinak upřesněte endpoint jako \"\" nebo \":\", podle toho jakou službu používáte." objectStorageRegion: "Región" -objectStorageRegionDesc: "Upřesněte region jako například \"xx-east-1\". Pokud vlastní služba nerozlišuje mezi regiony, zadejte \"us-east-1\". Zanechte prázdné pokud používáte AWS konfiguraci či proměnné veličiny." objectStorageUseSSL: "Použít SSL" -objectStorageUseSSLDesc: "Vypněte to pokud nebudete používat HTTPS pro API připojení" -objectStorageUseProxy: "Připojení skrze Proxy" -objectStorageUseProxyDesc: "Vypněte to pokud nebudete používat Proxy pro API připojení." -objectStorageSetPublicRead: "Při nahrátí nastavit na \"public-read\"" -s3ForcePathStyleDesc: "Pokud je povolena funkce s3ForcePathStyle, musí být název Bucketu zahrnut do cesty k adrese URL, nikoli do názvu hostitele adresy URL. Toto nastavení může být nutné povolit při používání služeb, jako je například samostatně hostovaná instance Minio." -serverLogs: "Logy serveru" deleteAll: "Smazat vše" showFixedPostForm: "Zobrazit formulář pro nové příspěvky nad časovou osou" -showFixedPostFormInChannel: "Zobrazit vkládací formulář na vrcholu časové osy (Kanály)" -newNoteRecived: "Jsou k dispozici nové poznámky" -sounds: "Zvuky" -sound: "Zvuky" listen: "Poslouchat" -none: "Žádný" showInPage: "Zobrazit na stránce" popout: "Pop-out" volume: "Hlasitost" masterVolume: "Celková hlasitost" -notUseSound: "Zakázat zvuk" details: "Detaily" chooseEmoji: "Vybrat emotikon" unableToProcess: "Operace nebyla dokončena." @@ -552,61 +433,29 @@ install: "Nainstalovat" uninstall: "Odinstalovat" installedApps: "Autorizované aplikace" nothing: "Nic nebylo nalezeno" -installedDate: "Datum autorizace" lastUsedDate: "Poslední použití" state: "Stav" sort: "Seřadit" ascendingOrder: "Vzestupně" descendingOrder: "Sestupně" scratchpad: "Zápisník" -scratchpadDescription: "Scratchpad poskytuje rozhraní pro AiScript experimenty. Můžete psát, spustit či zkontrolovat výsledky jeho interakce s Misskey." output: "Výstup" script: "Skript" -disablePagesScript: "Vypnout AiScript na stránkách" updateRemoteUser: "Aktualizovat informace o vzdáleném účtu" deleteAllFiles: "Smazat všechny soubory" deleteAllFilesConfirm: "Jste si jistí že chcete smazat všechny soubory?" -removeAllFollowing: "Přestat sledovat všechny sledované uživatele" -removeAllFollowingDescription: "Spuštěním přestanete sledovat všechny účty z {host}. Prosíme spustěte tohle v případě že instance už neexistuje. " userSuspended: "Tomuto uživateli byl pozastaven účet." -userSilenced: "Tenhle uživatel je umlčen." -yourAccountSuspendedTitle: "Tenhle účet je zmrazený" -yourAccountSuspendedDescription: "Tenhle účet byl zmrazen z důvodu porušení smluvní podmínky serveru. Pro přesnější informace kontaktujte administrátora. Prosíme nezakládejte si nový účet." -tokenRevoked: "Nesprávný token" -tokenRevokedDescription: "Tenhle token vyprchal. Prosíme přihlašte se znova." -accountDeleted: "Účet smazán" -accountDeletedDescription: "Tenhle účet byl smazán." menu: "Menu" divider: "Dělící čára" addItem: "Přidat položku" -rearrange: "Přeřadit" relays: "Relay" addRelay: "Přidat Relay" inboxUrl: "Inbox URL" -addedRelays: "Přidané přenosy" -serviceworkerInfo: "Musí být zapnut pro push notifikace." deletedNote: "Odstraněné příspěvky" invisibleNote: "Skryté příspěvky" -enableInfiniteScroll: "Automaticky načítat více" -visibility: "Viditelnost" -poll: "Anketa" -useCw: "Schovat obsah" -enablePlayer: "Otevřít video přehrávač" -disablePlayer: "Zavřít video přehrávač" -expandTweet: "Rozbalit tweet" -themeEditor: "Editor témat" description: "Popis" -describeFile: "Přidat popisek" -enterFileDescription: "Vložit popisek" author: "Autor" -leaveConfirm: "Máte neuložené změny. Opravdu je chcete zahodit?" manage: "Administrace" -plugins: "Pluginy" -preferencesBackups: "Zálohy nastavení" -deck: "Deck" -undeck: "Opustit Deck" -useBlurEffectForModal: "Použít efekt rozostření na okna" -useFullReactionPicker: "Používat plnou velikost výběru emoji" width: "Šířka" height: "Výška" large: "Velké" @@ -616,13 +465,10 @@ generateAccessToken: "Vygenerovat přístupový token" permission: "Oprávnění" enableAll: "Povolit vše" disableAll: "Vypnout vše" -tokenRequested: "Povolit přístup k účtu" -pluginTokenRequestedDescription: "Tenhle plugin bude moct používat oprávnění nastavená zde." notificationType: "Typy oznámení" edit: "Upravit" emailServer: "Mailový server" enableEmail: "Zapnout email dystribuci" -emailConfigInfo: "Používá se na ověření emailové adresy během registrace nebo při zapomenutí hesla." email: "Email" emailAddress: "Emailová adresa" smtpConfig: "Konfigurace SMTP serveru" @@ -630,15 +476,8 @@ smtpHost: "Hostitel" smtpPort: "Port" smtpUser: "Uživatelské jméno" smtpPass: "Heslo" -emptyToDisableSmtpAuth: "Zanechte uživatelské jméno a heslo prázdné pro vypnutí SMTP verifikace." -smtpSecure: "Použít implicitní SSL/TLS pro SMTP připojení" smtpSecureInfo: "Toto vypněte pokud používáte STARTTLS" testEmail: "Otestovat doručení emailů" -wordMute: "Ztlumené slova" -regexpError: "Chyba v regulérním výrazu" -regexpErrorDescription: "Došlo k chybě v regulérním výrazu v řádku {line} tabulky {tab} ztlumených slov:" -instanceMute: "Ztlumené instance" -userSaysSomething: "{name} řekl/a něco" makeActive: "Aktivovat" display: "Zobrazit" copy: "Kopírovat" @@ -650,103 +489,42 @@ database: "Databáze" channel: "Kanály" create: "Vytvořit" notificationSetting: "Nastavení oznámení" -notificationSettingDesc: "Vyberte typy oznámení k zobrazení." useGlobalSetting: "Použít globální nastavení" -useGlobalSettingDesc: "Pokud je to zapnuté, tak nastavení oznámení účtu bude použito. Pokud je to vypnuté, tak se bude moct použít jednotlivá nastavení." other: "Ostatní" -regenerateLoginToken: "Přegenerovat přihlašovací token" -regenerateLoginTokenDescription: "Přegeneruje token interně používaný během přihlášení. Běžně tahle akce není nutná. Pokud bude token přegenerovaný, tak se všechna přihlášená zařízení odhlásí." -setMultipleBySeparatingWithSpace: "Oddělení více položek mezerami." fileIdOrUrl: "ID nebo URL souboru" behavior: "Chování" sample: "Ukázka" -abuseReports: "Nahlášení" -reportAbuse: "Nahlášení" -reportAbuseOf: "Nahlásit {name}" -fillAbuseReportDescription: "Prosíme vyplňte všechny detaily ohledně tohodle nahlášení. Pokud jde o specifickou poznámku, prosíme o přiložení její URL." -abuseReported: "Nahlášení bylo odesláno. Děkujeme převelice." -reporter: "Nahlásil" -reporteeOrigin: "Původ nahlášení" -reporterOrigin: "Původ nahlasovače" send: "Odeslat" openInNewTab: "Otevřít v nové kartě" -openInSideView: "Otevřít v bočním panelu" -defaultNavigationBehaviour: "Výchozí chování navigace" -editTheseSettingsMayBreakAccount: "Uprávou těchto nastavení si můžete poškodit účet." -instanceTicker: "Informace instance o poznámkách" -waitingFor: "Čeká se na {x}" random: "Náhodně" system: "Systém" -switchUi: "Přepnout UI" desktop: "Plocha" clip: "Oříznout" createNew: "Vytvořit nový" optional: "Volitelné" -createNewClip: "Vytvořit nový klip" -unclip: "Odepnout" -confirmToUnclipAlreadyClippedNote: "Tahle poznámku je už součásti \"{name}\" klipu. Chcete ji místo toho odepnout z tohodle klipu?" -public: "Veřejný" -private: "Soukromý" -i18nInfo: "Misskey je překládán do jiných jazyků dobrovolníkama. Můžete pomoci na {link}." -manageAccessTokens: "Spravovat přístupové tokeny" -accountInfo: "Informace o účtu" -notesCount: "Počet poznámek" -repliesCount: "Počet odeslaných odpovědí" -renotesCount: "Počet přeposlaných poznámek" -repliedCount: "Počet přijatých odpovědí" -renotedCount: "Počet přijatých přeposlaných poznámek" -followingCount: "Počet sledovaných účtů" -followersCount: "Počet sledujících" -sentReactionsCount: "Počet odeslaných reakcí" -receivedReactionsCount: "Počet přijatých reakcí" -pollVotesCount: "Počet odeslaných anketových hlasů" -pollVotedCount: "Počet přijatých anketových hlasů" yes: "Ano" no: "Ne" -driveFilesCount: "Počet souborů na disku" -driveUsage: "Využití disku" -noCrawle: "Odmítat indexování crawleru" -noCrawleDescription: "Požádat vyhledávače aby neindexovali váš profil, poznámky, stránky, atd." -lockedAccountInfo: "Pokud nenastavíte viditelnost poznámek na \"Pouze pro sledující\", budou poznámky viditelné všem i přesto že vyžadujete manuální potvrzení pro sledování." -alwaysMarkSensitive: "Výchozně označovat jako citlivý" -loadRawImages: "Načítat originální obrázky místo náhledů" -disableShowingAnimatedImages: "Nepřehrávat animované obrázky" -verificationEmailSent: "Ověřovací email byl zaslán. Ověření dokončíte kliknutím na odkaz v emailu." notSet: "Není nastaveno" emailVerified: "Váš e-mail byl ověřen" -noteFavoritesCount: "Počet oblíbených poznámek" -pageLikesCount: "Počet oblíbených stránek" -pageLikedCount: "Počet přijatých \"Libí se mi\"" contact: "Kontakt" useSystemFont: "Použít výchozí font systému" clips: "Oříznout" experimentalFeatures: "Experimentální funkce" -experimental: "Experimentální" -thisIsExperimentalFeature: "Tohle je experimentální funkce. Její funkce se může změnit a nemusí fungovat tak, jak bylo zamýšleno." developer: "Vývojář" -makeExplorable: "Udělat účet viditelný v \"Objevit\"" -makeExplorableDescription: "Pokud tohle vypnete, tak se účet přestane zobrazovat v sekci \"Objevit\"." duplicate: "Duplikovat" left: "Vlevo" center: "Uprostřed" wide: "Široké" narrow: "Úzké" -reloadToApplySetting: "Tohle nastavení se použije až po obnovení stránky. Obnovit teď?" -needReloadToApply: "K projevení nastavení je zapotřebí obnovit stránku." -showTitlebar: "Zobrazit řádek s nadpisem" clearCache: "Vyprázdnit mezipaměť" -onlineUsersCount: "{n} uživatelů je online" nUsers: "{n} užívatelů" nNotes: "{n} poznámek" -sendErrorReports: "Odesílat chybové záznamy" -sendErrorReportsDescription: "Pokud je tato funkce zapnutá, budou se při výskytu problému sdílet podrobné informace o chybách se službou Misskey, což pomůže zlepšit kvalitu služby Misskey. Tyto informace budou zahrnovat například verzi operačního systému, jaký prohlížeč používáte, vaši aktivitu v Misskey atd." myTheme: "Moje vzhledy" backgroundColor: "Pozadí" accentColor: "Akcent" textColor: "Barva textu" saveAs: "Uložit jako…" advanced: "Pokročilé" -advancedSettings: "Pokročilá nastavení" value: "Hodnota" createdAt: "Vytvořeno" updatedAt: "Upraveno" @@ -754,35 +532,7 @@ saveConfirm: "Uložit změny?" deleteConfirm: "Opravdu smazat?" invalidValue: "Neplatná hodnota." registry: "Registr" -closeAccount: "Uzavřít účet" -currentVersion: "Aktuální verze" -latestVersion: "Nejnovější verze" -youAreRunningUpToDateClient: "Používáte nejnovější verzi klienta." -newVersionOfClientAvailable: "Nová verze klienta je k dispozici." -usageAmount: "Využití" -capacity: "Kapacita" -inUse: "Používáno" -editCode: "Upravit kód" -apply: "Potvrdit" -receiveAnnouncementFromInstance: "Dostávat oznámení z téhle instance" -emailNotification: "Emailové oznámení" -publish: "Zveřejnit" -inChannelSearch: "Vyhledat v kanálech" -useReactionPickerForContextMenu: "Otevřít výběr reakce na kliknutí pravého tlačítka myši" -typingUsers: "{users} píše..." -jumpToSpecifiedDate: "Skočit do konkrétního datumu" -showingPastTimeline: "Právě je zobrazována stará časová osa" -clear: "Vrátit" -markAllAsRead: "Označit všechno jako přečtené" -goBack: "Zpět" -unlikeConfirm: "Opravdu chcete odstranit like?" -fullView: "Plné zobrazení" -quitFullView: "Odejít z plného zobrazení" -addDescription: "Přidat popis" -userPagePinTip: "Zde můžete zobrazovat poznámky vybráním \"Připnout na profil\" z menu jednotlivých poznámek." -notSpecifiedMentionWarning: "Tahle poznámka zmiňuje uživatele, které nejsou mezi adresáty" info: "Informace" -userInfo: "Informace o uživateli" unknown: "Neznámý" onlineStatus: "Online status" hideOnlineStatus: "Skrýt Váš online status" @@ -802,18 +552,10 @@ user: "Uživatelé" administration: "Administrace" accounts: "Účty" switch: "Přepnout" -noMaintainerInformationWarning: "Informace o správci nejsou nastavené" -noBotProtectionWarning: "Ochrana proti botům není nastavena" configure: "Nastavit" -postToGallery: "Vytvořit nový příspěvek v galerii" -postToHashtag: "Přidat příspěvek k tomuhle hastagu" gallery: "Galerie" recentPosts: "Poslední příspěvky" -popularPosts: "Populární příspěvky" -shareWithNote: "Sdílet s poznámkou" ads: "Reklamy" -expiration: "Ukončit hlasování" -startingperiod: "Začátek" memo: "Memo" priority: "Priorita" high: "Vysoká" @@ -821,716 +563,63 @@ middle: "Střední" low: "Nízká" emailNotConfiguredWarning: "E-mailová adresa není nastavena." ratio: "Poměr" -previewNoteText: "Zobrazit náhled" -customCss: "Vlastní CSS" -customCssWarn: "Tohle nastavení by mělo být použito pouze v případě pokud víte co děláte. Vložením nesprávných hodnot může způsobit nefunkčnost klienta." global: "Globální" -squareAvatars: "Zobrazovat čtvercové avatary" sent: "Odeslat" -received: "Přijaté" -searchResult: "Výsledky hledání" hashtags: "Hashtagy" troubleshooting: "Poradce při potížích" -useBlurEffect: "Použít efekt rozostření v UI" -learnMore: "Zjistit více" -misskeyUpdated: "Misskey byl aktualizován!" whatIsNew: "Zobrazit změny" translate: "Přeložit" -translatedFrom: "Přeloženo z {x}" -accountDeletionInProgress: "Smazání účtu právě probíhá" -usernameInfo: "Jméno které identifikuje váš účet od jiných na tomhle serveru. Můžete použít abecedu (a~z, A~Z), čísla (0~9) nebo podtržítka (_). Uživatelské jména nemůžou být změněna později." -aiChanMode: "Režim Ai" -devMode: "Vývojářský režim" -keepCw: "Zachovat varování o obsahu" -pubSub: "Pub/Sub účty" -lastCommunication: "Poslední komunikace" -resolved: "Vyřešeno" -unresolved: "Nevyřešené" -breakFollow: "Odstranit sledujícího" -breakFollowConfirm: "Opravdu chcete odstranit tohodle sledujícího?" -itsOn: "Zapnuto" -itsOff: "Vypnuto" -on: "Zapnuto" -off: "Vypnuto" -emailRequiredForSignup: "Vyžadovat email pro registraci" -unread: "Nepřečtený" -filter: "Filtr" -controlPanel: "Ovládací panel" -manageAccounts: "Spravovat účty" -makeReactionsPublic: "Nastavit historii reakcí jako veřejnou" -makeReactionsPublicDescription: "Tohle zviditelný seznam vašich předchozích reakcí veřejně." -classic: "Klasický" -muteThread: "Ztlumit vlákno" -unmuteThread: "Zrušit ztlumení vlákna" -continueThread: "Zobrazit pokračování vlákna" -deleteAccountConfirm: "Tohle nenávratně smaže váš účet, chcete pokračovat?" -incorrectPassword: "Nesprávné heslo." -voteConfirm: "Potvrdit hlas pro \"{choice}\"?" hide: "Skrýt" -useDrawerReactionPickerForMobile: "Zobrazit výběr reakcí jako šuplík na mobilním zařízení" -welcomeBackWithName: "Vítejte zpět, {name}" -clickToFinishEmailVerification: "Prosíme klikněte na [{ok}] pro dokončení ověření emailu." -overridedDeviceKind: "Typ zařízení" smartphone: "Telefon" tablet: "Tablet" auto: "Auto" -themeColor: "Barva motivu" size: "Velikost" numberOfColumn: "Počet sloupců" searchByGoogle: "Vyhledávání" -instanceDefaultLightTheme: "Výchozí světlý motiv instance" -instanceDefaultDarkTheme: "Výhozí tmavý motiv instance" -instanceDefaultThemeDescription: "Zadejte kód motivu v objektovém formátu" -mutePeriod: "Délka ztlumení" -period: "Časový limit" indefinitely: "Navždy" tenMinutes: "10 minut" oneHour: "1 hodina" oneDay: "1 den" oneWeek: "1 týden" -oneMonth: "1 měsíc" reflectMayTakeTime: "Může trvat nějakou dobu, než se projeví změny." -failedToFetchAccountInformation: "Nepodařily se načíst informace o účtě" -rateLimitExceeded: "Překročení rychlostního limitu" cropImage: "Oříznout obrázek" -cropImageAsk: "Chcete oříznout tenhle obrázek?" -cropYes: "Uříznout" -cropNo: "Použít tak jak je" file: "Soubor(ů)" recentNHours: "Posledních {n} hodin" recentNDays: "Posledních {n} dnů" -noEmailServerWarning: "Emailový server není nastavený" -thereIsUnresolvedAbuseReportWarning: "Jsou k dispozici nevyřešené nahlášení zneužití" recommended: "Doporučeno" -check: "Zkontrolovat" -driveCapOverrideLabel: "Změnit velikost disku pro tohoto uživatele" -driveCapOverrideCaption: "K vyresetování velikosti na výchozí hodnotu zadejte hodnotu 0 nebo nižší." -requireAdminForView: "Pro zobrazení se musíte přihlásit administrátorským účtem." -isSystemAccount: "Účet automaticky vytvořený a ovládaný serverem." -typeToConfirm: "Prosíme zadejte {x} pro potvrzení" deleteAccount: "Odstranit účet" document: "Dokumentace" -numberOfPageCache: "Počet stránek uložených v mezipaměti" -numberOfPageCacheDescription: "Zvýšením čísla zlepšíte pohodlí pro uživatele ale může to způsobit větší zátěž na server a na paměť." logoutConfirm: "Opravdu se chcete odhlásit?" -lastActiveDate: "Naposledy použito" -statusbar: "Stavový řádek" pleaseSelect: "Vybrat možnost" reverse: "Otočit" colored: "Barevné" -refreshInterval: "Interval obnovení" -label: "Popisek" type: "Typ" speed: "Rychlost" slow: "Pomalá" fast: "Rychlá" -sensitiveMediaDetection: "Detekce citlivého média" -localOnly: "Jenom lokální" -remoteOnly: "Jenom vzdáleně" -failedToUpload: "Nahrání se nezdařilo" -cannotUploadBecauseInappropriate: "Tenhle soubor se nenahrál, protože některé části byly detekovány jako nevhodné." -cannotUploadBecauseNoFreeSpace: "Nahrání se nezdařilo z důvodu nedostatku místa na disku." -cannotUploadBecauseExceedsFileSizeLimit: "Tenhle soubor nemůže být nahráný protože překračuje velikostní limit." -beta: "Beta verze" -enableAutoSensitive: "Automaticky označovat jako citlivé" -enableAutoSensitiveDescription: "Umožňuje automatickou detekci a označování citlivého média skrze strojového účení všude kde je možno. I pokud je tahle možnost vypnutá, může být povolena instancí." -activeEmailValidationDescription: "Umožňuje striktní validaci emailové adresy, která zahrnuje kontrolu pro jednorázové adresy a pokud je možno s ní komunikovat. Pokud je to vypnuté, bude se kontrolovat pouze formát emailu." -navbar: "Navigační panel" -shuffle: "Zamíchat" account: "Účty" -move: "Přesunout" -pushNotification: "Push oznámení" -subscribePushNotification: "Povolit push oznamení" -unsubscribePushNotification: "Vypnout push oznámení" -pushNotificationAlreadySubscribed: "Push oznámení jsou už zapnuté" -pushNotificationNotSupported: "Tenhle prohlížeč nepodporuje push oznámení" -sendPushNotificationReadMessage: "Odstraněnit oznámení push po jejich přečtení" -sendPushNotificationReadMessageCaption: "Tohle může zvýšit spotřebu energie vašeho zařízení." -windowMaximize: "Maximalizovat" -windowMinimize: "Minimalizovat" -windowRestore: "Obnovit" -caption: "Titulek" -loggedInAsBot: "Právě jste přihlášen jako bot" -tools: "Nástroje" -cannotLoad: "Načtení se nezdařilo" -numberOfProfileView: "Počet zobrazení profilu" -like: "To se mi líbí" -unlike: "Už se mi to nelíbí" -numberOfLikes: "Počet \"To se mi líbí\"" show: "Zobrazit" -neverShow: "Znovu nezobrazovat" -remindMeLater: "Možná později" -didYouLikeMisskey: "Oblíbili jste si Misskey?" -pleaseDonate: "{host} používá bezplatný software Misskey. Velmi bychom ocenili vaše dary, aby mohl vývoj Misskey pokračovat!" -roles: "Role" -role: "Role" -noRole: "Role nenalezena" -normalUser: "Normální uživatel" -undefined: "Neurčeno" -assign: "Přiřadit" -unassign: "Zrušit přirazení" color: "Barva" -manageCustomEmojis: "Spravovat vlastní emoji" -youCannotCreateAnymore: "Narazili jste na limit pro vytváření." -cannotPerformTemporary: "Dočasně nedostupné" -cannotPerformTemporaryDescription: "Tuto akci nelze dočasně provést z důvodu překročení limitu provedení. Chvíli počkejte a zkuste to znovu." -invalidParamError: "Neplatné parametry" -invalidParamErrorDescription: "Parametry požadavku jsou neplatné. Obvykle je to způsobeno chybou, ale může to být také způsobeno překročením limitů velikosti vstupů nebo podobně." -permissionDeniedError: "Operace zamítnuta" -permissionDeniedErrorDescription: "Tento účet nemá oprávnění k provedení této akce." -preset: "Předvolba" -selectFromPresets: "Vybrat z předvoleb" -achievements: "Úspěchy" -gotInvalidResponseError: "Neplatná odpověď serveru" -gotInvalidResponseErrorDescription: "Server může být nedostupný nebo na něm probíhá údržba. Zkuste to prosím později." -thisPostMayBeAnnoying: "Tato poznámka může ostatní obtěžovat." -thisPostMayBeAnnoyingHome: "Zveřejnit na domovskou časovou osu" -thisPostMayBeAnnoyingCancel: "Zrušit" -thisPostMayBeAnnoyingIgnore: "I přesto zveřejnit" -collapseRenotes: "Sbalit poznámky, které jste již viděli" -internalServerError: "Interní chyba serveru" -internalServerErrorDescription: "Server narazil na neočekávanou chybu." -copyErrorInfo: "Zkopírovat detaily erroru" -joinThisServer: "Zaregistrovat se v této instanci" -exploreOtherServers: "Podívat se na ostatní instance" -letsLookAtTimeline: "Podívejte se na časovou osu" -disableFederationConfirm: "Chcete opravdu vypnout federace?" -disableFederationConfirmWarn: "I v případě defederace budou příspěvky nadále veřejné, pokud nebude nastaveno jinak. Obvykle to není nutné." -disableFederationOk: "Vypnout" -invitationRequiredToRegister: "Tahle instance je pouze na pozvánku. Musíte zadat validní kód pozvánky." -emailNotSupported: "Tahle instance nepodporuje zasílání emailů" -postToTheChannel: "Vložit do kanálu" -cannotBeChangedLater: "Tohle nemůže být změněno později." -reactionAcceptance: "Přijímání reakcí" -likeOnly: "Jenom \"oblíbené\"" -likeOnlyForRemote: "Všechny (Pouze \"oblíbené\" pro vzdálenou instanci)" -nonSensitiveOnly: "Pouze bez citlivých medií" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Pouze bez citlivých medií (Pouze vzdálený \"oblíbený\")" -rolesAssignedToMe: "Přiřazené role ke mně" -resetPasswordConfirm: "Opravdu chcete resetovat heslo?" -sensitiveWords: "Citlivá slova" -sensitiveWordsDescription: "Viditelnost všech poznámek obsahujících některé z nakonfigurovaných slov bude automaticky nastavena na \"Domů\". Můžete jich uvést více tak, že je oddělíte pomocí řádků." -sensitiveWordsDescription2: "Použití mezer vytvoří výrazy AND a obklopení klíčových slov lomítky je změní na regulární výraz." -prohibitedWordsDescription2: "Použití mezer vytvoří výrazy AND a obklopení klíčových slov lomítky je změní na regulární výraz." -notesSearchNotAvailable: "Vyhledávání poznámek je nedostupné." -license: "Licence" -unfavoriteConfirm: "Opravdu chcete odstranit z oblíbených?" -myClips: "Moje klipy" -drivecleaner: "Čistič disku" -retryAllQueuesNow: "Obnovit všechny běžící fronty" -retryAllQueuesConfirmTitle: "Opravdu chcete obnovit všechno?" -retryAllQueuesConfirmText: "Tohle dočasně zvýší zatěž na server." -enableChartsForRemoteUser: "Vygenerovat grafy dat vzdálených uživatelů" -enableChartsForFederatedInstances: "Vygenerovat grafy dat vzdálených instancí" -showClipButtonInNoteFooter: "Přidat \"Připnout\" do akčního menu poznámky" -noteIdOrUrl: "ID nebo URL poznámky" -video: "Video" -videos: "Videa" -dataSaver: "Spořič dat" -accountMigration: "Migrace účtu" -accountMoved: "Tenhle uživatel se přesunul na nový účet:" -accountMovedShort: "Tenhle účet byl migrován." -operationForbidden: "Zakázaná operace" -forceShowAds: "Vždycky zobrazovat reklamy" -addMemo: "Přidat memo" -editMemo: "Upravit memo" -reactionsList: "Reakce" -renotesList: "Poznámky" -notificationDisplay: "Oznámení" -leftTop: "Vlevo nahoře" -rightTop: "Vpravo nahoře" -leftBottom: "Vlevo dole" -rightBottom: "Vpravo dole" -stackAxis: "Směr ukládání" -vertical: "Svisle" -horizontal: "Vodorovně" -position: "Pozice" -serverRules: "Pravidla serveru" -pleaseConfirmBelowBeforeSignup: "Abyste se mohli přihlásit na server, musíte souhlasit s následujícím." -pleaseAgreeAllToContinue: "Musíte souhlasit se vším abyste mohli pokračovat." -continue: "Pokračovat" -preservedUsernames: "Rezervované uživatelské jména" -preservedUsernamesDescription: "Seznam uživatelských jmén na rezervaci oddělené mezerama. Tyhle jména se potom nebudou moc použít při normálním procesu vytvoření účtu ale můžou být použiti manuálně administratorém. Existujících účtů se to nedotkne." -createNoteFromTheFile: "Vytvořit poznámku z tohodle souboru" -archive: "Archiv" -channelArchiveConfirmTitle: "Opravdu chcete archivovat {name}?" -channelArchiveConfirmDescription: "Archivovaný kanál se objeví v seznamu kanálů nebo ve výsledcích hledání. Nové poznámky se nedají vložit do seznamu." -thisChannelArchived: "Tenhle kanál je archivovaný" -displayOfNote: "Zobrazit poznámku" -initialAccountSetting: "Nastavení profilu" -youFollowing: "Sleduji" -preventAiLearning: "Odmítnout použití v strojovém učení (Generative AI)" -preventAiLearningDescription: "Požaduje, aby prohlížeče nepoužívaly zveřejněný textový nebo obrazový materiál atd. v datových sadách pro strojové učení (prediktivní / generativní umělá inteligence). Toho se dosáhne přidáním příznaku \"noai\" HTML-Response k příslušnému obsahu. Úplné prevence však tímto příznakem nelze dosáhnout, protože může být jednoduše ignorován." -options: "Možnosti" -specifyUser: "Upřesnit uživatele" -failedToPreviewUrl: "Náhled se nezdařil" -update: "Aktualizovat" -rolesThatCanBeUsedThisEmojiAsReaction: "Role, které můžou tuhle emoji použít jako reakci" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Pokud nejsou určena role, tak pak každý může použít tenhle emoji." -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Role musí být veřejné." -cancelReactionConfirm: "Opravdu chcete odstranit vaší reakci?" -changeReactionConfirm: "Opravdu chcete změnit vaši reakci?" -later: "Později" -goToMisskey: "Jít na Misskey" -additionalEmojiDictionary: "Další slovníky emoji" -installed: "Nainstalováno" -branding: "Značka" -enableServerMachineStats: "Zveřejněnit statistiky hardwaru serveru" -enableIdenticonGeneration: "Povolit generování identicon uživatele" -turnOffToImprovePerformance: "Vypnutí této funkce může zvýšit výkon." -createInviteCode: "Vygenerovat pozvánku" -createWithOptions: "Vygenerovat s nastavením" -createCount: "Počet vytvořených pozvánek" -inviteCodeCreated: "Pozvánka vygenerována" -inviteLimitExceeded: "Překročili jste limit pozvánek, které můžete vygenerovat." -createLimitRemaining: "Limit pozvánek: {limit} zbývá" -inviteLimitResetCycle: "Tento limit se obnoví na hodnotu {limit} v {time}." -expirationDate: "Datum expirace" -noExpirationDate: "Bez expirace" -inviteCodeUsedAt: "Kód pozvánky použitý na" -registeredUserUsingInviteCode: "Pozvánku používá" -waitingForMailAuth: "Čeká se na ověření emailu" -inviteCodeCreator: "Pozvánku vytvořil" -usedAt: "Používá se v" -unused: "Nepoužívaná" -used: "Používaná" -expired: "Prošlá" -doYouAgree: "Souhlasíte?" -beSureToReadThisAsItIsImportant: "Přečtěte si prosím tyto důležité informace." -iHaveReadXCarefullyAndAgree: "Přečetl jsem si text \"{x}\" a souhlasím s ním." -icon: "Avatar" -replies: "Odpovědět" -renotes: "Přeposlat" -sourceCode: "Zdrojový kód" -flip: "Otočit" -lastNDays: "Posledních {n} dnů" -surrender: "Zrušit" -postForm: "Formulář pro odeslání" -information: "Informace" -_chat: - invitations: "Pozvat" - noHistory: "Žádná historie" - members: "Členové" - home: "Domů" - send: "Odeslat" -_delivery: - stop: "Suspendováno" - _type: - none: "Publikuji" -_initialAccountSetting: - accountCreated: "Váš účet byl úspěšně vytvořen!" - letsStartAccountSetup: "Pro začátek si nastavte svůj profil." - letsFillYourProfile: "Nejprve si nastavte svůj profil." - profileSetting: "Nastavení profilu" - privacySetting: "Nastavení soukromí" - theseSettingsCanEditLater: "Tato nastavení můžete vždy později změnit." - youCanEditMoreSettingsInSettingsPageLater: "Na stránce \"Nastavení\" můžete nakonfigurovat mnoho dalších nastavení. Nezapomeňte ji navštívit později." - followUsers: "Zkuste sledovat některé uživatele, kteří vás zajímají pro vystavění časový osy." - pushNotificationDescription: "Povolení push oznámení vám umožní přijímat oznámení od {name} přímo ve vašem zařízení." - initialAccountSettingCompleted: "Nastavení profilu dokončeno!" - haveFun: "Užívejte {name}!" - skipAreYouSure: "Opravdu chcete přeskočit nastavení profilu?" - laterAreYouSure: "Opravdu chcete provést nastavení profilu později?" -_serverRules: - description: "Soubor pravidel, která se zobrazí před registrací. Doporučuje se nastavit shrnutí podmínek služby." -_serverSettings: - iconUrl: "URL ikony" -_accountMigration: - moveFrom: "Migrace jiného účtu na tento účet" - moveFromSub: "Vytvořit alias na jiný účet" - moveFromLabel: "Původní účet #{n}" - moveFromDescription: "Pro účet, ze kterého se chcete přesunout, musíte vytvořit alias na tomto účtu.\nZadejte účet, ze kterého chcete přejít, v následujícím formátu: @username@server.example.com\nChcete-li alias odstranit, ponechte pole prázdné (nedoporučuje se)." - moveTo: "Přesunout tenhle účet do jiného" - moveToLabel: "Cílový účet pro přesunutí:" - moveCannotBeUndone: "Migrace účtu nemůže být vrácena." - moveAccountDescription: "Tím dojde k migraci vašeho účtu na jiný účet.\n ・Sledovatelé z tohoto účtu budou automaticky převedeni na nový účet.\n ・Tento účet zruší sledování všech uživatelů, které aktuálně sleduje.\n ・Na tomto účtu nebude možné vytvářet nové poznámky atd.\n\nZatímco migrace sledovaných uživatelů probíhá automaticky, pro migraci seznamu sledovaných uživatelů je nutné připravit některé kroky ručně. Za tímto účelem proveďte export sledovaných, který později naimportujete na nový účet v nabídce nastavení. Stejný postup platí pro seznamy i pro ztlumené a zablokované uživatele.\n\n(Tento výklad platí pro Misskey v13.12.0 a novější. Jiný software ActivityPub, například Mastodon, může fungovat jinak.)" - moveAccountHowTo: "Chcete-li migrovat, vytvořte nejprve alias tohoto účtu na účtu, na který chcete přejít.\nPo vytvoření aliasu zadejte účet, na který chcete přejít, v následujícím formátu: @username@server.example.com" - startMigration: "Migrovat" - migrationConfirm: "Opravdu chcete migrovat tento účet na {account}? Jednou zahájený proces nelze zastavit ani vrátit zpět a tento účet již nebudete moci používat v původním stavu." - movedAndCannotBeUndone: "\nTento účet byl převeden.\nMigraci nelze vrátit zpět." - postMigrationNote: "Tento účet zruší sledování všech účtů, které aktuálně sleduje, 24 hodin po dokončení migrace.\nPočet sledujících i následovníků se poté vynuluje. Aby se zabránilo tomu, že vaši sledující nebudou moci vidět příspěvky tohoto účtu určené pouze pro sledující, budou však tento účet sledovat i nadále." - movedTo: "Cílový účet pro přesunutí:" -_achievements: - earnedAt: "Odemčeno v" - _types: - _notes1: - title: "Dobrý den Misskey!" - description: "Zveřejněte vaší první poznámku" - flavor: "Užijte si to s Misskey!" - _notes10: - title: "Pár poznámek" - description: "Zveřejněte 10 poznámek" - _notes100: - title: "Hodně poznámek" - description: "Zveřejněte 100 poznámek" - _notes500: - title: "Zahlcen poznámkama" - description: "Zveřejněte 500 poznámek" - _notes1000: - title: "Hora poznámek" - description: "Zveřejněte 1000 poznámek" - _notes5000: - title: "Přetékající poznámky" - description: "Zveřejněte 5000 poznámek" - _notes10000: - title: "Super poznámka" - description: "Zveřejněte 10 000 poznámek" - _notes20000: - title: "Potřebuju... více... poznámek..." - description: "Zveřejněte 20 000 poznámek" - _notes30000: - title: "Poznámky, poznámky, POZNÁMKY!" - description: "Zveřejněte 30 000 poznámek" - _notes40000: - title: "Továrna na poznámky" - description: "Zveřejněte 40 000 poznámek" - _notes50000: - title: "Planeta poznámek" - description: "Zveřejněte 50 000 poznámek" - _notes60000: - title: "Poznámkový kvasar" - description: "Zveřejněte 60 000 poznámek" - _notes70000: - title: "Černá díra poznámek" - description: "Zveřejněte 70 000 poznámek" - _notes80000: - title: "Galaxie poznámek" - description: "Zveřejněte 80 000 poznámek" - _notes90000: - title: "Vesmír poznámek" - description: "Zveřejněte 90 000 poznámek" - _notes100000: - title: "ALL YOUR NOTE ARE BELONG TO US" - description: "Zveřejněte 100 000 poznámek" - flavor: "Máte toho hodně co říct." - _login3: - title: "Začátečník I" - description: "Přihlaste se celkově za 3 dny" - flavor: "Ode dneška mi říkejte Misskista." - _login7: - title: "Začátečník II" - description: "Přihlaste se celkově za 7 dní" - flavor: "Máte pocit, že už jste se v tom vyznali?" - _login15: - title: "Začátečník III" - description: "Přihlaste se celkově za 15 dní" - _login30: - title: "Misskista I" - description: "Přihlaste se celkově za 30 dní" - _login60: - title: "Misskista II" - description: "Přihlaste se celkově za 60 dní" - _login100: - title: "Misskista III" - description: "Přihlaste se celkově za 100 dní" - flavor: "Violent Misskista" - _login200: - title: "Stálý zákazník I" - description: "Přihlaste se celkově za 200 dní" - _login300: - title: "Stálý zákazník II" - description: "Přihlaste se celkově za 300 dní" - _login400: - title: "Stálý zákazník III" - description: "Přihlaste se celkově za 400 dní" - _login500: - title: "Expert I" - description: "Přihlaste se celkově za 500 dní" - flavor: "Moji přátelé, často se říká, že mám rád poznámky." - _login600: - title: "Expert II" - description: "Přihlaste se celkově za 600 dní" - _login700: - title: "Expert III" - description: "Přihlaste se celkově za 700 dní" - _login800: - title: "Mistr poznámek I" - description: "Přihlaste se celkově za 800 dní" - _login900: - title: "Mistr poznámek II" - description: "Přihlaste se celkově za 900 dní" - _login1000: - title: "Mistr poznámek III" - description: "Přihlaste se celkově za 1000 dní" - flavor: "Děkujeme, že používáte Misskey!" - _noteClipped1: - title: "Musím... připnout..." - description: "Připněte si první poznámku" - _noteFavorited1: - title: "Hvězdář" - description: "Oblíbena první poznámka" - _myNoteFavorited1: - title: "Hledání hvězd" - description: "Někdo si oblíbil jednu z vašich poznámek" - _profileFilled: - title: "Dobře připravený" - description: "Nastavte si profil" - _markedAsCat: - title: "Já jsem kočka" - description: "Označte váš účet \"jako kočka\"" - flavor: "Jméno ti dám později." - _following1: - title: "Sledujte prvního uživatele" - description: "Sledujte uživatele" - _following10: - title: "Drž se... drž se..." - description: "Sledujte 10 uživatelů" - _following50: - title: "Hodně přátel" - description: "Sledujte 50 uživatelů" - _following100: - title: "100 přátel" - description: "Sledujte 100 uživatelů" - _following300: - title: "Přetížení přátel" - description: "Sledujte 300 účtů" - _followers1: - title: "První sledující" - description: "Získejte 1 sledujícího" - _followers10: - title: "Sledujte mě!" - description: "Získejte 10 sledujících" - _followers50: - title: "Přicházejí davy" - description: "Získejte 50 sledujících" - _followers100: - title: "Populární" - description: "Získejte 100 sledujících" - _followers300: - title: "Prosíme srovnejte se do jedné řady!" - description: "Získejte 300 sledujících" - _followers500: - title: "Rádiová věž" - description: "Získejte 500 sledujících" - _followers1000: - title: "Influencer" - description: "Získejte 1000 sledujících" - _collectAchievements30: - title: "Sběratel úspěchů" - description: "Získejte 30 úspěchů" - _viewAchievements3min: - title: "Máš rád úspěchy" - description: "Koukejte na váš seznam úspěchů alespoň po dobu 3 minut" - _iLoveMisskey: - title: "Miluju Misskey" - description: "Zveřejněte \" I ❤ #Misskey\"" - flavor: "Vývojový tým Misskey si velmi váží vaší podpory!" - _foundTreasure: - title: "Hon za pokladem" - description: "Našli jste schovaný poklad!" - _client30min: - title: "Krátká pauza" - description: "Mějte otevřený Misskey alespoň po dobu 30 minut" - _client60min: - title: "Žádný \"Miss\" v Misskey" - description: "Mějte otevřený Misskey alespoň po dobu 60 minut" - _noteDeletedWithin1min: - title: "Ups, nevadí" - description: "Vymažte poznámku během minuty co ji zveřejníte" - _postedAtLateNight: - title: "Noční typ" - description: "Zveřejněte poznámku pozdě v noci" - flavor: "Je nejvyšší čas jít spát." - _postedAt0min0sec: - title: "Mluvící hodiny" - description: "Zveřejněte poznámku přesně v 00:00" - flavor: "Klik Klik Klik Bum" - _selfQuote: - title: "Sebereference" - description: "Citujte vlastní poznámku" - _htl20npm: - title: "Plynoucí časová osa" - description: "Mějte rychlost vaší domovské časové osy vyšší než 20 pzm (poznámek za minutu)." - _viewInstanceChart: - title: "Analytik" - description: "Zobrazte graf instance" - _outputHelloWorldOnScratchpad: - title: "Hello, world!" - description: "Dostaňte výpis \"hello world\" do Scratchpadu" - _open3windows: - title: "Splitscreen" - description: "Mějte otevřená alespoň 3 okna zároveň" - _driveFolderCircularReference: - title: "Okružní reference" - description: "Pokuste se o vytvoření rekurzivně vnořené složky v disku" - _reactWithoutRead: - title: "Opravdu jste to četl/a?" - description: "Reagujte na poznámku, která má více než 100 znaků, do 3 sekund od jejího zveřejnění." - _clickedClickHere: - title: "Klikněte sem" - description: "Kliknul si tam" - _justPlainLucky: - title: "Čisté štěstí" - description: "Mějte šanci na získání s pravděpodobností 0,005 % každých 10 sekund." - _setNameToSyuilo: - title: "Boží komplex" - description: "Nastavte si jméno na \"syuilo\"" - _passedSinceAccountCreated1: - title: "Roční výročí" - description: "Od vytvoření vašeho účtu uplynul jeden rok" - _passedSinceAccountCreated2: - title: "Dvouleté výročí" - description: "Od vytvoření vašeho účtu uplynuly dva roky" - _passedSinceAccountCreated3: - title: "Tříleté výročí" - description: "Od vytvoření vašeho účtu uplynuly tři roky" - _loggedInOnBirthday: - title: "Všechno nejlepší!" - description: "Přihlašte se v den vašich narozenin" - _loggedInOnNewYearsDay: - title: "Štastný nový rok!" - description: "Přihlašte se v den nového roku" - flavor: "Na další skvělý rok v této instanci" - _cookieClicked: - title: "Hra, ve které klikáte na sušenky" - description: "Klikněte na soubor cookie" - flavor: "Počkejte, jste na správné webové stránce?" - _brainDiver: - title: "Brain Diver" - description: "Zveřejněte odkaz na Brain Diver" - flavor: "Misskey-Misskey La-Tu-Ma" _role: - new: "Nová role" - edit: "Upravit roli" - name: "Název role" - description: "Popis role" - permission: "Oprávnění role" - descriptionOfPermission: "Moderators může provádět základní operace moderování.\nAdministrators může měnit všechna nastavení instance." - assignTarget: "Přiřadit" - descriptionOfAssignTarget: "Manual ručně změnit, kdo je součástí této role a kdo ne.\nConditional mít uživatelé automaticky přiřazováni a odebíráni z této role na základě podmínky." - manual: "Dokumentace" - conditional: "Podmíněné" - condition: "Podmínky" - isConditionalRole: "Tato role je podmíněná." - isPublic: "Veřejná role" - descriptionOfIsPublic: "Tato role se zobrazí v profilech přiřazených uživatelů." - options: "Nastavení" - policies: "Zásady" - baseRole: "Šablona role" - useBaseValue: "Použít hodnotu šablony role" - chooseRoleToAssign: "Vyberte roli, kterou chcete přiřadit" - iconUrl: "URL ikony" - asBadge: "Zobrazovat jako odznak" - descriptionOfAsBadge: "Ikona této role se zobrazí vedle uživatelského jména uživatelů s touto rolí, pokud je zapnuta." - isExplorable: "Udělat roli objevitelnou" - descriptionOfIsExplorable: "Časová osa této role a seznam uživatelů s touto rolí budou zveřejněny, pokud jsou povoleny." - displayOrder: "Pozice" - descriptionOfDisplayOrder: "Čím vyšší číslo, tím vyšší pozice v uživatelském rozhraní." - canEditMembersByModerator: "Umožnit moderátorům upravovat seznam členů pro tuto roli" - descriptionOfCanEditMembersByModerator: "Po zapnutí této role budou moci moderátoři i administrátoři přiřazovat a odebírat uživatele do této role. Pokud je tato funkce vypnutá, budou moci uživatele přiřazovat pouze správci." priority: "Priorita" _priority: low: "Nízká" middle: "Střední" high: "Vysoká" - _options: - gtlAvailable: "Může zobrazit globální časovou osu" - ltlAvailable: "Může zobrazit místní časovou osu" - canPublicNote: "Může posílat veřejné poznámky" - canInvite: "Může vytvářet kódy pozvánek instance" - inviteLimit: "Limit pozvánek" - inviteLimitCycle: "Limit mezi pozvánkama" - inviteExpirationTime: "Interval vypršení platnosti pozvánky" - canManageCustomEmojis: "Spravovat vlastní emoji" - driveCapacity: "Velikost disku" - alwaysMarkNsfw: "Vždy označovat soubory jako NSFW" - pinMax: "Maximální počet připnutých poznámek" - antennaMax: "Maximální počet antén" - wordMuteMax: "Maximální počet znaků povolených v ztlumených slovech" - webhookMax: "Maximální počet Webhooků" - clipMax: "Maximální počet připnutí" - noteEachClipsMax: "Maximální počet poznámek v připnutí" - userListMax: "Maximální počet seznamů uživatelů" - userEachUserListsMax: "Maximální počet uživatelů v seznamu uživatelů" - rateLimitFactor: "Limit rychlosti" - descriptionOfRateLimitFactor: "Nižší limity rychlosti jsou méně omezující, vyšší více omezující. " - canHideAds: "Může schovat reklamy" - canSearchNotes: "Použití vyhledávání poznámek" - _condition: - isLocal: "Místní uživatel" - isRemote: "Vzdálený uživatel" - createdLessThan: "Od vytvoření účtu uplynulo méně než X" - createdMoreThan: "Od vytvoření účtu uplynulo více než X" - followersLessThanOrEq: "Má X nebo méně sledujících" - followersMoreThanOrEq: "Má X nebo více sledujících" - followingLessThanOrEq: "Sleduje X nebo méně účtů" - followingMoreThanOrEq: "Sleduje X nebo více účtů" - notesLessThanOrEq: "Počet příspěvků je menší než/rovná se" - notesMoreThanOrEq: "Počet příspěvků je větší než/rovná se" - and: "AND kondice" - or: "OR kondice" - not: "NOT kondice" -_sensitiveMediaDetection: - description: "Snižuje náročnost moderování serveru díky automatickému rozpoznávání citlivých médií pomocí strojového učení. Tím se mírně zvýší zatížení serveru." - sensitivity: "Detekce citlivosti" - sensitivityDescription: "Snížení citlivosti povede k menšímu počtu chybných detekcí (falešně pozitivních), zatímco její zvýšení povede k menšímu počtu chybných detekcí (falešně negativních)." - setSensitiveFlagAutomatically: "Označit jako citlivé" - setSensitiveFlagAutomaticallyDescription: "Výsledky interní detekce se zachovají, i když je tato možnost vypnutá." - analyzeVideos: "Povolit analýzy videí" - analyzeVideosDescription: "Kromě obrázků analyzuje i videa. Tím se mírně zvýší zatížení serveru." -_emailUnavailable: - used: "Tato emailová adresa se již používá" - format: "Formát této emailové adresy je neplatný" - disposable: "Jednorázové emailové adresy se nesmí používat" - mx: "Tento e-mailový server je neplatný" - smtp: "Tento emailový server neodpovídá" -_ffVisibility: - public: "Zveřejnit" - followers: "Viditelné pouze pro sledující" - private: "Soukromý" -_signup: - almostThere: "Už to skoro je" - emailAddressInfo: "Zadejte prosím svou emailovou adresu. Nebude zveřejněna." - emailSent: "Na vaši e-mailovou adresu ({email}) byl odeslán potvrzovací e-mail. Kliknutím na přiložený odkaz dokončete vytvoření účtu." -_accountDelete: - accountDelete: "Smazat účet" - mayTakeTime: "Vzhledem k tomu, že odstranění účtu je proces náročný na zdroje, může jeho dokončení trvat určitou dobu v závislosti na tom, kolik obsahu jste vytvořili a kolik souborů jste nahráli." - sendEmail: "Po dokončení odstranění účtu bude na emailovou adresu registrovanou k tomuto účtu zaslán email." - requestAccountDelete: "Žádost o smazání účtu" - started: "Bylo zahájeno mazání." - inProgress: "V současné době probíhá mazání" _ad: back: "Zpět" - reduceFrequencyOfThisAd: "Zobrazovat tuto reklamu méně" - hide: "Schovat" - timezoneinfo: "Den v týdnu se určuje podle časového pásma serveru." -_forgotPassword: - enterEmail: "Zadejte emailovou adresu, kterou jste použili při registraci. Na ni vám pak bude zaslán odkaz, pomocí kterého si můžete obnovit heslo." - ifNoEmail: "Pokud jste při registraci nepoužili email, obraťte se na správce instance." - contactAdmin: "Tato instance nepodporuje používání emailových adres, pro obnovení hesla se obraťte na správce instance." _gallery: my: "Moje galerie" - liked: "Oblíbené příspěvky" - like: "To se mi líbí" - unlike: "Už se mi to nelíbí" _email: _follow: title: "Máte nového následovníka" - _receiveFollowRequest: - title: "Obdrželi jste žádost o sledování" _plugin: install: "Instalovat plugin" - installWarn: "Neinstalujte nedůvěryhodné pluginy." manage: "Správce pluginů" - viewSource: "Zobrazit zdroj" _preferencesBackups: list: "Vytvořit backup" - saveNew: "Uložit novou zálohu" loadFile: "Načíst ze souboru" - apply: "Použít pro toto zařízení" save: "Uložit změny" - inputName: "Zadejte prosím název pro tuto zálohu" - cannotSave: "Uložení selhalo" - nameAlreadyExists: "Záloha s názvem \"{name}\" již existuje. Zadejte prosím jiný název." - applyConfirm: "Opravdu chcete na toto zařízení použít zálohu \"{name}\"? Stávající nastavení tohoto zařízení bude přepsáno." - saveConfirm: "Uložit zálohu jako {name}?" - deleteConfirm: "Odstranit zálohu {name}?" - renameConfirm: "Přejmenovat tuto zálohu z \"{old}\" na \"{new}\"?" - noBackups: "Neexistují žádné zálohy. Nastavení klienta na tomto serveru můžete zálohovat pomocí \"Vytvořit novou zálohu\"." - createdAt: "Vytvořeno v: {date} {time}" - updatedAt: "Aktualizováno: {date} {time}" - cannotLoad: "Načítání selhalo" - invalidFile: "Neplatný typ souboru" _registry: scope: "Rozsah" key: "Klíč" @@ -1538,207 +627,46 @@ _registry: domain: "Doména" createKey: "Vytvořit klíč" _aboutMisskey: - about: "Misskey je open-source software vyvíjený syuilo od roku 2014." - contributors: "Hlavní přispěvatelé" allContributors: "Všichni přispěvatelé" source: "Zdrojový kód" - translation: "Přeložit Misskey" - donate: "Přispějte na Misskey" - morePatrons: "Vážíme si také podpory mnoha dalších pomocníků, kteří zde nejsou uvedeni. Děkujeme! 🥰" - patrons: "Patroni" -_displayOfSensitiveMedia: - respect: "Skrýt média označená jako citlivá" - ignore: "Zobrazit média označená jako citlivá" - force: "Skrýt všechna média" -_instanceTicker: - none: "Nikdy nezobrazovat" - remote: "Zobrazit pro vzdálené uživatelé" - always: "Vždy zobrazovat" -_serverDisconnectedBehavior: - reload: "Automatické znovunačtení" - dialog: "Zobrazení dialogového okna s varováním" - quiet: "Zobrazit nerušivé upozornění" _channel: - create: "Vytvořit kanál" - edit: "Upravit kanál" - setBanner: "Nastavit banner" - removeBanner: "Odstranit banner" featured: "Trendy" - owned: "Vlastněný" - following: "Sledovaný" - usersCount: "{n} Účastníků" - notesCount: "{n} Poznámek" - nameAndDescription: "Název a popis" - nameOnly: "Pouze název" _menuDisplay: - sideFull: "Postranně" - sideIcon: "Postranně (Ikony)" top: "Nahoru" hide: "Skrýt" -_wordMute: - muteWords: "Ztlumená slova" - muteWordsDescription: "Podmínku AND oddělujte mezerami, podmínku OR oddělujte řádkovými zlomy." - muteWordsDescription2: "Chcete-li použít regulární výrazy, obklopte klíčová slova lomítky." -_instanceMute: - instanceMuteDescription: "Tímhle se ztlumí všechny poznámky/poznámky z uvedených instancí, včetně poznámek uživatelů, kteří odpovídají uživateli ze ztlumené instance." - instanceMuteDescription2: "Oddělte novými řádky" - title: "Skryje poznámky z uvedených případů." - heading: "Seznam instancí, které mají být ztlumeny" _theme: - explore: "Objevit témata" install: "Nainstalovat vzhled" manage: "Správa vzhledů" code: "Kód vzhledu" description: "Popis" - installed: "{name} byl nainstalován" installedThemes: "Nainstalované vzhledy" - builtinThemes: "Vestavěné temáta" - alreadyInstalled: "Tento vzhled je již nainstalován." - invalid: "Formát tohoto tématu je neplatný" - make: "Vytvořit téma" - base: "Základ" - addConstant: "Přidat konstantu" constant: "Konstanta" defaultValue: "Výchozí hodnota" color: "Barva" - refProp: "Odkázat na vlastnost" - refConst: "Odkázat na konstantu" key: "Klíč" func: "Funkce " - funcKind: "Typ funkce" - argument: "Argument" - basedProp: "Odkazovaná vlastnost" - alpha: "Průhlednost" - darken: "Ztmavit" - lighten: "Zesvětlit" - inputConstantName: "Zadejte název pro tuto konstantu" - importInfo: "Pokud zde zadáte kód motivu, můžete jej importovat do editoru motivu." - deleteConstantConfirm: "Opravdu chcete odstranit konstantu {const}?" keys: - accent: "Akcent" - bg: "Pozadí" - fg: "Text" - focus: "Fokus" - indicator: "Indikátor" - panel: "Panely" shadow: "Stín" header: "Nadpis" - navBg: "Pozadí postranního panelu" - navFg: "Text na postranním panelu" - navActive: "Text na postranním panelu (Aktivní)" - navIndicator: "Indikátor na postranním panelu" link: "Odkaz" hashtag: "Hashtag" mention: "Zmínění" - mentionMe: "Zmínky (mě)" renote: "Přeposlat" - modalBg: "Pozadí Modalu" divider: "Dělící čára" - scrollbarHandle: "Rukojeť posuvníku" - scrollbarHandleHover: "Rukojeť posuvníku (Hover)" - dateLabelFg: "Text štítku s datem" - infoBg: "Pozadí informací" - infoFg: "Text informací" - infoWarnBg: "Pozadí varování" - infoWarnFg: "Text varování" - toastBg: "Pozadí oznámení" - toastFg: "Text oznámení" - buttonBg: "Pozadí tlačítka" - buttonHoverBg: "Pozadí tlačítka (Hover)" - inputBorder: "Ohraničení vstupního pole" - badge: "Odznak" - messageBg: "Pozadí chatu" - fgHighlighted: "Zvýrazněný text" _sfx: note: "Poznámky" - noteMy: "Moje poznámka" notification: "Oznámení" + chat: "Zprávy" _ago: future: "Budoucí" justNow: "Teď" - secondsAgo: "Před {n}s" - minutesAgo: "Před {n}min" - hoursAgo: "Před {n}h" - daysAgo: "Před {n}d" - weeksAgo: "Před {n}t" - monthsAgo: "Před {n}m" - yearsAgo: "Před {n}r" invalid: "Nic nebylo nalezeno" _time: second: "Sekund" minute: "Minut" hour: "Hodin" - day: "Dnů" _2fa: - alreadyRegistered: "Již jste zaregistrovali dvoufaktorové ověřovací zařízení." - registerTOTP: "Registrovat aplikaci autentizátoru" - step1: "Nejprve si do zařízení nainstalujte aplikaci pro ověřování (například {a} nebo {b})." - step2: "Poté naskenujte QR kód zobrazený na této obrazovce." - step3Title: "Zadejte ověřovací kód" - step3: "Pro dokončení nastavení zadejte token poskytnutý vaší aplikací." - step4: "Od této chvíle budou všechny budoucí pokusy o přihlášení vyžadovat tento přihlašovací token." - securityKeyNotSupported: "Váš prohlížeč nepodporuje bezpečnostní klíče." - registerTOTPBeforeKey: "Nastavte aplikaci autentizátoru pro registraci bezpečnostního nebo přístupového klíče." - securityKeyInfo: "Kromě ověřování otiskem prstu nebo PIN můžete nastavit také ověřování pomocí hardwarových bezpečnostních klíčů, které podporují FIDO2, a svůj účet tak dále zabezpečit." - registerSecurityKey: "Registrace bezpečnostního nebo přístupového klíče" - securityKeyName: "Zadejte název klíče" - tapSecurityKey: "Při registraci bezpečnostního nebo přístupového klíče postupujte podle svého prohlížeče." - removeKey: "Odstranit bezpečnostní klíč" - removeKeyConfirm: "Opravdu chcete odstranit klíč {name}?" - whyTOTPOnlyRenew: "Aplikaci autentizátoru nelze odstranit, dokud je zaregistrován bezpečnostní klíč." - renewTOTP: "Překonfigurování aplikace autentizátor" - renewTOTPConfirm: "Tohle způsobí, že ověřovací kódy z předchozí aplikace přestanou fungovat." - renewTOTPOk: "Přenastavit" renewTOTPCancel: "Ne děkuji" -_permissions: - "read:account": "Zobrazit informace o účtu" - "write:account": "Upravit informace o účtu" - "read:blocks": "Zobrazit seznam blokovaných uživatelů" - "write:blocks": "Upravit seznam blokovaných uživatelů" - "read:drive": "Přístup k souborům a složkám na disku" - "write:drive": "Úprava nebo odstranění souborů a složek na disku" - "read:favorites": "Zobrazit seznam oblíbených" - "write:favorites": "Upravit seznam oblíbených" - "read:following": "Zobrazit informace o tom, koho sledujete" - "write:following": "Sledování nebo zrušení sledování jiných účtů" - "read:messaging": "Zobrazit chat" - "write:messaging": "Sestavit nebo mazat zprávy chatu" - "read:mutes": "Zobrazit seznam ztlumených uživatelů" - "write:mutes": "Upravit seznam ztlumených uživatelů" - "write:notes": "Sestavit nebo odstranit poznámky" - "read:notifications": "Zobrazit oznámení" - "write:notifications": "Spravit oznámení" - "read:reactions": "Zobrazit vaše reakce" - "write:reactions": "Upravit své reakce" - "write:votes": "Hlasovat v anketě" - "read:pages": "Zobrazit své stránky" - "write:pages": "Upravit nebo odstranit stránky" - "read:page-likes": "Zobrazit to se mi líbí na stránkách" - "write:page-likes": "Upravit to se mi líbí na stránkách" - "read:user-groups": "Zobrazit skupiny uživatelů" - "write:user-groups": "Upravit nebo odstranit skupiny uživatelů" - "read:channels": "Zobrazit své kanály" - "write:channels": "Upravit kanály" - "read:gallery": "Zobrazit galerii" - "write:gallery": "Upravit galerii" - "read:gallery-likes": "Zobrazit seznam to se mi líbí příspěvků v galerii" - "write:gallery-likes": "Upravit seznam to se mi líbí příspěvků v galerii" - "write:chat": "Sestavit nebo mazat zprávy chatu" -_auth: - shareAccessTitle: "Udělovat oprávnění k aplikacím" - shareAccess: "Chcete autorizovat \"{name}\" pro přístup k tomuto účtu?" - shareAccessAsk: "Opravdu chcete této aplikaci povolit přístup k vašemu účtu?" - permission: "{name} požaduje tato oprávnění" - permissionAsk: "Tato aplikace požaduje následující oprávnění" - pleaseGoBack: "Vraťte se prosím zpět do aplikace" - callback: "Návrat k aplikaci" - denied: "Přístup odepřen" - pleaseLogin: "Pro autorizaci aplikací se prosím přihlaste." -_antennaSources: - all: "Všechny poznámky" - homeTimeline: "Poznámky sledovaných uživatelů" - users: "Poznámky konkrétních uživatelů" - userList: "Poznámky z určitého seznamu uživatelů" _weekday: sunday: "Neděle" monday: "Pondělí" @@ -1750,81 +678,38 @@ _weekday: _widgets: profile: "Váš profil" instanceInfo: "Informace o instanci" - memo: "Přilepené poznámky" notifications: "Oznámení" timeline: "Časová osa" calendar: "Kalendář" trends: "Trendy" clock: "Hodiny" rss: "RSS čtečka" - rssTicker: "RSS Ticker" activity: "Aktivita" photos: "Fotky" digitalClock: "Digitální hodiny" - unixClock: "Hodiny UNIX" federation: "Federace" - instanceCloud: "Cloud instance" - postForm: "Formulář pro odeslání" slideshow: "Prezentace" button: "Tlačítko" onlineUsers: "Online uživatelé" jobQueue: "Fronta úloh" - serverMetric: "Metriky serveru" aiscript: "AiScript conzole" - aiscriptApp: "Aplikace AiScript" aichan: "Ai" - userList: "Seznam uživatelů" _userList: chooseList: "Vybrat seznam" - clicker: "Clicker" _cw: hide: "Skrýt" show: "Zobrazit více" - chars: "{count} charakterů" - files: "{count} souborů" _poll: - noOnlyOneChoice: "Jsou zapotřebí alespoň dvě možnosti" - choiceN: "Volba {n}" noMore: "Více už přidat nemůžete" - canMultipleVote: "Umožnit výběr více možností" - expiration: "Ukončení ankety" infinite: "Nikdy" - at: "Ukončit v" - after: "Ukončit po" deadlineDate: "Datum ukončení" deadlineTime: "Hodin" duration: "Trvání" - votesCount: "{n} hlasů" - totalVotes: "{n} hlasů celkově" - vote: "Hlasovat v anketě" - showResult: "Zobrazit výsledky" - voted: "Odhlasováno" - closed: "Uzavřeno" - remainingDays: "Zbývá {d} den/dní a {h} hodin/a" - remainingHours: "Zbývá {h} hodin/a a {m} minut/a" - remainingMinutes: "Zbývá {m} minut/a a {s} sekund/a" - remainingSeconds: "Zbývá {s} sekund/a" _visibility: - public: "Veřejný" - publicDescription: "Vaše poznámka bude viditelná pro všechny uživatele" home: "Domů" - homeDescription: "Zveřejnit příspěvek pouze na domovskou časovou osu" followers: "Sledující" - followersDescription: "Zviditelnit pouze pro své sledující" - specified: "Přímý" - specifiedDescription: "Zviditelnit pouze pro určité uživatele" - disableFederation: "Defederace" - disableFederationDescription: "Nepřenášet do jiných instancí" _postForm: - replyPlaceholder: "Odpovědět na tuto poznámku..." - quotePlaceholder: "Citovat tuto poznámku..." - channelPlaceholder: "Zveřejnit příspěvek do kanálu..." _placeholders: - a: "Co máte v plánu?" - b: "Co se děje kolem vás?" - c: "Co máte na mysli?" - d: "Co chcete říct?" - e: "Začít psát..." f: "Čekám, až něco napíšete..." _profile: name: "Jméno" @@ -1832,98 +717,36 @@ _profile: description: "O mně" youCanIncludeHashtags: "V popisku o Vás můžete použít i hastagy." metadata: "Doplňující informace" - metadataEdit: "Upravit doplňující informace" - metadataDescription: "Pomocí nich můžete ve svém profilu zobrazit doplňující informační pole." - metadataLabel: "Popisek" metadataContent: "Obsah" - changeAvatar: "Změnit avatara" - changeBanner: "Změnit banner" _exportOrImport: allNotes: "Všechny poznámky" - favoritedNotes: "Oblíbené poznámky" - clips: "Oříznout" followingList: "Sledovaní" muteList: "Ztlumit" blockingList: "Zablokovat" userLists: "Seznamy" - excludeMutingUsers: "Vyloučit ztlumené uživatele" - excludeInactiveUsers: "Vyloučit neaktivní uživatele" _charts: federation: "Federace" apRequest: "Požadavek" - usersIncDec: "Rozdíl v počtech uživatelů" usersTotal: "Celkem uživatelů" activeUsers: "Aktivní uživatelé" - notesIncDec: "Rozdíl v počtu poznámek" - localNotesIncDec: "Rozdíl v počtu místních poznámek" - remoteNotesIncDec: "Rozdíl v počtu vzdálených poznámek" notesTotal: "Celkový počet poznámek" - filesIncDec: "Rozdíl v počtu souborů" - filesTotal: "Celkový počet souborů" - storageUsageIncDec: "Rozdíl ve využití úložiště" - storageUsageTotal: "Celkové využití úložiště" -_instanceCharts: - requests: "Požadavky" - users: "Rozdíl v počtech uživatelů" - usersTotal: "Kumulativní počet uživatelů" - notes: "Rozdíl v počtu poznámek" - notesTotal: "Kumulativní počet poznámek" - ff: "Rozdíl v počtu sledovaných uživatelů / sledujících" - ffTotal: "Kumulativní počet sledovaných uživatelů / sledujících" - cacheSize: "Rozdíl ve velikosti mezipaměti" - cacheSizeTotal: "Kumulativní celková velikost mezipaměti" - files: "Rozdíl v počtu souborů" - filesTotal: "Kumulativní počet souborů" _timelines: home: "Domů" - local: "Místní" - social: "Sociální síť" global: "Globální" _play: - new: "Vytvořit Play" - edit: "Upravit Play" - created: "Play vytvořen" - updated: "Play upraven" - deleted: "Play smazán" - pageSetting: "Nastavení Play" - editThisPage: "Upravit tenhle Play" - viewSource: "Zobrazit zdroj" - my: "Moje Plays" - liked: "To se mi líbí Plays" - featured: "Populární" - title: "Titulek" script: "Skript" summary: "Popis" _pages: newPage: "Vytvořit novou stránku" editPage: "Upravit stránku" - readPage: "Prohlížení zdroje této stránky" + created: "Stránka byla úspěšně vytvořena" + updated: "Stránka byla úspěšně aktualizována" + deleted: "Stránka byla úspěšně smazána" pageSetting: "Nastavení stránky" - nameAlreadyExists: "Zadaná adresa URL stránky již existuje" - invalidNameTitle: "Zadaná adresa URL stránky je neplatná" invalidNameText: "Ujistěte se že jméno stránky je vyplněno" - editThisPage: "Upravit tuto stránku" - viewSource: "Zobrazit zdroj" - viewPage: "Zobrazit své stránky" - like: "To se mi líbí" - unlike: "Už se mi to nelíbí" - my: "Moje stránky" - liked: "To se mi líbí stránky" - featured: "Populární" - inspector: "Inspektor" contents: "Obsah" - content: "Blok stránky" - variables: "Proměnné" - title: "Titulek" - url: "URL stránky" - summary: "Přehled stránky" - alignCenter: "Vycentrovat prvky" - hideTitleWhenPinned: "Skrytí názvu stránky při připnutí k profilu" - font: "Písmo" fontSerif: "Serif" fontSansSerif: "Sans Serif" - eyeCatchingImageSet: "Nastavení miniatury" - eyeCatchingImageRemove: "Smazání miniatury" chooseBlock: "Přidat blok" selectType: "Vyberte typ" contentBlocks: "Obsah" @@ -1935,28 +758,8 @@ _pages: section: "Sekce" image: "Obrázky" button: "Tlačítko" - note: "Vestavěná poznámka" - _note: - id: "ID poznámky" - idDescription: "Adresu URL poznámky můžete vložit také sem." - detailed: "Podrobné zobrazení" -_relayStatus: - requesting: "Čeká se" - accepted: "Schváleno" - rejected: "Odmítnuto" _notification: - fileUploaded: "Soubor úspěšně nahrán" - youGotMention: "{name} vás zmínil" - youGotReply: "{name} vám odpověděl" - youGotQuote: "{name} vás citoval" - youRenoted: "Poznámka od {name}" youWereFollowed: "Máte nového následovníka" - youReceivedFollowRequest: "Obdrželi jste žádost o sledování" - yourFollowRequestAccepted: "Vaše žádost o sledování byla přijata" - pollEnded: "Výsledky ankety jsou k dispozici" - unreadAntennaNote: "Anténa {name}" - emptyPushNotificationMessage: "Push oznámení byla aktualizována" - achievementEarned: "Úspěch odemčen" _types: all: "Vše" follow: "Sledovaní" @@ -1965,81 +768,17 @@ _notification: renote: "Přeposlat" quote: "Citovat" reaction: "Reakce" - pollEnded: "Anketa končí" - receiveFollowRequest: "Obdržené žádosti o sledování" - followRequestAccepted: "Přijaté žádosti o sledování" - achievementEarned: "Úspěch odemčen" - login: "Přihlásit se" - app: "Oznámení z propojených aplikací" _actions: - followBack: "vás začal sledovat zpět" reply: "Odpovědět" renote: "Přeposlat" _deck: - alwaysShowMainColumn: "Vždy zobrazovat hlavní sloupec" - columnAlign: "Zarovnat sloupce" - addColumn: "Přidat sloupec" - configureColumn: "Nastavení sloupců" - swapLeft: "Prohodit s levým sloupcem" - swapRight: "Prohodit s pravým sloupcem" - swapUp: "Prohodit s výše uvedeným sloupcem" - swapDown: "Prohodit s níže uvedeným sloupcem" - stackLeft: "Nahromadit v levém sloupci" - popRight: "Popnout sloupec na pravou stranu" - profile: "Profil" - newProfile: "Nový profil" - deleteProfile: "Smazat profil" - introduction: "Vytvořte si dokonalé rozhraní volným uspořádáním sloupců!" - introduction2: "Kliknutím na tlačítko + v pravé části obrazovky můžete kdykoli přidat nové sloupce." - widgetsIntroduction: "V nabídce sloupce vyberte možnost \"Upravit widgety\" a přidejte widget." - useSimpleUiForNonRootPages: "Použít zjednodušené uživatelské rozhraní pro navigaci na stránkách" _columns: - main: "Hlavní" - widgets: "Widgety" notifications: "Oznámení" tl: "Časová osa" antenna: "Antény" list: "Seznamy" channel: "Kanály" mentions: "Zmínění" - direct: "Přímý" - roleTimeline: "Časová osa role" -_dialog: - charactersExceeded: "Překročili jste maximální počet znaků! V současné době je na hodnotě {current} z {max}." - charactersBelow: "Nedosahujete minimálního limitu znaků! V současné době je na {current} z {min}." -_disabledTimeline: - title: "Časová osa vypnuta" - description: "Tuto časovou osu nemůžete používat v rámci svých současných rolí." -_drivecleaner: - orderBySizeDesc: "Sestupná velikost souborů" - orderByCreatedAtAsc: "Vzestupné datumy" _webhookSettings: - createWebhook: "Vytvořit Webhook" name: "Jméno" - secret: "Tajné" active: "Zapnuto" - _events: - follow: "Při sledování uživatele" - followed: "Při sledování" - note: "Při zveřejňování poznámky" - reply: "Při obdržení odpovědi" - renote: "Při renotaci poznámky" - reaction: "Při obdržení reakce" - mention: "Při zmínce" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" -_moderationLogTypes: - suspend: "Zmrazit" - resetPassword: "Resetovat heslo" - createInvitation: "Vygenerovat pozvánku" -_reversi: - total: "Celkem" -_remoteLookupErrors: - _noSuchObject: - title: "Nenalezeno" -_search: - searchScopeAll: "Vše" - searchScopeLocal: "Místní" - searchScopeUser: "Upřesnit uživatele" diff --git a/locales/da-DK.yml b/locales/da-DK.yml index 5eb7a5a5f4..08c15ed092 100644 --- a/locales/da-DK.yml +++ b/locales/da-DK.yml @@ -1,4 +1,2 @@ --- _lang_: "Dansk" -headlineMisskey: "" -introMisskey: "ようこそ!Misskeyは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 26445ae0ca..e33ceb0f0b 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -2,16 +2,12 @@ _lang_: "Deutsch" headlineMisskey: "Ein durch Notizen verbundenes Netzwerk" introMisskey: "Willkommen! Misskey ist eine dezentralisierte Open-Source Microblogging-Platform.\nVerfasse „Notizen“ um mitzuteilen, was gerade passiert oder um Ereignisse mit anderen zu teilen. 📡\nMit „Reaktionen“ kannst du außerdem schnell deine Gefühle über Notizen anderer Benutzer zum Ausdruck bringen. 👍\nEine neue Welt wartet auf dich! 🚀" -poweredByMisskeyDescription: "{name} ist einer der durch die Open-Source-Plattform Misskey betriebenen Dienste." +poweredByMisskeyDescription: "{name} ist einer der durch die Open-Source-Plattform Misskey betriebenen Dienste (meist als \"Misskey-Instanz\" bezeichnet)." monthAndDay: "{day}.{month}." search: "Suchen" -reset: "Zurücksetzen" notifications: "Benachrichtigungen" username: "Benutzername" password: "Passwort" -initialPasswordForSetup: "Initiales Passwort für die Einrichtung" -initialPasswordIsIncorrect: "Das initiale Passwort für die Einrichtung ist falsch" -initialPasswordForSetupDescription: "Verwende das in der Konfigurationsdatei angegebene Passwort, wenn du Misskey selbst installiert hast.\nWenn du einen Misskey-Hostingdienst o.ä. nutzt, verwende das dort angegebene Kennwort.\nWenn du kein Passwort festgelegt hast, lasse es leer, um fortzufahren." forgotPassword: "Passwort vergessen" fetchingAsApObject: "Wird aus dem Fediverse angefragt …" ok: "OK" @@ -49,13 +45,10 @@ pin: "An dein Profil anheften" unpin: "Von deinem Profil lösen" copyContent: "Inhalt kopieren" copyLink: "Link kopieren" -copyRemoteLink: "Remote-Link kopieren" -copyLinkRenote: "Renote-Link kopieren" delete: "Löschen" deleteAndEdit: "Löschen und Bearbeiten" deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen." addToList: "Zu Liste hinzufügen" -addToAntenna: "Zu Antenne hinzufügen" sendMessage: "Nachricht senden" copyRSS: "RSS kopieren" copyUsername: "Benutzernamen kopieren" @@ -63,9 +56,7 @@ copyUserId: "Benutzer-ID kopieren" copyNoteId: "Notiz-ID kopieren" copyFileId: "Datei-ID kopieren" copyFolderId: "Ordner-ID kopieren" -copyProfileUrl: "Profil-URL kopieren" searchUser: "Nach einem Benutzer suchen" -searchThisUsersNotes: "Notizen dieses Benutzers suchen" reply: "Antworten" loadMore: "Mehr laden" showMore: "Mehr anzeigen" @@ -81,7 +72,7 @@ import: "Import" export: "Export" files: "Dateien" download: "Herunterladen" -driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Einige Inhalte, die diese Datei verwenden, werden auch verschwinden." +driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Sie wird in allen Inhalten, die sie verwenden, auch verschwinden." unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?" exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt." importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen." @@ -114,14 +105,11 @@ enterEmoji: "Gib ein Emoji ein" renote: "Renote" unrenote: "Renote zurücknehmen" renoted: "Renote getätigt." -renotedToX: "Renoted zu {name}." cantRenote: "Renote dieses Beitrags nicht möglich." cantReRenote: "Renote einer Renote nicht möglich." quote: "Zitieren" inChannelRenote: "Kanal-interner Renote" inChannelQuote: "Kanal-internes Zitat" -renoteToChannel: "Renote zu Kanal" -renoteToOtherChannel: "Renote zu anderem Kanal" pinnedNote: "Angeheftete Notiz" pinned: "Angeheftet" you: "Du" @@ -130,16 +118,10 @@ sensitive: "Sensibel" add: "Hinzufügen" reaction: "Reaktionen" reactions: "Reaktionen" -emojiPicker: "Emoji auswählen" -pinnedEmojisForReactionSettingDescription: "Lege Emojis fest, die angepinnt werden sollen, um sie beim Reagieren als Erstes anzuzeigen." -pinnedEmojisSettingDescription: "Lege Emojis fest, die angepinnt werden sollen, um sie in der Emoji-Auswahl als Erstes anzuzeigen" -emojiPickerDisplay: "Anzeige der Emoji-Auswahl" -overwriteFromPinnedEmojisForReaction: "Überschreiben mit den Reaktions-Einstellungen" -overwriteFromPinnedEmojis: "Überschreiben mit den allgemeinen Einstellungen" +reactionSetting: "In der Reaktionsauswahl anzuzeigende Reaktionen" reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen" rememberNoteVisibility: "Notizsichtbarkeit merken" attachCancel: "Anhang entfernen" -deleteFile: "Datei löschen" markAsSensitive: "Als sensibel markieren" unmarkAsSensitive: "Als nicht sensibel markieren" enterFileName: "Dateinamen eingeben" @@ -160,7 +142,6 @@ editList: "Liste bearbeiten" selectChannel: "Kanal auswählen" selectAntenna: "Antenne auswählen" editAntenna: "Antenne bearbeiten" -createAntenna: "Erstelle eine Antenne" selectWidget: "Widget auswählen" editWidgets: "Widgets bearbeiten" editWidgetsExit: "Fertig" @@ -173,9 +154,6 @@ addEmoji: "Emoji hinzufügen" settingGuide: "Empfohlene Einstellung" cacheRemoteFiles: "Dateien von fremden Instanzen im Cache speichern" cacheRemoteFilesDescription: "Ist diese Einstellung deaktiviert, so werden Dateien fremder Instanzen direkt von dort geladen. Hierdurch wird Speicherplatz auf diesem Server gespart, aber durch fehlende Generierung von Vorschaubildern mehr Bandbreite verwendet." -youCanCleanRemoteFilesCache: "Klicke auf den 🗑️-Knopf der Dateiverwaltungsansicht, um den Cache zu leeren." -cacheRemoteSensitiveFiles: "Sensitive Dateien von fremden Instanzen im Cache speichern" -cacheRemoteSensitiveFilesDescription: "Ist diese Einstellung deaktiviert, so werden sensitive Dateien fremder Instanzen direkt von dort ohne Zwischenspeicherung geladen." flagAsBot: "Als Bot markieren" flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt Misskeys interne Systeme dieses Benutzerkonto als Bot behandeln." flagAsCat: "Als Katze markieren" @@ -187,10 +165,6 @@ addAccount: "Benutzerkonto hinzufügen" reloadAccountsList: "Benutzerkontoliste aktualisieren" loginFailed: "Anmeldung fehlgeschlagen" showOnRemote: "Auf Ursprungsinstanz ansehen" -continueOnRemote: "Weiter auf Remote-Server" -chooseServerOnMisskeyHub: "Wähle einen Server aus dem Misskey Hub" -specifyServerHost: "Server-Host auswählen" -inputHostName: "Gib die Domain an" general: "Allgemein" wallpaper: "Hintergrund" setWallpaper: "Hintergrund festlegen" @@ -199,9 +173,8 @@ searchWith: "Suchen: {q}" youHaveNoLists: "Du hast keine Listen" followConfirm: "Möchtest du {name} wirklich folgen?" proxyAccount: "Proxy-Benutzerkonto" -proxyAccountDescription: "Ein Proxy-Konto ist ein Benutzerkonto, das unter bestimmten Bedingungen als Follower für Benutzer fremder Instanzen fungiert. Wenn zum Beispiel ein Benutzer einen Benutzer einer fremden Instanz zu einer Liste hinzufügt, werden die Aktivitäten des entfernten Benutzers nicht an die Instanz übermittelt, wenn kein lokaler Benutzer diesem Benutzer folgt; stattdessen folgt das Proxy-Konto." +proxyAccountDescription: "Ein Proxy-Benutzerkonto ist ein Benutzerkonto, das sich für Nutzer unter bestimmten Konditionen wie ein Follower aus einer fremden Instanz verhält. Zum Beispiel wird die Aktivität eines Nutzers aus einer fremden Instanz nicht an diese Instanz übermittelt, falls es keinen Benutzer dieser Instanz gibt, der diesem Nutzer aus fremder Instanz folgt. In diesem Fall folgt stattdessen das Proxy-Benutzerkonto." host: "Hostname" -selectSelf: "Mich auswählen" selectUser: "Benutzer auswählen" recipient: "Empfänger" annotation: "Anmerkung" @@ -216,11 +189,8 @@ perHour: "Pro Stunde" perDay: "Pro Tag" stopActivityDelivery: "Senden von Aktivitäten einstellen" blockThisInstance: "Diese Instanz blockieren" -silenceThisInstance: "Instanz stummschalten" -mediaSilenceThisInstance: "Medien dieses Servers stummschalten" operations: "Aktionen" software: "Software" -softwareName: "Software Name" version: "Version" metadata: "Metadaten" withNFiles: "{n} Datei(en)" @@ -238,12 +208,6 @@ clearCachedFiles: "Cache leeren" clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen Instanzen wirklich gelöscht werden?" blockedInstances: "Blockierte Instanzen" blockedInstancesDescription: "Gib die Hostnamen der Instanzen, welche blockiert werden sollen, durch Zeilenumbrüche getrennt an. Blockierte Instanzen können mit dieser instanz nicht mehr kommunizieren." -silencedInstances: "Stummgeschaltete Instanzen" -silencedInstancesDescription: "Gib die Hostnamen der Instanzen, welche stummgeschaltet werden sollen, durch Zeilenumbrüche getrennt an. Alle Konten dieser Instanzen werden als stummgeschaltet behandelt, können nur noch Follow-Anfragen stellen und wenn nicht gefolgt keine lokalen Konten erwähnen. Blockierte Instanzen sind davon nicht betroffen." -mediaSilencedInstances: "Medien-stummgeschaltete Server" -mediaSilencedInstancesDescription: "Gib pro Zeile die Hostnamen der Server ein, dessen Medien du stummschalten möchtest. Alle Benutzerkonten der aufgeführten Server werden als sensibel behandelt und können keine benutzerdefinierten Emojis verwenden. Gesperrte Server sind davon nicht betroffen." -federationAllowedHosts: "Föderierte Instanzen" -federationAllowedHostsDescription: "Trage die Hostnamen ein mit den du eine Föderation eingehen möchtest. Trenne mit Zeilenumbruch." muteAndBlock: "Stummschaltungen und Blockierungen" mutedUsers: "Stummgeschaltete Benutzer" blockedUsers: "Blockierte Benutzer" @@ -251,6 +215,7 @@ noUsers: "Keine Benutzer gefunden" editProfile: "Profil bearbeiten" noteDeleteConfirm: "Möchtest du diese Notiz wirklich löschen?" pinLimitExceeded: "Du kannst nicht noch mehr Notizen anheften." +intro: "Misskey ist installiert! Lass uns nun ein Administratorkonto einrichten." done: "Fertig" processing: "In Bearbeitung …" preview: "Vorschau" @@ -261,7 +226,7 @@ noJobs: "Keine Jobs vorhanden" federating: "Wird föderiert" blocked: "Blockiert" suspended: "Gesperrt" -all: "Alle" +all: "Alles" subscribing: "Wird abonniert" publishing: "Wird veröffentlicht" notResponding: "Antwortet nicht" @@ -287,8 +252,8 @@ removed: "Erfolgreich gelöscht" removeAreYouSure: "Möchtest du „{x}“ wirklich entfernen?" deleteAreYouSure: "Möchtest du „{x}“ wirklich löschen?" resetAreYouSure: "Wirklich zurücksetzen?" -areYouSure: "Bist du sicher?" saved: "Erfolgreich gespeichert" +messaging: "Chat" upload: "Hochladen" keepOriginalUploading: "Originalbild speichern" keepOriginalUploadingDescription: "Speichert das Originalbild so, wie es ist. Ist dies deaktiviert, wird eine Version zum Anzeigen im Internet generiert." @@ -301,7 +266,7 @@ uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschl explore: "Erkunden" messageRead: "Gelesen" noMoreHistory: "Kein weiterer Verlauf vorhanden" -startChat: "Chat starten" +startMessaging: "Neuen Chat erstellen" nUsersRead: "Von {n} Benutzern gelesen" agreeTo: "Ich stimme {0} zu" agree: "Zustimmen" @@ -332,15 +297,12 @@ selectFile: "Datei auswählen" selectFiles: "Dateien auswählen" selectFolder: "Ordner auswählen" selectFolders: "Ordner auswählen" -fileNotSelected: "Keine Datei ausgewählt" renameFile: "Datei umbenennen" folderName: "Ordnername" createFolder: "Ordner erstellen" renameFolder: "Ordner umbenennen" deleteFolder: "Ordner löschen" -folder: "Ordner" addFile: "Datei hinzufügen" -showFile: "Datei anzeigen" emptyDrive: "Deine Drive ist leer" emptyFolder: "Dieser Ordner ist leer" unableToDelete: "Nicht löschbar" @@ -353,7 +315,7 @@ copyUrl: "URL kopieren" rename: "Umbenennen" avatar: "Profilbild" banner: "Banner" -displayOfSensitiveMedia: "Darstellung sensibler Medien" +displayOfSensitiveMedia: "Anzeige von sensiblen Medien" whenServerDisconnected: "Bei Verbindungsverlust zum Server" disconnectedFromServer: "Die Verbindung zum Server wurde getrennt" reload: "Aktualisieren" @@ -383,10 +345,12 @@ enableLocalTimeline: "Lokale Chronik aktivieren" enableGlobalTimeline: "Globale Chronik aktivieren" disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle Chroniken, auch wenn diese deaktiviert sind." registration: "Registrieren" +enableRegistration: "Registration neuer Benutzer erlauben" invite: "Einladen" driveCapacityPerLocalAccount: "Drive-Kapazität pro lokalem Benutzerkonto" driveCapacityPerRemoteAccount: "Drive-Kapazität pro Benutzer fremder Instanzen" inMb: "In Megabytes" +iconUrl: "Icon-URL (favicon etc)" bannerUrl: "Banner-URL" backgroundImageUrl: "Hintergrundbild-URL" basicInfo: "Grundlegende Informationen" @@ -400,11 +364,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptcha aktivieren" hcaptchaSiteKey: "Site key" hcaptchaSecretKey: "Secret key" -mcaptcha: "mCaptcha" -enableMcaptcha: "mCaptcha aktivieren" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret key" -mcaptchaInstanceUrl: "mCaptcha Instanz-URL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHA aktivieren" recaptchaSiteKey: "Site key" @@ -420,11 +379,9 @@ name: "Name" antennaSource: "Antennenquelle" antennaKeywords: "Zu beobachtende Schlüsselwörter" antennaExcludeKeywords: "Zu ignorierende Schlüsselwörter" -antennaExcludeBots: "Bot-Accounts ausschließen" antennaKeywordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen" notifyAntenna: "Über neue Notizen benachrichtigen" withFileAntenna: "Nur Notizen mit Dateien" -excludeNotesInSensitiveChannel: "Schließe Notizen von sensitive Kanäle aus" enableServiceworker: "Push-Benachrichtigungen im Browser aktivieren" antennaUsersDescription: "Benutzernamen getrennt durch Zeilenumbrüche angeben" caseSensitive: "Groß-/Kleinschreibung unterscheiden" @@ -449,23 +406,18 @@ aboutMisskey: "Über Misskey" administrator: "Administrator" token: "Token" 2fa: "Zwei-Faktor-Authentifizierung" -setupOf2fa: "Zweifaktorauthentifizierung einrichten" totp: "Authentifizierungs-App" totpDescription: "Logge dich via Authentifizierungs-App mit Einmalpasswort ein" moderator: "Moderator" moderation: "Moderation" -moderationNote: "Moderationsnotiz" -moderationNoteDescription: "Trage hier Notizen ein. Diese sind nur für die Moderatoren sichtbar." -addModerationNote: "Moderationsnotiz hinzufügen" -moderationLogs: "Moderationsprotokolle" nUsersMentioned: "Von {n} Benutzern erwähnt" -securityKeyAndPasskey: "Hardware-Sicherheitsschlüssel und Passkeys" -securityKey: "Hardware-Sicherheitsschlüssel" +securityKeyAndPasskey: "Security-Tokens und Passkeys" +securityKey: "Sicherheitsschlüssel" lastUsed: "Zuletzt benutzt" lastUsedAt: "Zuletzt verwendet: {t}" unregister: "Deaktivieren" passwordLessLogin: "Passwortloses Anmelden" -passwordLessLoginDescription: "Ermöglicht passwortloses Einloggen mit einem Security-Token oder Passkey" +passwordLessLoginDescription: "Ermöglicht passwortfreies Einloggen, nur via Security-Token oder Passkey" resetPassword: "Passwort zurücksetzen" newPasswordIs: "Das neue Passwort ist „{password}“" reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren" @@ -473,6 +425,7 @@ share: "Teilen" notFound: "Nicht gefunden" notFoundDescription: "Es konnte keine Seite unter dieser URL gefunden werden." uploadFolder: "Standardordner für Uploads" +cacheClear: "Cache leeren" markAsReadAllNotifications: "Alle Benachrichtigungen als gelesen markieren" markAsReadAllUnreadNotes: "Alle Notizen als gelesen markieren" markAsReadAllTalkMessages: "Alle Chats als gelesen markieren" @@ -490,10 +443,10 @@ retype: "Erneut eingeben" noteOf: "Notiz von {user}" quoteAttached: "Zitat" quoteQuestion: "Als Zitat anhängen?" -attachAsFileQuestion: "Der Text in der Zwischenablage ist lang. Möchtest du ihn als Textdatei anhängen?" +noMessagesYet: "Noch keine Nachrichten vorhanden" +newMessageExists: "Du hast eine neue Nachricht" onlyOneFileCanBeAttached: "Es kann pro Nachricht nur eine Datei angehängt werden" signinRequired: "Bitte registriere oder melde dich an, um fortzufahren" -signinOrContinueOnRemote: "Um fortzufahren, gehe zu deiner Instanz oder registriere bzw. melde dich an dieser Instanz an. " invitations: "Einladungen" invitationCode: "Einladungscode" checking: "Wird überprüft …" @@ -515,12 +468,8 @@ uiLanguage: "Sprache der Benutzeroberfläche" aboutX: "Über {x}" emojiStyle: "Emoji-Stil" native: "Nativ" -menuStyle: "Menü Stil" -style: "Stil" -drawer: "App-Übersicht" -popup: "Pop-up" +disableDrawer: "Keine ausfahrbaren Menüs verwenden" showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen" -showReactionsCount: "Zeige die Anzahl der Reaktionen auf Notizen an" noHistory: "Kein Verlauf gefunden" signinHistory: "Anmeldungsverlauf" enableAdvancedMfm: "Erweitertes MFM aktivieren" @@ -573,7 +522,6 @@ serverLogs: "Serverprotokolle" deleteAll: "Alle löschen" showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen" showFixedPostFormInChannel: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen (Kanäle)" -withRepliesByDefaultForNewlyFollowed: "Standardmäßig Antworten von neu gefolgten Benutzern in der Chronik anzeigen" newNoteRecived: "Es gibt neue Notizen" sounds: "Töne" sound: "Töne" @@ -583,15 +531,12 @@ showInPage: "In einer Seite anzeigen" popout: "Pop-Up" volume: "Lautstärke" masterVolume: "Gesamtlautstärke" -notUseSound: "Gebe kein Ton aus" -useSoundOnlyWhenActive: "Gebe nur Ton aus, wenn Misskey aktiv ist" details: "Details" -renoteDetails: "Renote Details" chooseEmoji: "Emoji auswählen" unableToProcess: "Der Vorgang konnte nicht abgeschlossen werden" recentUsed: "Vor kurzem verwendet" install: "Installieren" -uninstall: "Deinstallieren" +uninstall: "Uninstallieren" installedApps: "Authorisierte Anwendungen" nothing: "Hier gibt es nichts zu sehen" installedDate: "Authorisiert am" @@ -602,16 +547,10 @@ ascendingOrder: "Aufsteigende Reihenfolge" descendingOrder: "Absteigende Reihenfolge" scratchpad: "Testumgebung" scratchpadDescription: "Die Testumgebung bietet einen Bereich für AiScript-Experimente. Dort kannst du AiScript schreiben, ausführen sowie dessen Auswirkungen auf Misskey überprüfen." -uiInspector: "UI-Inspektor" -uiInspectorDescription: "Die Liste der UI-Komponenten-Server können im Zwischenspeicher angesehen werden. Die UI-Komponente wird von der Funktion Ui:C: generiert." output: "Ausgabe" script: "Skript" disablePagesScript: "AiScript auf Seiten deaktivieren" updateRemoteUser: "Benutzerinformationen aktualisieren" -unsetUserAvatar: "Entferne Profilbild" -unsetUserAvatarConfirm: "Möchtest du dein Profilbild entfernen?" -unsetUserBanner: "Entferne Profilbanner" -unsetUserBannerConfirm: "Möchtest du dein Profilbanner entfernen?" deleteAllFiles: "Alle Dateien löschen" deleteAllFilesConfirm: "Möchtest du wirklich alle Dateien löschen?" removeAllFollowing: "Allen gefolgten Benutzern entfolgen" @@ -662,7 +601,6 @@ medium: "Mittel" small: "Klein" generateAccessToken: "Zugriffstoken generieren" permission: "Berechtigungen" -adminPermission: "Administratorberechtigung" enableAll: "Alle aktivieren" disableAll: "Alle deaktivieren" tokenRequested: "Zugriff zum Benutzerkonto gewähren" @@ -684,22 +622,16 @@ smtpSecure: "Für SMTP-Verbindungen implizit SSL/TLS verwenden" smtpSecureInfo: "Schalte dies aus, falls du STARTTLS verwendest." testEmail: "Emailversand testen" wordMute: "Wortstummschaltung" -wordMuteDescription: "Minimiert Notizen, die das angegebene Wort oder den angegebenen Ausdruck enthalten. Minimierte Notizen können angezeigt werden, indem du auf sie klickst." -hardWordMute: "Harte Wortstummschaltung" -showMutedWord: "Stummgeschaltete Wörter anzeigen" -hardWordMuteDescription: "Blendet Notizen aus, die das angegebene Wort oder die angegebene Phrase enthalten. Im Gegensatz zur Wortstummschaltung wird die Notiz vollständig ausgeblendet." regexpError: "Fehler in einem regulären Ausdruck" -regexpErrorDescription: "Im regulären Ausdruck deiner in Zeile {line} von {tab}en Wortstummschaltungen ist ein Fehler aufgetreten:" +regexpErrorDescription: "Im regulären Ausdruck deiner {tab}en Wortstummschaltungen ist ein Fehler aufgetreten:" instanceMute: "Instanzstummschaltungen" userSaysSomething: "{name} hat etwas gesagt" -userSaysSomethingAbout: "{name} sagt etwas über '{word}'" makeActive: "Aktivieren" -display: "Anzeigeart" +display: "Anzeigen" copy: "Kopieren" -copiedToClipboard: "In die Zwischenablage kopiert" metrics: "Metriken" overview: "Übersicht" -logs: "Protokolle" +logs: "Logs" delayed: "Verzögert" database: "Datenbank" channel: "Kanäle" @@ -711,21 +643,22 @@ useGlobalSettingDesc: "Ist diese Option aktiviert, werden die Benachrichtigungse other: "Anderes" regenerateLoginToken: "Anmeldetoken regenerieren" regenerateLoginTokenDescription: "Den zur Anmeldung intern verwendeten Token regenerieren. Normalerweise wird dies nicht benötigt. Bei Regeneration werden alle Geräte ausgeloggt." -theKeywordWhenSearchingForCustomEmoji: "Das ist das Schlagwort beim Suchen von benutzerdefinierten Emojis." setMultipleBySeparatingWithSpace: "Trenne Elemente durch ein Leerzeichen um mehrere Einstellungen zu kofigurieren." fileIdOrUrl: "Datei-ID oder URL" behavior: "Verhalten" sample: "Beispiel" abuseReports: "Meldungen" reportAbuse: "Melden" -reportAbuseRenote: "Renote melden" reportAbuseOf: "{name} melden" fillAbuseReportDescription: "Bitte gib zusätzliche Informationen zu dieser Meldung an. Falls es sich um eine spezielle Notiz handelt, bitte gib dessen URL an." abuseReported: "Deine Meldung wurde versendet. Vielen Dank." reporter: "Melder" reporteeOrigin: "Herkunft des Gemeldeten" reporterOrigin: "Herkunft des Meldenden" +forwardReport: "Meldung an fremde Instanz weiterleiten" +forwardReportIsAnonymous: "Anstatt deines Benutzerkontos wird bei der fremden Instanz ein anonymes Systemkonto als Melder angezeigt." send: "Senden" +abuseMarkAsResolved: "Meldung als gelöst markieren" openInNewTab: "In neuem Tab öffnen" openInSideView: "In Seitenansicht öffnen" defaultNavigationBehaviour: "Standardnavigationsverhalten" @@ -743,7 +676,6 @@ createNewClip: "Neuen Clip erstellen" unclip: "Aus Clip entfernen" confirmToUnclipAlreadyClippedNote: "Diese Notiz ist bereits im \"{name}\" Clip enthalten. Möchtest du sie aus diesem Clip entfernen?" public: "Öffentlich" -private: "Privat" i18nInfo: "Misskey wird durch freiwillige Helfer in viele verschiedene Sprachen übersetzt. Auf {link} kannst du mithelfen." manageAccessTokens: "Zugriffstokens verwalten" accountInfo: "Benutzerkonto-Informationen" @@ -768,7 +700,6 @@ lockedAccountInfo: "Auch wenn du Follow-Anfragen auf manuelle Bestätigung setzt alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren" loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen" disableShowingAnimatedImages: "Animierte Bilder nicht abspielen" -highlightSensitiveMedia: "Sensitive Medien markieren" verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen." notSet: "Nicht konfiguriert" emailVerified: "Email-Adresse bestätigt" @@ -784,6 +715,7 @@ thisIsExperimentalFeature: "Dies ist eine experimentelle Funktion. Änderungen a developer: "Entwickler" makeExplorable: "Benutzerkonto in „Erkunden“ sichtbar machen" makeExplorableDescription: "Wenn diese Option deaktiviert ist, ist dein Benutzerkonto nicht im „Erkunden“-Bereich sichtbar." +showGapBetweenNotesInTimeline: "Abstände zwischen Notizen auf der Chronik anzeigen" duplicate: "Duplizieren" left: "Links" center: "Mittig" @@ -850,7 +782,7 @@ active: "Aktiv" offline: "Offline" notRecommended: "Nicht empfohlen" botProtection: "Schutz vor Bots" -instanceBlocking: "Blockierte/Stummgeschaltete Instanzen" +instanceBlocking: "Blockierte Instanzen" selectAccount: "Benutzerkonto auswählen" switchAccount: "Konto wechseln" enabled: "Aktiviert" @@ -861,7 +793,6 @@ administration: "Verwaltung" accounts: "Benutzerkonten" switch: "Wechseln" noMaintainerInformationWarning: "Betreiberinformationen sind nicht konfiguriert." -noInquiryUrlWarning: "Keine gültige Kontakt-URL." noBotProtectionWarning: "Schutz vor Bots ist nicht konfiguriert." configure: "Konfigurieren" postToGallery: "Neuen Galeriebeitrag erstellen" @@ -921,12 +852,11 @@ makeReactionsPublicDescription: "Jeder wird die Liste deiner gesendeten Reaktion classic: "Classic" muteThread: "Thread stummschalten" unmuteThread: "Threadstummschaltung aufheben" -followingVisibility: "Sichtbarkeit der Gefolgten" -followersVisibility: "Sichtbarkeit der Folgenden" +ffVisibility: "Sichtbarkeit von Gefolgten/Followern" +ffVisibilityDescription: "Konfiguriere wer sehen kann, wem du folgst sowie wer dir folgt." continueThread: "Weiteren Threadverlauf anzeigen" deleteAccountConfirm: "Dein Benutzerkonto wird unwiderruflich gelöscht. Trotzdem fortfahren?" incorrectPassword: "Falsches Passwort." -incorrectTotp: "Das Einmalpasswort ist falsch oder abgelaufen." voteConfirm: "Wirklich für „{choice}“ abstimmen?" hide: "Inhalt verbergen" useDrawerReactionPickerForMobile: "Auf mobilen Geräten ausfahrbare Reaktionsauswahl anzeigen" @@ -951,9 +881,6 @@ oneHour: "Eine Stunde" oneDay: "Einen Tag" oneWeek: "Eine Woche" oneMonth: "1 Monat" -threeMonths: "3 Monate" -oneYear: "1 Jahr" -threeDays: "3 Tage" reflectMayTakeTime: "Es kann etwas dauern, bis sich dies widerspiegelt." failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgefragt werden" rateLimitExceeded: "Versuchsanzahl überschritten" @@ -962,8 +889,8 @@ cropImageAsk: "Möchtest du das Bild zuschneiden?" cropYes: "Zuschneiden" cropNo: "Unbearbeitet verwenden" file: "Datei" -recentNHours: "Letzte {n} Stunden" -recentNDays: "Letzte {n} Tage" +recentNHours: "Letzten {n} Stunden" +recentNDays: "Letzten {n} Tage" noEmailServerWarning: "Es ist kein Email-Server konfiguriert." thereIsUnresolvedAbuseReportWarning: "Es liegen ungelöste Meldungen vor." recommended: "Empfehlung" @@ -971,14 +898,13 @@ check: "Check" driveCapOverrideLabel: "Die Drive-Kapazität dieses Nutzers verändern" driveCapOverrideCaption: "Gib einen Wert von 0 oder weniger ein, um die Kapazität auf den Standard zurückzusetzen." requireAdminForView: "Melde dich mit einem Administratorkonto an, um dies einzusehen." -isSystemAccount: "Ein Benutzerkonto, das durch das System erstellt und automatisch verwaltet wird." +isSystemAccount: "Ein Benutzerkonto, dass durch das System erstellt und automatisch kontrolliert wird." typeToConfirm: "Bitte gib zur Bestätigung {x} ein" deleteAccount: "Benutzerkonto löschen" document: "Dokumentation" numberOfPageCache: "Seitencachegröße" -numberOfPageCacheDescription: "Das Erhöhen dieses Caches führt zu einer angenehmerern Benutzererfahrung, aber erhöht Last und Arbeitsspeicherauslastung auf dem Nutzergerät." +numberOfPageCacheDescription: "Das Erhöhen dieses Caches führt zu einer angenehmerern Benutzererfahrung, erhöht aber Serverlast und Arbeitsspeicherauslastung." logoutConfirm: "Wirklich abmelden?" -logoutWillClearClientData: "Beim Abmelden werden die Konfigurationsdaten des Clients aus dem Browser gelöscht. Um sicherzustellen, dass die Konfigurationsdaten beim erneuten Einloggen wiederhergestellt werden können, aktivieren Sie bitte die automatische Sicherung der Konfiguration." lastActiveDate: "Zuletzt verwendet am" statusbar: "Statusleiste" pleaseSelect: "Wähle eine Option" @@ -1028,7 +954,6 @@ neverShow: "Nicht wieder anzeigen" remindMeLater: "Vielleicht später" didYouLikeMisskey: "Gefällt dir Misskey?" pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!" -correspondingSourceIsAvailable: "Der entsprechende Quellcode ist verfügbar unter {anchor}" roles: "Rollen" role: "Rolle" noRole: "Rolle nicht gefunden" @@ -1038,7 +963,6 @@ assign: "Zuweisen" unassign: "Entfernen" color: "Farbe" manageCustomEmojis: "Kann benutzerdefinierte Emojis verwalten" -manageAvatarDecorations: "Profilbilddekorationen verwalten" youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht." cannotPerformTemporary: "Vorübergehend nicht verfügbar" cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes des Ausführungslimits temporär nicht verfügbar. Bitte versuche es nach einiger Zeit erneut." @@ -1056,7 +980,6 @@ thisPostMayBeAnnoyingHome: "Zur Startseite schicken" thisPostMayBeAnnoyingCancel: "Abbrechen" thisPostMayBeAnnoyingIgnore: "Trotzdem schicken" collapseRenotes: "Bereits gesehene Renotes verkürzt anzeigen" -collapseRenotesDescription: "Klappe Notizen ein, auf die du bereits reagiert oder die du renoted hast." internalServerError: "Serverinterner Fehler" internalServerErrorDescription: "Im Server ist ein unerwarteter Fehler aufgetreten." copyErrorInfo: "Fehlerdetails kopieren" @@ -1080,11 +1003,6 @@ resetPasswordConfirm: "Wirklich Passwort zurücksetzen?" sensitiveWords: "Sensible Wörter" sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden." sensitiveWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden." -prohibitedWords: "Verbotene Wörter" -prohibitedWordsDescription: "Aktiviert eine Fehlermeldung, wenn versucht wird, eine Notiz zu veröffentlichen, die das/die eingestellte(n) Wort(e) enthält. Mehrere Begriffe können durch Zeilenumbrüche getrennt festgelegt werden." -prohibitedWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden." -hiddenTags: "Ausgeblendete Hashtags" -hiddenTagsDescription: "Die hier eingestellten Tags werden nicht mehr in den Trends angezeigt. Mit der Umschalttaste können mehrere ausgewählt werden." notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar." license: "Lizenz" unfavoriteConfirm: "Wirklich aus Favoriten entfernen?" @@ -1095,15 +1013,11 @@ retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?" retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen." enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen" enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen" -enableStatsForFederatedInstances: "Abruf von Informationen über förderierte Server" showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen" -reactionsDisplaySize: "Reaktionsanzeigegröße" -limitWidthOfReaction: "Begrenze die Breite der Reaktion und zeige sie verkleinert an" +largeNoteReactions: "Reaktionen vergrößert anzeigen" noteIdOrUrl: "Notiz-ID oder URL" video: "Video" videos: "Videos" -audio: "Audio" -audioFiles: "Audio" dataSaver: "Datensparmodus" accountMigration: "Kontomigration" accountMoved: "Dieser Benutzer ist zu einem neuen Konto migriert:" @@ -1124,28 +1038,23 @@ vertical: "Vertikal" horizontal: "Horizontal" position: "Position" serverRules: "Serverregeln" -pleaseConfirmBelowBeforeSignup: "Lies bitte diese Informationen und stimme ihnen vor der Registration zu." +pleaseConfirmBelowBeforeSignup: "Lies bitte Untenstehendes vor der Registration." pleaseAgreeAllToContinue: "Zum Fortfahren muss allen obigen Feldern zugestimmt werden." continue: "Fortfahren" preservedUsernames: "Reservierte Benutzernamen" preservedUsernamesDescription: "Gib zu reservierende Benutzernamen durch Zeilenumbrüche getrennt an. Diese werden für die Registrierung gesperrt, können aber von Administratoren zur manuellen Erstellung von Konten verwendet werden. Existierende Konten, die diese Namen bereits verwenden, werden nicht beeinträchtigt." createNoteFromTheFile: "Notiz für diese Datei schreiben" archive: "Archivieren" -archived: "Archiviert" -unarchive: "Dearchivieren" channelArchiveConfirmTitle: "{name} wirklich archivieren?" channelArchiveConfirmDescription: "Ein archivierter Kanal taucht nicht mehr in der Kanalliste oder in Suchergebnissen auf. Zudem können ihm keine Beiträge mehr hinzugefügt werden." thisChannelArchived: "Dieser Kanal wurde archiviert." -displayOfNote: "Darstellung von Notizen" +displayOfNote: "Anzeige von Notizen" initialAccountSetting: "Kontoeinrichtung" youFollowing: "Gefolgt" preventAiLearning: "Verwendung in machinellem Lernen (Generative bzw. Prediktive AI/KI) ablehnen" preventAiLearningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (Generative bzw. Prediktive AI/KI) zu verwenden. Dies wird durch das Hinzufügen einer \"noai\"-Flag in der HTML-Antwort des jeweiligen Inhalts erreicht. Da diese Flag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich." options: "Optionen" specifyUser: "Spezifischer Benutzer" -lookupConfirm: "Bist du sicher, dass du das nachschlagen möchtest?" -openTagPageConfirm: "Hashtag Seite wirklich öffnen?" -specifyHost: "Host" failedToPreviewUrl: "Vorschau nicht anzeigbar" update: "Aktualisieren" rolesThatCanBeUsedThisEmojiAsReaction: "Rollen, die dieses Emoji als Reaktion verwenden können" @@ -1161,344 +1070,6 @@ branding: "Branding" enableServerMachineStats: "Hardwareinformationen des Servers veröffentlichen" enableIdenticonGeneration: "Generierung von Benutzer-Identicons aktivieren" turnOffToImprovePerformance: "Deaktivierung kann zu höherer Leistung führen." -createInviteCode: "Einladung erstellen" -createWithOptions: "Einladung mit Optionen erstellen" -createCount: "Einladungsanzahl" -inviteCodeCreated: "Einladung erstellt" -inviteLimitExceeded: "Du hast das Maximum an erstellbaren Einladungen erreicht." -createLimitRemaining: "Erstellbare Einladungen: Noch {limit}" -inviteLimitResetCycle: "Am {time} wird dies auf {limit} zurückgesetzt." -expirationDate: "Ablaufdatum" -noExpirationDate: "Keins" -inviteCodeUsedAt: "Einladung verwendet am" -registeredUserUsingInviteCode: "Einladung verwendet von" -waitingForMailAuth: "Bestätigungsemail ausstehend" -inviteCodeCreator: "Einladung erstellt von" -usedAt: "Benutzt am" -unused: "Unbenutzt" -used: "Benutzt" -expired: "Abgelaufen" -doYouAgree: "Zustimmen?" -beSureToReadThisAsItIsImportant: "Lies bitte diese wichtige Informationen." -iHaveReadXCarefullyAndAgree: "Ich habe den Text \"{x}\" gelesen und stimme zu." -dialog: "Dialogfeld" -icon: "Symbol" -forYou: "Für dich" -currentAnnouncements: "Aktuelle Ankündigungen" -pastAnnouncements: "Alte Ankündigungen" -youHaveUnreadAnnouncements: "Es gibt neue Ankündigungen." -useSecurityKey: "Folge bitten den Anweisungen deines Browsers bzw. Gerätes und verwende deinen Hardware-Sicherheitsschlüssel oder Passkey." -replies: "Antworten" -renotes: "Renotes" -loadReplies: "Antworten anzeigen" -loadConversation: "Unterhaltung anzeigen" -pinnedList: "Angeheftete Liste" -keepScreenOn: "Bildschirm angeschaltet lassen" -verifiedLink: "Link-Besitz wurde verifiziert" -notifyNotes: "Über neue Notizen benachrichtigen" -unnotifyNotes: "Nicht über neue Notizen benachrichtigen" -authentication: "Authentifikation" -authenticationRequiredToContinue: "Bitte authentifiziere dich, um fortzufahren" -dateAndTime: "Zeit" -showRenotes: "Renotes anzeigen" -edited: "Bearbeitet" -notificationRecieveConfig: "Benachrichtigungseinstellungen" -mutualFollow: "Gegenseitig gefolgt" -followingOrFollower: "Follow oder Follower" -fileAttachedOnly: "Nur Notizen mit Dateien" -showRepliesToOthersInTimeline: "Antworten in Chronik anzeigen" -hideRepliesToOthersInTimeline: "Antworten nicht in Chronik anzeigen" -showRepliesToOthersInTimelineAll: "Antworten von allen momentan gefolgten Benutzern in Chronik anzeigen" -hideRepliesToOthersInTimelineAll: "Antworten von allen momentan gefolgten Benutzern nicht in Chronik anzeigen" -confirmShowRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern in der Chronik anzeigen?" -confirmHideRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern nicht in der Chronik anzeigen?" -externalServices: "Externe Dienste" -sourceCode: "Quellcode" -sourceCodeIsNotYetProvided: "Der Quellcode ist noch nicht verfügbar. Kontaktiere den Administrator, um das Problem zu lösen." -repositoryUrl: "Repository URL" -repositoryUrlDescription: "Solltest du Misskey so wie es ist verwenden (im unveränderten Quellcode), gebe Folgendes an:\nhttps://github.com/misskey-dev/misskey" -repositoryUrlOrTarballRequired: "Wenn du kein Repository veröffentlicht hast, musst du stattdessen einen Tarball bereitstellen. Siehe .config/example.yml für weitere Informationen." -feedback: "Feedback" -feedbackUrl: "Feedback-Website" -impressum: "Impressum" -impressumUrl: "Impressums-URL" -impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, ist die Angabe von Betreiberinformationen (ein Impressum) bei kommerziellem Betrieb zwingend." -privacyPolicy: "Datenschutzerklärung" -privacyPolicyUrl: "Datenschutzerklärungs-URL" -tosAndPrivacyPolicy: "Nutzungsbedingungen und Datenschutzerklärung" -avatarDecorations: "Profilbilddekoration" -attach: "Anbringen" -detach: "Entfernen" -detachAll: "Alles Entfernen" -angle: "Winkel" -flip: "Umdrehen" -showAvatarDecorations: "Profilbilddekoration anzeigen" -releaseToRefresh: "Zum Aktualisieren loslassen" -refreshing: "Wird aktualisiert..." -pullDownToRefresh: "Zum Aktualisieren ziehen" -useGroupedNotifications: "Benachrichtigungen gruppieren" -signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen. Der Link könnte abgelaufen sein." -cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden." -doReaction: "Reagieren" -code: "Code" -reloadRequiredToApplySettings: "Eine Aktualisierung ist erforderlich, um die Einstellungen zu übernehmen." -remainingN: "Verbleibend: {n}" -overwriteContentConfirm: "Bist du sicher, dass du den aktuellen Inhalt überschreiben willst?" -seasonalScreenEffect: "Saisonaler Bildschirmeffekt" -decorate: "Dekorieren" -addMfmFunction: "MFM hinzufügen" -enableQuickAddMfmFunction: "Erweiterte MFM-Auswahl anzeigen" -bubbleGame: "Bubble Game" -sfx: "Soundeffekte" -soundWillBePlayed: "Es wird Ton wiedergegeben" -showReplay: "Wiederholung anzeigen" -replay: "Aufzeichnen" -replaying: "Aufzeichnung" -endReplay: "Aufzeichnung verlassen" -copyReplayData: "Aufzeichnung kopieren" -ranking: "Rangliste" -lastNDays: "Letzte {n} Tage" -backToTitle: "Zurück zum Startbildschirm" -hemisphere: "Hemisphäre" -withSensitive: "Zeige \"sensitive Inhalte\" an" -userSaysSomethingSensitive: "{name} sagt etwas mit sensiblem Inhalt." -enableHorizontalSwipe: "Wischen, um zwischen Tabs zu wechseln" -loading: "Laden" -surrender: "Abbrechen" -gameRetry: "Erneut versuchen" -notUsePleaseLeaveBlank: "Leer lassen, wenn nicht verwendet" -useTotp: "Gib das Einmalpasswort ein" -useBackupCode: "Verwende die Backup-Codes" -launchApp: "Starte die App" -useNativeUIForVideoAudioPlayer: "Browser-Benutzeroberfläche für die Video- und Audiowiedergabe verwenden" -keepOriginalFilename: "Ursprünglichen Dateinamen beibehalten" -keepOriginalFilenameDescription: "Wenn diese Einstellung deaktiviert ist, wird der Dateiname beim Hochladen automatisch durch eine zufällige Zeichenfolge ersetzt." -noDescription: "Keine Beschreibung vorhanden" -alwaysConfirmFollow: "Folgen immer bestätigen" -inquiry: "Kontakt" -tryAgain: "Bitte später erneut versuchen" -confirmWhenRevealingSensitiveMedia: "Das Anzeigen von sensiblen Medien bestätigen" -sensitiveMediaRevealConfirm: "Es könnte sich um sensible Medien handeln. Möchtest du sie anzeigen?" -createdLists: "Erstellte Listen" -createdAntennas: "Erstellte Antennen" -fromX: "Von {x}" -genEmbedCode: "Einbettungscode generieren" -noteOfThisUser: "Notizen dieses Benutzers" -clipNoteLimitExceeded: "Zu diesem Clip können keine weiteren Notizen hinzugefügt werden." -performance: "Leistung" -modified: "Bearbeitet" -discard: "Verwerfen" -thereAreNChanges: "Es gibt {n} Änderung(en)" -signinWithPasskey: "Mit Passkey anmelden" -unknownWebAuthnKey: "Unbekannter Passkey" -passkeyVerificationFailed: "Die Passkey-Verifizierung ist fehlgeschlagen." -passkeyVerificationSucceededButPasswordlessLoginDisabled: "Die Verifizierung des Passkeys war erfolgreich, aber die passwortlose Anmeldung ist deaktiviert." -messageToFollower: "Nachricht an die Follower" -target: "Speicherort" -testCaptchaWarning: "Diese Funktion ist für CAPTCHA-Testzwecke gedacht.\nNicht in einer Produktivumgebung verwenden." -prohibitedWordsForNameOfUser: "Verbotene Begriffe für Benutzernamen" -prohibitedWordsForNameOfUserDescription: "Wenn eine Zeichenfolge aus dieser Liste im Namen eines Benutzers enthalten ist, wird der Benutzername abgelehnt. Benutzer mit Moderatorenrechten sind von dieser Einschränkung nicht betroffen." -yourNameContainsProhibitedWords: "Dein Name enthält einen verbotenen Begriff" -yourNameContainsProhibitedWordsDescription: "Der Name enthält eine verbotene Zeichenfolge. Wende dich an deinen Serveradministrator, wenn du diesen Namen verwenden möchtest." -thisContentsAreMarkedAsSigninRequiredByAuthor: "Logge dich ein, um weitere Inhalte von diesem Nutzer zu sehen." -lockdown: "Sperren" -pleaseSelectAccount: "Bitte Konto auswählen" -availableRoles: "Verfügbare Rollen" -acknowledgeNotesAndEnable: "Schalten Sie dies erst ein, wenn Sie die Vorsichtsmaßnahmen verstanden haben." -federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren." -federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren." -confirmOnReact: "Reagieren bestätigen" -reactAreYouSure: "Willst du eine \"{emoji}\"-Reaktion hinzufügen?" -markAsSensitiveConfirm: "Möchtest du dieses Medium als sensibel kennzeichnen?" -unmarkAsSensitiveConfirm: "Möchtest du die Kennzeichnung dieses Mediums als sensibel aufheben?" -preferences: "Einstellungen" -accessibility: "Eingabehilfe" -preferencesProfile: "Einstellungsprofil" -copyPreferenceId: "Kopiere die Einstellungs-ID" -resetToDefaultValue: "Auf Standard zurücksetzen" -overrideByAccount: "Überschreibung durch das Konto" -untitled: "Unbenannt" -noName: "Kein Name" -skip: "Überspringen" -restore: "Wiederherstellen" -syncBetweenDevices: "Zwischen Geräten synchronisieren" -preferenceSyncConflictTitle: "Der konfigurierte Wert ist auf dem Server bereits vorhanden." -preferenceSyncConflictText: "Die Einstellungen mit aktivierter Synchronisierung werden ihre Werte auf dem Server speichern. Es gibt jedoch bereits Werte auf dem Server. Welche Einstellungswerte sollen überschrieben werden?" -preferenceSyncConflictChoiceServer: "Konfigurierte Werte auf dem Server" -preferenceSyncConflictChoiceDevice: "Konfigurierte Werte auf dem Gerät" -preferenceSyncConflictChoiceCancel: "Einrichten der Synchronisierung abbrechen" -paste: "Einfügen" -emojiPalette: "Emoji-Palette" -postForm: "Notizfenster" -textCount: "Zeichenanzahl" -information: "Über" -chat: "Chat" -migrateOldSettings: "Alte Client-Einstellungen migrieren" -migrateOldSettings_description: "Dies sollte normalerweise automatisch geschehen, aber wenn die Migration aus irgendeinem Grund nicht erfolgreich war, kannst du den Migrationsprozess selbst manuell auslösen. Die aktuellen Konfigurationsinformationen werden dabei überschrieben." -compress: "Komprimieren" -right: "Rechts" -bottom: "Unten" -top: "Oben" -embed: "Einbetten" -settingsMigrating: "Ihre Einstellungen werden gerade migriert, Bitte warten Sie einen Moment... (Sie können die Einstellungen später auch manuell migrieren, indem Sie zu Einstellungen → Sonstiges → Alte Einstellungen migrieren gehen)" -readonly: "Nur Lesezugriff" -goToDeck: "Zurück zum Deck" -federationJobs: "Föderation Jobs" -driveAboutTip: "In Drive sehen Sie eine Liste der Dateien, die Sie in der Vergangenheit hochgeladen haben.
\nSie können diese Dateien wiederverwenden um sie zu beispiel an Notizen anzuhängen, oder sie können Dateien vorab hochzuladen, um sie später zu versenden!
\nWenn Sie eine Datei löschen, verschwindet sie auch von allen Stellen, an denen Sie sie verwendet haben (Notizen, Seiten, Avatare, Banner usw.).
\nSie können auch Ordner erstellen, um sie zu organisieren." -scrollToClose: "Zum Schließen scrollen" -_chat: - noMessagesYet: "Noch keine Nachrichten" - newMessage: "Neue Nachricht" - individualChat: "Privater Chat" - individualChat_description: "Führe einen privaten Chat mit einer anderen Person." - roomChat: "Chatraum" - roomChat_description: "Ein Chat-Raum, an dem mehrere Personen teilnehmen können.\nDu kannst auch Personen einladen, die keine privaten Chats zulassen, wenn sie die Einladung annehmen." - createRoom: "Raum erstellen" - inviteUserToChat: "Lade Benutzer ein, um mit dem Chatten zu beginnen" - yourRooms: "Erstellte Räume" - joiningRooms: "Raum beitreten" - invitations: "Einladen" - noInvitations: "Keine Einladungen" - history: "Verlauf" - noHistory: "Kein Verlauf gefunden" - noRooms: "Keine Räume gefunden" - inviteUser: "Benutzer einladen" - sentInvitations: "Verschickte Einladungen" - join: "Beitreten" - ignore: "Ignorieren" - leave: "Raum verlassen" - members: "Mitglieder" - searchMessages: "Nachrichten suchen" - home: "Startseite" - send: "Senden" - newline: "Neue Zeile" - muteThisRoom: "Raum stummschalten" - deleteRoom: "Raum löschen" - chatNotAvailableForThisAccountOrServer: "Der Chat ist auf diesem Server oder für dieses Konto nicht aktiviert." - chatIsReadOnlyForThisAccountOrServer: "Der Chat ist auf dieser Instanz oder diesem Konto nur zum Lesen freigegeben. Es ist nicht möglich, neue Nachrichten zu schreiben oder Chaträume zu erstellen oder zu betreten." - chatNotAvailableInOtherAccount: "Die Chatfunktion wurde vom anderen Benutzer deaktiviert." - cannotChatWithTheUser: "Starten eines Chats mit diesem Benutzer nicht möglich" - cannotChatWithTheUser_description: "Der Chat ist entweder nicht verfügbar oder die andere Seite hat den Chat nicht aktiviert." - chatWithThisUser: "Mit dem Benutzer chatten" - thisUserAllowsChatOnlyFromFollowers: "Dieser Benutzer nimmt nur Chats von Followern an." - thisUserAllowsChatOnlyFromFollowing: "Dieser Benutzer nimmt nur Chats von Benutzern an, denen er folgt." - thisUserAllowsChatOnlyFromMutualFollowing: "Dieser Benutzer akzeptiert nur Chats von Benutzern, die sich gegenseitig folgen." - thisUserNotAllowedChatAnyone: "Dieser Benutzer nimmt keine Chats von anderen Benutzern an." - chatAllowedUsers: "Wem das Chatten erlaubt werden soll" - chatAllowedUsers_note: "Du kannst unabhängig von dieser Einstellung mit allen Personen chatten, denen du eine Chat-Nachricht gesendet hast." - _chatAllowedUsers: - everyone: "Jeder" - followers: "Nur deine Follower" - following: "Nur Benutzer, denen du folgst" - mutual: "Nur Benutzer, die sich gegenseitig folgen" - none: "Niemand" -_emojiPalette: - palettes: "Palette" - enableSyncBetweenDevicesForPalettes: "Synchronisierung der Paletten zwischen Geräten aktivieren" - paletteForMain: "Hauptpalette" - paletteForReaction: "Reaktions-Palette" -_settings: - driveBanner: "Du kannst den Drive verwalten und konfigurieren, die Auslastung überprüfen und Einstellungen für das Hochladen von Dateien vornehmen." - pluginBanner: "Du kannst die Funktionen des Clients mit Plugins erweitern. Plugins können installiert, individuell konfiguriert und verwaltet werden." - notificationsBanner: "Sie können die Arten und den Umfang der Benachrichtigungen vom Server und der Push- Mitteilungen konfigurieren." - api: "API" - webhook: "Webhook" - serviceConnection: "Integrierte Dienste" - serviceConnectionBanner: "Du kannst Zugriffstoken und Webhooks für die Integration mit externen Anwendungen und Diensten verwalten und konfigurieren." - accountData: "Kontodaten" - accountDataBanner: "Export/Import und Verwaltung von Kontodatenarchiven." - muteAndBlockBanner: "Du kannst Einstellungen konfigurieren und verwalten, um Inhalte auszublenden und Aktionen für bestimmte Benutzer zu beschränken." - accessibilityBanner: "Die Clients können personalisiert und für eine optimale Nutzung im Hinblick auf ihre Darstellung und ihr Verhalten eingerichtet werden." - privacyBanner: "Du kannst Einstellungen für die Privatsphäre deines Kontos vornehmen, z. B. inwieweit Inhalte veröffentlicht werden, wie leicht sie zu finden sind und ob Follower genehmigt werden müssen." - securityBanner: "Du kannst Einstellungen für die Kontosicherheit konfigurieren, z. B. Passwörter, Anmeldemethoden, Authentifizierungs-Apps und Passkeys." - preferencesBanner: "Sie können das Gesamtverhalten des Clients nach Ihren Wünschen konfigurieren." - appearanceBanner: "Du kannst das Erscheinungsbild und die Anzeigeeinstellungen für den Client nach deinen Wünschen konfigurieren." - soundsBanner: "Du kannst die Einstellungen für die Wiedergabe von Klängen im Client konfigurieren." - timelineAndNote: "Chroniken und Notizen" - makeEveryTextElementsSelectable: "Alle Textelemente auswählbar machen" - makeEveryTextElementsSelectable_description: "Die Aktivierung kann in manchen Situationen die Benutzerfreundlichkeit beeinträchtigen." - useStickyIcons: "Icons beim Scrollen folgen lassen" - showNavbarSubButtons: "Unterschaltflächen in der Navigationsleiste anzeigen" - ifOn: "Wenn eingeschaltet" - ifOff: "Wenn ausgeschaltet" - enableSyncThemesBetweenDevices: "Synchronisierung von installierten Themen auf verschiedenen Endgeräten" - enablePullToRefresh: "Ziehen zum Aktualisieren" - enablePullToRefresh_description: "Bei Benutzung einer Maus, mit gedrücktem Mausrad ziehen" - _chat: - showSenderName: "Name des Absenders anzeigen" - sendOnEnter: "Eingabetaste sendet Nachricht" -_preferencesProfile: - profileName: "Profilname" - profileNameDescription: "Lege einen Namen fest, der dieses Gerät identifiziert." - profileNameDescription2: "Beispiel: \"Haupt-PC\", \"Smartphone\"" - manageProfiles: "Profile verwalten" -_preferencesBackup: - autoBackup: "Automatische Sicherung" - restoreFromBackup: "Wiederherstellen aus der Sicherung" - noBackupsFoundTitle: "Keine Sicherungen gefunden" - noBackupsFoundDescription: "Es wurden keine automatisch erstellten Sicherungen gefunden, aber wenn du eine Sicherungsdatei manuell gespeichert hast, kannst du diese importieren und wiederherstellen." - selectBackupToRestore: "Wähle die wiederherzustellende Sicherung" - youNeedToNameYourProfileToEnableAutoBackup: "Um die automatische Sicherung zu aktivieren, müssen Profilnamen festgelegt werden." - autoPreferencesBackupIsNotEnabledForThisDevice: "Die automatische Sicherung der Einstellungen ist auf diesem Gerät nicht aktiviert." - backupFound: "Konfigurationssicherung gefunden." -_accountSettings: - requireSigninToViewContents: "Anmeldung erfordern, um Inhalte anzuzeigen" - requireSigninToViewContentsDescription1: "Erfordere eine Anmeldung, um alle Notizen und andere Inhalte anzuzeigen, die du erstellt hast. Dadurch wird verhindert, dass Crawler deine Informationen sammeln." - requireSigninToViewContentsDescription2: "Der Inhalt wird nicht in URL-Vorschauen (OGP), eingebettet in Webseiten oder auf Servern, die keine Zitate unterstützen, angezeigt." - requireSigninToViewContentsDescription3: "Diese Einschränkungen gelten möglicherweise nicht für föderierte Inhalte von anderen Servern." - makeNotesFollowersOnlyBefore: "Macht frühere Notizen nur für Follower sichtbar" - makeNotesFollowersOnlyBeforeDescription: "Solange diese Funktion aktiviert ist, sind Notizen, die nach dem eingestellten Datum und der eingestellten Zeit liegen oder die eingestellte Zeit abgelaufen ist, nur für Follower sichtbar. Bei Deaktivierung wird auch der öffentliche Status der Notiz wiederhergestellt." - makeNotesHiddenBefore: "Frühere Notizen privat machen" - makeNotesHiddenBeforeDescription: "" - mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden." - mayNotEffectSomeSituations: "Diese Einschränkungen sind vereinfacht. Sie gelten möglicherweise nicht in allen Situationen, z. B. bei der Anzeige auf einem fremden Server oder während der Moderation." - notesHavePassedSpecifiedPeriod: "Notizen die nach der folgenden Zeit veröffentlicht worden" - notesOlderThanSpecifiedDateAndTime: "Notizen vor einem bestimmtem Datum und Uhrzeit" -_abuseUserReport: - forward: "Weiterleiten" - forwardDescription: "Leite die Meldung an einen entfernten Server als anonymes Systemkonto weiter." - resolve: "lösen" - accept: "Akzeptieren" - reject: "Ablehnen" - resolveTutorial: "Wenn der Inhalt der Meldung rechtmäßig ist, wähle „Akzeptieren“, um sie als gelöst zu markieren.\nWenn der Inhalt der Meldung unzulässig ist, wähle „Ablehnen“, um sie zu ignorieren." -_delivery: - status: "Auslieferungsstatus" - stop: "Gesperrt" - resume: "Zustellung wieder fortsetzen" - _type: - none: "Wird veröffentlicht" - manuallySuspended: "Manuell gesperrt" - goneSuspended: "Gesperrt wegen Löschung des Servers" - autoSuspendedForNotResponding: "Gesperrt, weil der Server nicht antwortet" - softwareSuspended: "Ausgesetzt, weil die Software nicht mehr beliefert wird" -_bubbleGame: - howToPlay: "Wie man spielt" - hold: "Halten" - _score: - score: "Spielstand" - scoreYen: "Verdienter Geldbetrag" - highScore: "Höchstpunktzahl" - maxChain: "Maximale Anzahl an Verkettungen" - yen: "{yen} Yen" - estimatedQty: "{qty} Stück" - scoreSweets: "{onigiriQtyWithUnit} Onigiri" - _howToPlay: - section1: "Passe die Position an und lasse das Objekt in das Spielfeld fallen." - section2: "Wenn sich zwei Objekte der gleichen Art berühren, verwandeln sie sich in ein anderes Objekt und du bekommst Punkte." - section3: "Das Spiel ist vorbei, wenn die Objekte aus dem Spielfeld herausragen. Versuche eine hohe Punktzahl zu erreichen, indem du die Objekte miteinander verschmelzt, ohne dass das Spielfeld überläuft!" -_announcement: - forExistingUsers: "Nur für existierende Nutzer" - forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." - needConfirmationToRead: "Separate Lesebestätigung erfordern" - needConfirmationToReadDescription: "Ist dies aktiviert, so wird beim Markieren dieser Ankündigung als gelesen ein separates Bestätigungsfenster angezeigt. Auch wird sie von der \"Alle als gelesen markieren\"-Funktion ausgenommen." - end: "Ankündigung archivieren" - tooManyActiveAnnouncementDescription: "Zu viele aktive Ankündigungen können die Benutzerfreundlichkeit verschlechtern. Es wird empfohlen, veraltete Ankündigungen zu archivieren." - readConfirmTitle: "Als gelesen markieren?" - readConfirmText: "Dies markiert den Inhalt von \"{title}\" als gelesen." - shouldNotBeUsedToPresentPermanentInfo: "Es wird empfohlen, Ankündigungen für aktuelle und zeitlich begrenzte Neuigkeiten zu nutzen, statt für Informationen, die langfristig relevant sind." - dialogAnnouncementUxWarn: "Bei der Verwendung von mehr als zwei Meldungen im Dialog-Format wird um Vorsicht geboten, da dies negative Auswirkungen auf die UX haben kann." - silence: "Keine Benachrichtigung" - silenceDescription: "Wenn aktiviert, gibt diese Meldung keine Nachricht aus und muss nicht als \"gelesen\" markiert werden." _initialAccountSetting: accountCreated: "Dein Konto wurde erfolgreich erstellt!" letsStartAccountSetup: "Lass uns nun dein Konto einrichten." @@ -1511,99 +1082,11 @@ _initialAccountSetting: pushNotificationDescription: "Durch die Aktivierung von Push-Benachrichtigungen kannst du von {name} Benachrichtigungen direkt auf dein Gerät erhalten." initialAccountSettingCompleted: "Kontoeinrichtung abgeschlossen!" haveFun: "Viel Spaß mit {name}!" - youCanContinueTutorial: "Du kannst mit dem Tutorial von {name}(Misskey) fortfahren, oder auch abbrechen und gleich anfangen Misskey zu benutzen." - startTutorial: "Fange mit dem Tutorial an" + ifYouNeedLearnMore: "Besuche {link}, falls du mehr über {name} (Misskey) lernen möchtest." skipAreYouSure: "Die Kontoeinrichtung wirklich überspringen?" laterAreYouSure: "Die Kontoeinrichtung wirklich später erledigen?" -_initialTutorial: - launchTutorial: "Tutorial ansehen" - title: "Tutorial" - wellDone: "Gut gemacht!" - skipAreYouSure: "Möchtest du das Tutorial verlassen?" - _landing: - title: "Willkommen zum Tutorial" - description: "Hier kannst du sehen, wie Misskey funktioniert" - _note: - title: "Was sind Notizen?" - description: "Beiträge auf Misskey heißen \"Notizen\". Notizen werden chronologisch in der Chronik angeordnet und in Echtzeit aktualisiert." - reply: "Klicke auf diesen Button, um auf eine Nachricht zu antworten. Es ist auch möglich, auf Antworten zu antworten und die Unterhaltung wie einen Thread fortzusetzen." - renote: "Du kannst diese Notiz in deiner eigenen Chronik teilen. Du kannst sie auch mit deinen Kommentaren zitieren." - reaction: "Du kannst der Notiz Reaktionen hinzufügen. Weitere Einzelheiten werden auf der nächsten Seite erläutert." - menu: "Du kannst Details zu Notizen anzeigen, Links kopieren und verschiedene andere Aktionen durchführen." - _reaction: - title: "Was sind Reaktionen?" - description: "Auf Notizen kann mit verschiedenen Emojis reagiert werden. Reaktionen ermöglichen es dir, Nuancen auszudrücken, die mit einem einfachen „Gefällt mir“ vielleicht nicht ausgedrückt werden können." - letsTryReacting: "Reaktionen können durch Klicken auf die Schaltfläche „+“ in der Notiz hinzugefügt werden. Versuche, auf diese Beispielnotiz zu reagieren!" - reactToContinue: "Füge eine Reaktion hinzu, um fortzufahren." - reactNotification: "Du erhältst Echtzeit-Benachrichtigungen, wenn jemand auf deine Notiz reagiert." - reactDone: "Du kannst eine Reaktion zurücknehmen, indem du auf den '-' Button drückst." - _timeline: - title: "So funktionieren die Chroniken" - description1: "Misskey stellt mehrere Chroniken bereit (einige können je nach den Richtlinien des Servers nicht verfügbar sein)." - home: "Du kannst Beiträge von den Konten sehen, denen du folgst." - local: "Du kannst Beiträge aller Benutzer auf diesem Server sehen." - social: "Notizen von der Startseite und der lokalen Chronik werden angezeigt." - global: "Du kannst Notizen von allen föderierten Servern sehen." - description2: "Du kannst jederzeit am oberen Rand des Bildschirms zwischen den jeweiligen Chroniken wechseln." - description3: "Darüber hinaus gibt es Listen-Chroniken und Kanal-Chroniken. Weitere Einzelheiten findest du unter {link}." - _postNote: - title: "Optionen bei Abschicken einer Notiz" - description1: "Wenn du eine Notiz auf Misskey veröffentlichst, stehen dir verschiedene Optionen zur Verfügung. Die Oberfläche sieht folgendermaßen aus." - _visibility: - description: "Du kannst einschränken, wer deine Notiz sehen kann." - public: "Deine Notiz wird für alle Nutzer sichtbar sein." - home: "Nur auf der Startseite sichtbar. Kann von Followern, Profilbesuchern und durch Renotes gesehen werden." - followers: "Nur für Follower sichtbar. Nur Follower können es sehen und niemand sonst, und es kann nicht von anderen gerenoted werden." - direct: "Die Notiz wird nur für den angegebenen Benutzer veröffentlicht und der Empfänger wird benachrichtigt. Kann anstelle von Direktnachrichten verwendet werden." - doNotSendConfidencialOnDirect1: "Sei vorsichtig, wenn du sensible Informationen verschickst!" - doNotSendConfidencialOnDirect2: "Die Administratoren des Servers können den Inhalt der Notiz sehen. Sei vorsichtig mit sensiblen Informationen, wenn du Direktnachrichten an Benutzer auf nicht vertrauenswürdigen Servern sendest." - localOnly: "Wenn du eine Notiz mit dieser Einstellung veröffentlichst, wird sie nicht an andere Server weitergeleitet. Benutzer auf anderen Servern können diese Notizen nicht direkt sehen, unabhängig von den obigen Anzeigeeinstellungen." - _cw: - title: "Inhaltswarnung" - description: "Anstelle des Textes wird das angezeigt, was du im Abschnitt „Anmerkungen“ angibst. Drücke auf „Inhalt anzeigen“, um den vollständigen Text zu sehen." - _exampleNote: - cw: "Das wird dich bestimmt hungrig machen!" - note: "Ich hatte gerade einen Donut mit Schokoladenüberzug 🍩😋" - useCases: "Dient zur Kennzeichnung von Notizen, wie sie in den Serverrichtlinien vorgeschrieben sind, oder zur eigenen Festlegung von Spoiler-Beiträgen oder sensiblem Text." - _howToMakeAttachmentsSensitive: - title: "Wie markiert man Anhänge als sensibel?" - description: "Markiere Anhänge als sensibel, die aufgrund von den Serverregeln nicht sichtbar sein sollen." - tryThisFile: "Versuche, das angehängte Bild als sensibel zu markieren!" - _exampleNote: - note: "Ups, ich habe es vergeigt, den Natto-Deckel zu öffnen..." - method: "Um einen Anhang als sensibel zu kennzeichnen, klicke auf das Vorschaubild der Datei, um das Menü zu öffnen, und klicke auf „Als sensibel markieren“." - sensitiveSucceeded: "Wenn du Dateien anhängst, stelle bitte die Sensibilität entsprechend der Serverrichtlinien ein." - doItToContinue: "Markiere die angehängte Datei als sensibel, um fortzufahren." - _done: - title: "Du hast das Tutorial abgeschlossen! 🎉" - description: "Die hier beschriebenen Funktionen sind nur ein kleiner Teil dessen, was Misskey zu bieten hat; um mehr darüber zu erfahren, wie du Misskey benutzen kannst, besuche bitte {link}." -_timelineDescription: - home: "In der Startseiten-Chronik kannst du Notizen von Konten sehen, denen du folgst." - local: "In der lokalen Chronik siehst du Notizen von allen Benutzern auf diesem Server." - social: "Die soziale Chronik zeigt Notizen von der Startseite und der lokalen Chronik." - global: "In der globalen Chronik siehst du Notizen von allen föderierten Servern." _serverRules: description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen." -_serverSettings: - iconUrl: "Icon-URL" - appIconDescription: "Gibt das zu verwendende Icon bei der Anzeige von {host} als App an." - appIconUsageExample: "Beispielsweise als PWA, oder bei Lesezeichen auf dem Startbildschirm von Smartphones" - appIconStyleRecommendation: "Da das Icon zu einem Kreis oder Quadrat zugeschnitten wird, wird ein Icon mit gefülltem Margin um den Inhalt herum empfohlen." - appIconResolutionMustBe: "Die Mindestauflösung ist {resolution}." - manifestJsonOverride: "Überschreiben von manifest.json" - shortName: "Abkürzung" - shortNameDescription: "Ein Kürzel für den Namen der Instanz, der angezeigt werden kann, falls der volle Instanzname lang ist." - fanoutTimelineDescription: "Ist diese Option aktiviert, kann eine erhebliche Verbesserung im Abrufen von Chroniken und eine Reduzierung der Datenbankbelastung erzielt werden, im Gegenzug zu einer Steigerung in der Speichernutzung von Redis. Bei geringem Serverspeicher oder Serverinstabilität kann diese Option deaktiviert werden." - fanoutTimelineDbFallback: "Auf die Datenbank zurückfallen" - fanoutTimelineDbFallbackDescription: "Ist diese Option aktiviert, wird die Chronik auf zusätzliche Abfragen in der Datenbank zurückgreifen, wenn sich die Chronik nicht im Cache befindet. Eine Deaktivierung führt zu geringerer Serverlast, aber schränkt den Zeitraum der abrufbaren Chronik ein. " - reactionsBufferingDescription: "Wenn diese Option aktiviert ist, kann sie die Leistung beim Erstellen von Reaktionen erheblich verbessern und die Belastung der Datenbank verringern. Allerdings steigt die Speichernutzung von Redis." - inquiryUrl: "Kontakt-URL" - inquiryUrlDescription: "Gib eine URL für das Kontaktformular der Serverbetreiber oder eine Webseite an, die Kontaktinformationen enthält." - openRegistration: "Registrierung von Konten aktivieren" - openRegistrationWarning: "Das Aktivieren von Registrierungen ist riskant. Es wird empfohlen, sie nur dann zu aktivieren, wenn der Server ständig überwacht wird und im Falle eines Problems sofort reagiert werden kann." - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern." - deliverSuspendedSoftware: "Software, die nicht mehr beliefert wird" - deliverSuspendedSoftwareDescription: "Sie können eine Auswahl von Namen und Versionen verschiedener Serversoftware angeben, um die Zustellung zu stoppen, z. B. aufgrund von Sicherheitslücken. Diese Versionsinformationen werden vom Server bereitgestellt und ihre Zuverlässigkeit ist nicht garantiert. Es wird jedoch empfohlen, eine Vorabversion anzugeben, wie z. B. >= 2024.3.1-0, da die Angabe >= 2024.3.1 keine benutzerdefinierten Versionen wie 2024.3.1-custom.0 einschließt." _accountMigration: moveFrom: "Von einem anderen Konto zu diesem migrieren" moveFromSub: "Alias für ein anderes Konto erstellen" @@ -1858,19 +1341,6 @@ _achievements: title: "Brain Diver" description: "Sende den Link zu Brain Diver" flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "Testüberfluss" - description: "Betätige den Benachrichtigungstest mehrfach innerhalb einer extrem kurzen Zeitspanne" - _tutorialCompleted: - title: "Misskey Grundkurs-Diplom" - description: "Tutorial abgeschlossen" - _bubbleGameExplodingHead: - title: "🤯" - description: "Das größte Objekt im Bubble Game" - _bubbleGameDoubleExplodingHead: - title: "Doppel🤯" - description: "Zwei der größten Objekte im Bubble Game zur gleichen Zeit" - flavor: "Eine Lunchbox kann man auch mit etwas mehr 🤯 🤯 füllen" _role: new: "Rolle erstellen" edit: "Rolle bearbeiten" @@ -1881,9 +1351,7 @@ _role: assignTarget: "Zuweisungsart" descriptionOfAssignTarget: "Manuell bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\nKonditional bedeutet, dass die Liste der Benutzer einer Rolle durch eine Bedingung automatisch verwaltet wird." manual: "Manuell" - manualRoles: "Manuelle Rollen" conditional: "Konditional" - conditionalRoles: "Bedingte Rolle" condition: "Bedingung" isConditionalRole: "Dies ist eine konditionale Rolle." isPublic: "Öffentliche Rolle" @@ -1900,8 +1368,6 @@ _role: descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Chronik dieser Rolle, sowie eine Liste der Benutzer mit dieser Rolle, frei zugänglich." displayOrder: "Position" descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position." - preserveAssignmentOnMoveAccount: "Rolle übertragbar machen" - preserveAssignmentOnMoveAccount_description: "Wenn diese Option aktiviert ist, wird diese Rolle bei der Migration mit übertragen." canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten." priority: "Priorität" @@ -1913,46 +1379,25 @@ _role: gtlAvailable: "Kann auf die globale Chronik zugreifen" ltlAvailable: "Kann auf die lokale Chronik zugreifen" canPublicNote: "Kann öffentliche Notizen erstellen" - mentionMax: "Maximale Anzahl von Erwähnungen in einer Notiz" canInvite: "Erstellung von Einladungscodes für diese Instanz" - inviteLimit: "Maximalanzahl an Einladungen" - inviteLimitCycle: "Zyklus des Einladungslimits" - inviteExpirationTime: "Gültigkeitsdauer von Einladungen" canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten" - canManageAvatarDecorations: "Profilbilddekorationen verwalten" driveCapacity: "Drive-Kapazität" - maxFileSize: "Maximale Dateigröße, die hochgeladen werden kann" alwaysMarkNsfw: "Dateien immer als NSFW markieren" - canUpdateBioMedia: "Kann ein Profil- oder ein Bannerbild bearbeiten" pinMax: "Maximale Anzahl an angehefteten Notizen" antennaMax: "Maximale Anzahl an Antennen" wordMuteMax: "Maximale Zeichenlänge für Wortstummschaltungen" webhookMax: "Maximale Anzahl an Webhooks" clipMax: "Maximale Anzahl an Clips" noteEachClipsMax: "Maximale Anzahl an Notizen innerhalb eines Clips" - userListMax: "Maximale Anzahl an Benutzerlisten" - userEachUserListsMax: "Maximale Anzahl an Benutzern in einer Benutzerliste" + userListMax: "Maximale Anzahl an Benutzern in einer Benutzerliste" + userEachUserListsMax: "Maximale Anzahl an Benutzerlisten" rateLimitFactor: "Versuchsanzahl" descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." canHideAds: "Kann Werbung ausblenden" canSearchNotes: "Nutzung der Notizsuchfunktion" - canUseTranslator: "Verwendung des Übersetzers" - avatarDecorationLimit: "Maximale Anzahl an Profilbilddekorationen, die angebracht werden können" - canImportAntennas: "Importieren von Antennen erlauben" - canImportBlocking: "Importieren von Blockierungen zulassen" - canImportFollowing: "Importieren von Gefolgten zulassen" - canImportMuting: "Importieren von Stummgeschalteten zulassen" - canImportUserLists: "Importieren von Listen erlauben" - chatAvailability: "Chatten erlauben" _condition: - roleAssignedTo: "Manuellen Rollen zugewiesen" isLocal: "Lokaler Benutzer" isRemote: "Benutzer fremder Instanz" - isCat: "Katzen-Benutzer" - isBot: "Bot-Benutzer" - isSuspended: "Gesperrter Benutzer" - isLocked: "Private Konten" - isExplorable: "Benutzer, die ihr Konto im \"Erkunden\"-Bereich sichtbar machen" createdLessThan: "Kontoerstellung liegt weniger als X zurück" createdMoreThan: "Kontoerstellung liegt mehr als X zurück" followersLessThanOrEq: "Hat X oder weniger Follower" @@ -1978,7 +1423,6 @@ _emailUnavailable: disposable: "Wegwerf-Email-Adressen können nicht verwendet werden" mx: "Dieser Email-Server ist ungültig" smtp: "Dieser Email-Server antwortet nicht" - banned: "Du kannst dich mit dieser E-Mail-Adresse nicht registrieren" _ffVisibility: public: "Öffentlich" followers: "Nur für Follower sichtbar" @@ -1999,10 +1443,6 @@ _ad: reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen" hide: "Ausblenden" timezoneinfo: "Der Wochentag wird durch die Serverzeitzone bestimmt." - adsSettings: "Werbeeinstellungen" - notesPerOneAd: "Werbeintervall während Echtzeitaktualisierung (Notizen pro Werbung)" - setZeroToDisable: "Setze dies auf 0, um Werbung während Echtzeitaktualisierung zu deaktivieren" - adsTooClose: "Durch den momentan sehr niedrigen Werbeintervall kann es zu einer starken Verschlechterung der Benutzererfahrung kommen." _forgotPassword: enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst." ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator." @@ -2021,8 +1461,6 @@ _plugin: install: "Plugins installieren" installWarn: "Installiere bitte nur vertrauenswürdige Plugins." manage: "Plugins verwalten" - viewSource: "Quelltext anzeigen" - viewLog: "Protokoll anzeigen" _preferencesBackups: list: "Erstellte Backups" saveNew: "Neu erstellen" @@ -2052,13 +1490,10 @@ _aboutMisskey: contributors: "Hauptmitwirkende" allContributors: "Alle Mitwirkenden" source: "Quellcode" - original: "Original" - thisIsModifiedVersion: "{name} verwendet eine modifizierte Version des ursprünglichen Misskey." translation: "Misskey übersetzen" donate: "An Misskey spenden" morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰" patrons: "UnterstützerInnen" - projectMembers: "Projektmitglieder" _displayOfSensitiveMedia: respect: "Sensible Medien verbergen" ignore: "Sensible Medien anzeigen" @@ -2083,7 +1518,6 @@ _channel: notesCount: "{n} Notizen" nameAndDescription: "Name und Beschreibung" nameOnly: "Nur Name" - allowRenoteToExternal: "Renotes und Zitierungen außerhalb des Kanals erlauben" _menuDisplay: sideFull: "Seitlich" sideIcon: "Seitlich (Icons)" @@ -2093,6 +1527,11 @@ _wordMute: muteWords: "Stummgeschaltete Wörter" muteWordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen." muteWordsDescription2: "Umgib Schlüsselworter mit Schrägstrichen, um Reguläre Ausdrücke zu verwenden." + softDescription: "Notizen, die die angegebenen Konditionen erfüllen, in der Chronik ausblenden." + hardDescription: "Verhindern, dass Notizen, die die angegebenen Konditionen erfüllen, der Chronik hinzugefügt werden. Zudem werden diese Notizen auch nicht der Chronik hinzugefügt, falls die Konditionen geändert werden." + soft: "Leicht" + hard: "Schwer" + mutedNotes: "Stummgeschaltete Notizen" _instanceMute: instanceMuteDescription: "Schaltet alle Notizen/Renotes stumm, die von den gelisteten Instanzen stammen, inklusive Antworten von Benutzern an einen Benutzer einer stummgeschalteten Instanz." instanceMuteDescription2: "Instanzen getrennt durch Zeilenumbrüchen angeben" @@ -2107,7 +1546,6 @@ _theme: installed: "{name} wurde installiert" installedThemes: "Installierte Farbschemata" builtinThemes: "Eingebaute Farbschemata" - instanceTheme: "Server-Thema" alreadyInstalled: "Dieses Farbschema ist bereits installiert" invalid: "Der Code dieses Farbschemas ist ungültig" make: "Farbschema erstellen" @@ -2140,6 +1578,7 @@ _theme: header: "Kopfzeile" navBg: "Hintergrund der Seitenleiste" navFg: "Text der Seitenleiste" + navHoverFg: "Text der Seitenleiste (Mouseover)" navActive: "Text der Seitenleiste (Aktiv)" navIndicator: "Indikator der Seitenleiste" link: "Link" @@ -2156,28 +1595,30 @@ _theme: infoFg: "Text von Informationen" infoWarnBg: "Hintergrund von Warnungen" infoWarnFg: "Text von Warnungen" + cwBg: "Hintergrund des Inhaltswarnungsknopfs" + cwFg: "Text des Inhaltswarnungsknopfs" + cwHoverBg: "Hintergrund des Inhaltswarnungsknopfs (Mouseover)" toastBg: "Hintergrund von Benachrichtigungen" toastFg: "Text von Benachrichtigungen" buttonBg: "Hintergrund von Schaltflächen" buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)" inputBorder: "Rahmen von Eingabefeldern" + listItemHoverBg: "Hintergrund von Listeneinträgen (Mouseover)" + driveFolderBg: "Hintergrund von Drive-Ordnern" + wallpaperOverlay: "Hintergrundbild-Overlay" badge: "Wappen" messageBg: "Hintergrund von Chats" + accentDarken: "Akzent (Verdunkelt)" + accentLighten: "Akzent (Erhellt)" fgHighlighted: "Hervorgehobener Text" _sfx: note: "Notizen" noteMy: "Meine Notizen" notification: "Benachrichtigungen" - reaction: "Auswählen einer Reaktion" - chatMessage: "Chat-Nachrichten" -_soundSettings: - driveFile: "Audiodatei aus dem Drive verwenden" - driveFileWarn: "Wähle eine Audiodatei aus dem Drive" - driveFileTypeWarn: "Diese Datei wird nicht unterstützt" - driveFileTypeWarnDescription: "Bitte wähle eine Audiodatei" - driveFileDurationWarn: "Audio zu lang." - driveFileDurationWarnDescription: "Lange Töne kann die Verwendung von Misskey stören. Trotzdem fortfahren?" - driveFileError: "Audio konnte nicht geladen werden. Bitte ändere die Einstellung." + chat: "Chat" + chatBg: "Chat (Hintergrund)" + antenna: "Antennen" + channel: "Kanalbenachrichtigung" _ago: future: "Zukunft" justNow: "Gerade eben" @@ -2189,33 +1630,37 @@ _ago: monthsAgo: "vor {n} Monat(en)" yearsAgo: "vor {n} Jahr(en)" invalid: "Ungültig" -_timeIn: - seconds: "In {n}s" - minutes: "In {n} Min." - hours: "In {n} Std." - days: "In {n} Tagen" - weeks: "In {n} Wochen" - months: "In {n} Monaten" - years: "In {n} Jahren" _time: second: "Sekunde(n)" minute: "Minute(n)" hour: "Stunde(n)" day: "Tag(en)" +_timelineTutorial: + title: "Wie du Misskey verwendest" + step1_1: "Dieser Bildschirm ist die \"Chronik\". Hier werden alle \"Notizen\" von {name} angezeigt." + step1_2: "Es gibt einige verschiedene Chroniken. Beispielsweise werden in der \"Startseite\" alle Notizen von Nutzern, denen du folgst, angezeigt, und in der \"Lokalen Chronik\" werden Notizen aller Nutzer auf {name} angezeigt." + step2_1: "Lass uns als nächstes versuchen, eine Notiz zu schreiben. Dies kannst du tun, indem du auf den Knopf mit dem Stift-Icon drückst." + step2_2: "Stell dich den anderen vor oder schreibe einfach \"Hallo {name}!\", wenn du darauf keine Lust hast oder dir nichts einfällt." + step3_1: "Fertig mit dem Senden deiner ersten Notiz?" + step3_2: "Falls deine Notiz nun in deiner Chronik auftaucht, hast du alles richtig gemacht." + step4_1: "Notizen können zusätzlich mit \"Reaktionen\" ausgestattet werden." + step4_2: "Um eine Reaktion anzufügen, klicke auf das „+“-Symbol einer Notiz und wähle ein Emoji aus, mit dem du reagieren möchtest." _2fa: alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert." registerTOTP: "Authentifizierungs-App registrieren" + passwordToTOTP: "Bitte Passwort eingeben" step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät." step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät." - step2Uri: "Nutzt du ein Desktopprogramm, gib folgende URI eingeben" + step2Click: "Durch Klicken dieses QR-Codes kannst du Verifikation mit deinem Security-Token oder einer App registrieren." + step2Url: "Nutzt du ein Desktopprogramm kannst du alternativ diese URL eingeben:" step3Title: "Authentifizierungsscode eingeben" - step3: "Gib zum Abschluss den Code (Token) ein, der von deiner App angezeigt wird." - setupCompleted: "Einrichtung abgeschlossen" + step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird." step4: "Alle folgenden Anmeldeversuche werden ab sofort die Eingabe eines solchen Tokens benötigen." - securityKeyNotSupported: "Dein Browser unterstützt keine Hardware-Sicherheitsschlüssel." + securityKeyNotSupported: "Dein Browser unterstützt keine Security-Tokens." registerTOTPBeforeKey: "Um einen Security-Token oder einen Passkey zu registrieren, musst du zuerst eine Authentifizierungs-App registrieren." securityKeyInfo: "Du kannst neben Fingerabdruck- oder PIN-Authentifizierung auf deinem Gerät auch Anmeldung mit Hilfe eines FIDO2-kompatiblen Hardware-Sicherheitsschlüssels einrichten." - registerSecurityKey: "Hardware-Sicherheitsschlüssel oder Passkey registrieren" + chromePasskeyNotSupported: "Chrome-Passkeys werden zur Zeit nicht unterstützt." + registerSecurityKey: "Security-Token oder Passkey registrieren" securityKeyName: "Schlüsselname eingeben" tapSecurityKey: "Bitten folge den Anweisungen deines Browsers zur Registrierung" removeKey: "Sicherheitsschlüssel entfernen" @@ -2225,12 +1670,6 @@ _2fa: renewTOTPConfirm: "Codes der bisherigen App werden hierdurch nutzlos" renewTOTPOk: "Neu einrichten" renewTOTPCancel: "Abbrechen" - checkBackupCodesBeforeCloseThisWizard: "Notiere bitte deine Backup-Codes, bevor du dieses Fenster schließt." - backupCodes: "Backup-Codes" - backupCodesDescription: "Verwende diese Codes, falls du nicht mehr auf deine App zur Zweifaktorauthentifizierung zugreifen kannst. Jeder Code kann nur einmal verwendet werden. Bewahre sie an einem sicheren Ort auf." - backupCodeUsedWarning: "Ein Backup-Code wurde verwendet. Falls du den Zugriff zu deiner Zweifaktorauthentifizierungsapp verloren hast, konfiguriere diese bitte möglichst bald erneut." - backupCodesExhaustedWarning: "Alle Backup-Codes wurden verwendet. Falls du den Zugang zu deiner Zweifaktorauthentifizierungsapp verlierst, wirst du dich nicht mehr in dieses Konto einloggen können. Bitte konfiguriere diese App erneut." - moreDetailedGuideHere: "Hier ist eine ausführliche Anleitung" _permissions: "read:account": "Deine Benutzerkontoinformationen lesen" "write:account": "Deine Benutzerkontoinformationen bearbeiten" @@ -2264,60 +1703,6 @@ _permissions: "write:gallery": "Deine Galerie bearbeiten" "read:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Galerie-Beiträge lesen" "write:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Galerie-Beiträge bearbeiten" - "read:flash": "Deine Plays lesen" - "write:flash": "Deine Plays bearbeiten oder löschen" - "read:flash-likes": "Liste der Plays, die mir gefallen, lesen" - "write:flash-likes": "Liste der Plays, die mir gefallen, bearbeiten" - "read:admin:abuse-user-reports": "Meldungen von Benutzern ansehen" - "write:admin:delete-account": "Benutzerkonto löschen" - "write:admin:delete-all-files-of-a-user": "Alle Dateien eines Benutzers löschen" - "read:admin:index-stats": "Statistiken zu Datenbankindizes einsehen" - "read:admin:table-stats": "Statistiken zu Datenbanktabellen einsehen" - "read:admin:user-ips": "IP-Adressen von Benutzern anzeigen" - "read:admin:meta": "Metadaten der Instanz einsehen" - "write:admin:reset-password": "Benutzerpasswort zurücksetzen" - "write:admin:resolve-abuse-user-report": "Meldungen von Benutzern lösen" - "write:admin:send-email": "E-Mail versenden" - "read:admin:server-info": "Serverinformationen anzeigen" - "read:admin:show-moderation-log": "Moderationsprotokoll einsehen" - "read:admin:show-user": "Private Benutzerinformationen einsehen" - "write:admin:suspend-user": "Benutzer sperren" - "write:admin:unset-user-avatar": "Benutzer-Profilbild entfernen" - "write:admin:unset-user-banner": "Benutzer-Banner entfernen" - "write:admin:unsuspend-user": "Benutzer entsperren" - "write:admin:meta": "Metadaten der Instanz verwalten" - "write:admin:user-note": "Moderationsvermerke verwalten" - "write:admin:roles": "Rollen verwalten" - "read:admin:roles": "Rollen anzeigen" - "write:admin:relays": "Relays verwalten" - "read:admin:relays": "Relays anzeigen" - "write:admin:invite-codes": "Einladungscodes verwalten" - "read:admin:invite-codes": "Einladungscodes anzeigen" - "write:admin:announcements": "Ankündigungen verwalten" - "read:admin:announcements": "Ankündigungen einsehen" - "write:admin:avatar-decorations": "Kann Avatar-Dekorationen verwalten" - "read:admin:avatar-decorations": "Avatar-Dekorationen ansehen" - "write:admin:federation": "Informationen über Föderationen bearbeiten oder löschen" - "write:admin:account": "Benutzerkonten verwalten" - "read:admin:account": "Benutzerkonten anzeigen" - "write:admin:emoji": "Emojis verwalten" - "read:admin:emoji": "Emojis anzeigen" - "write:admin:queue": "Job-Warteschlange verwalten" - "read:admin:queue": "Job-Warteschlange anzeigen" - "write:admin:promo": "Moderationsnotiz hinzufügen" - "write:admin:drive": "Benutzer-Drive verwalten" - "read:admin:drive": "Benutzer-Drive ansehen" - "read:admin:stream": "Verwendung der Websocket-API für Administratoren" - "write:admin:ad": "Werbung verwalten" - "read:admin:ad": "Werbung ansehen" - "write:invite-codes": "Einladungscodes erstellen" - "read:invite-codes": "Einladungscodes anzeigen" - "write:clip-favorite": "Clip-Likes bearbeiten oder löschen" - "read:clip-favorite": "Clip-Likes ansehen" - "read:federation": "Informationen zur Föderation einsehen" - "write:report-abuse": "Verstöße melden" - "write:chat": "Chats bedienen" - "read:chat": "Chats durchsuchen" _auth: shareAccessTitle: "Verteilung von App-Berechtigungen" shareAccess: "Möchtest du „{name}“ authorisieren, auf dieses Benutzerkonto zugreifen zu können?" @@ -2326,17 +1711,13 @@ _auth: permissionAsk: "Diese Anwendung fordert folgende Berechtigungen" pleaseGoBack: "Bitte kehre zur Anwendung zurück" callback: "Es wird zur Anwendung zurückgekehrt" - accepted: "Zugriff gewährt" denied: "Zugriff verweigert" - scopeUser: "Als folgender Benutzer agieren" pleaseLogin: "Bitte logge dich ein, um Apps zu authorisieren." - byClickingYouWillBeRedirectedToThisUrl: "Wenn der Zugang gewährt wird, wirst du automatisch zu folgender URL weitergeleitet" _antennaSources: all: "Alle Notizen" homeTimeline: "Notizen von Benutzern, denen gefolgt wird" users: "Notizen von einem oder mehreren angegebenen Benutzern" userList: "Notizen von allen Benutzern einer Liste" - userBlacklist: "Alle Notizen abgesehen derer angegebener Benutzer" _weekday: sunday: "Sonntag" monday: "Montag" @@ -2375,8 +1756,6 @@ _widgets: _userList: chooseList: "Liste auswählen" clicker: "Klickzähler" - birthdayFollowings: "Nutzer, die heute Geburtstag haben" - chat: "Chat" _cw: hide: "Inhalt verbergen" show: "Inhalt anzeigen" @@ -2438,22 +1817,15 @@ _profile: metadataContent: "Inhalt" changeAvatar: "Profilbild ändern" changeBanner: "Banner ändern" - verifiedLinkDescription: "Gibst du hier eine URL ein, die einen Link zu deinem Profile enthält, wird neben diesem Feld ein Icon zur Besitzbestätigung angezeigt." - avatarDecorationMax: "Du kannst bis zu {max} Dekorationen hinzufügen." - followedMessage: "Nachricht, wenn dir jemand folgt" - followedMessageDescription: "Du kannst eine kurze Nachricht festlegen, die dem Empfänger angezeigt wird, wenn er dir folgt." - followedMessageDescriptionForLockedAccount: "Wenn Folgeanfragen deine Genehmigung brauchen, wird dies beim Genehmigen einer Anfrage angezeigt." _exportOrImport: allNotes: "Alle Notizen" favoritedNotes: "Als Favorit markierte Notizen" - clips: "Clip erstellen" followingList: "Gefolgte Benutzer" muteList: "Stummschaltungen" blockingList: "Blockierungen" userLists: "Listen" excludeMutingUsers: "Stummgeschaltete Benutzer aussortieren" excludeInactiveUsers: "Inaktive Benutzer aussortieren" - withReplies: "Antworten von importierten Benutzern in der Chronik beinhalten" _charts: federation: "Föderation" apRequest: "Anfragen" @@ -2500,11 +1872,13 @@ _play: title: "Titel" script: "Skript" summary: "Beschreibung" - visibilityDescription: "Wenn du die Sichtbarkeit auf Privat stellst, wird der Play nicht auf deinem Profil sichtbar sein, aber jeder, der die URL hat, kann ihn trotzdem aufrufen." _pages: newPage: "Seite erstellen" editPage: "Seite bearbeiten" readPage: "Quelltextansicht" + created: "Seite erfolgreich erstellt" + updated: "Seite erfolgreich aktualisiert" + deleted: "Seite erfolgreich gelöscht" pageSetting: "Seiteneinstellungen" nameAlreadyExists: "Die angegebene Seiten-URL existiert bereits" invalidNameTitle: "Die angegebene Seiten-URL ist ungültig" @@ -2532,7 +1906,6 @@ _pages: eyeCatchingImageSet: "Vorschaubild festlegen" eyeCatchingImageRemove: "Vorschaubild entfernen" chooseBlock: "Block hinzufügen" - enterSectionTitle: "Titel des Abschnitts eingeben" selectType: "Typ auswählen" contentBlocks: "Inhalt" inputBlocks: "Eingabe" @@ -2543,8 +1916,6 @@ _pages: section: "Abschnitt" image: "Bild" button: "Knopf" - dynamic: "Dynamische Bausteine" - dynamicDescription: "Dieser Baustein wurde abgeschafft. Bitte verwende von nun an {play}." note: "Eingebettete Notiz" _note: id: "Notiz-ID" @@ -2564,28 +1935,11 @@ _notification: youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten" yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert" pollEnded: "Umfrageergebnisse sind verfügbar" - newNote: "Neue Notiz" unreadAntennaNote: "Antenne {name}" - roleAssigned: "Rolle zugewiesen" - chatRoomInvitationReceived: "Du wurdest in einen Chatraum eingeladen" emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert" achievementEarned: "Errungenschaft freigeschaltet" - testNotification: "Testbenachrichtigung" - checkNotificationBehavior: "Aussehen von Benachrichtigungen überprüfen" - sendTestNotification: "Testbenachrichtigung senden" - notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus" - reactedBySomeUsers: "{n} Benutzer haben eine Reaktion geschickt" - likedBySomeUsers: "{n} Benutzer mochten deine Notiz" - renotedBySomeUsers: "Renote von {n} Benutzern" - followedBySomeUsers: "Von {n} Benutzern gefolgt" - flushNotification: "Benachrichtigungen löschen" - exportOfXCompleted: "Der Export von {x} ist abgeschlossen" - login: "Neue Anmeldung erfolgt" - createToken: "Ein Zugangstoken wurde erstellt" - createTokenDescription: "Wenn Sie keine Ahnung haben, löschen Sie das Zugriffstoken über \"{text}\"" _types: all: "Alle" - note: "Neue Notizen" follow: "Neue Follower" mention: "Erwähnungen" reply: "Antworten" @@ -2595,13 +1949,7 @@ _notification: pollEnded: "Ende von Umfragen" receiveFollowRequest: "Erhaltene Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen" - roleAssigned: "Rolle zugewiesen" - chatRoomInvitationReceived: "Einladungen zum Chatraum" achievementEarned: "Errungenschaft freigeschaltet" - exportCompleted: "Der Export ist abgeschlossen" - login: "Anmeldung" - createToken: "Erstellung von Zugriffstokens" - test: "Test-Benachrichtigungen" app: "Benachrichtigungen von Apps" _actions: followBack: "folgt dir nun auch" @@ -2610,11 +1958,7 @@ _notification: _deck: alwaysShowMainColumn: "Hauptspalte immer zeigen" columnAlign: "Spaltenausrichtung" - columnGap: "Spaltenabstand" - deckMenuPosition: "Position des Deck-Menüs" - navbarPosition: "Position der Navigationsleiste" addColumn: "Spalte hinzufügen" - newNoteNotificationSettings: "Benachrichtigungseinstellungen für neue Notizen" configureColumn: "Spalteneinstellungen" swapLeft: "Mit linker Spalte tauschen" swapRight: "Mit rechter Spalte tauschen" @@ -2628,10 +1972,6 @@ _deck: introduction: "Erstelle eine auf dich zugeschneiderte Benutzeroberfläche durch das Aneinanderreihen von Spalten!" introduction2: "Klicke auf das + rechts um wann immer du möchtest neue Spalten hinzuzufügen." widgetsIntroduction: "Drücke bitte \"Widgets bearbeiten\" im Spaltenmenü und füge ein Widget hinzu." - useSimpleUiForNonRootPages: "Simple Benutzeroberfläche für navigierte Seiten verwenden" - usedAsMinWidthWhenFlexible: "Ist \"Automatische Breitenanpassung\" aktiviert, wird hierfür die minimale Breite verwendet" - flexible: "Automatische Breitenanpassung" - enableSyncBetweenDevicesForProfiles: "Aktivieren der Synchronisierung von Profilinformationen zwischen Geräten" _columns: main: "Hauptspalte" widgets: "Widgets" @@ -2643,7 +1983,6 @@ _deck: mentions: "Erwähnungen" direct: "Direktnachrichten" roleTimeline: "Rollenchronik" - chat: "Chat" _dialog: charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}" charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}" @@ -2655,10 +1994,9 @@ _drivecleaner: orderByCreatedAtAsc: "Aufsteigendes Erstelldatum" _webhookSettings: createWebhook: "Webhook erstellen" - modifyWebhook: "Webhook bearbeiten" name: "Name" secret: "Secret" - trigger: "Auslöser" + events: "Webhook-Ereignisse" active: "Aktiviert" _events: follow: "Wenn du jemandem folgst" @@ -2668,336 +2006,3 @@ _webhookSettings: renote: "Wenn du ein Renote erhältst" reaction: "Wenn du eine Reaktion erhältst" mention: "Wenn du erwähnt wirst" - _systemEvents: - abuseReport: "Wenn eine neue Meldung eingeht" - abuseReportResolved: "Wenn eine Meldung gelöst wird" - userCreated: "Beim Anlegen eines Benutzers" - inactiveModeratorsWarning: "Wenn Moderatoren für eine gewisse Zeit inaktiv sind" - inactiveModeratorsInvitationOnlyChanged: "Wenn ein Moderator über einen gewissen Zeitraum inaktiv war und der Server auf Einladungsbasis umgestellt wird" - deleteConfirm: "Bist du sicher, dass du den Webhook löschen willst?" - testRemarks: "Klicke auf die Schaltfläche rechts neben dem Schalter, um einen Test-Webhook mit Dummy-Daten zu senden." -_abuseReport: - _notificationRecipient: - createRecipient: "Meldungsempfänger hinzufügen" - modifyRecipient: "Bearbeite einen Empfänger für Meldungen" - recipientType: "Art der Benachrichtigung" - _recipientType: - mail: "Email" - webhook: "Webhook" - _captions: - mail: "Die Benachrichtigung wird bei Eingang einer Meldung an die E-Mail-Adressen der Moderatoren gesendet" - webhook: "Sendet eine Benachrichtigung an den System Webhook, wenn eine Meldung eingegangen ist oder gelöst wurde" - keywords: "Schlüsselwort" - notifiedUser: "Zu benachrichtigender Benutzer" - notifiedWebhook: "Zu verwendender Webhook" - deleteConfirm: "Bist du sicher, dass du den Empfänger der Benachrichtigung entfernen möchtest?" -_moderationLogTypes: - createRole: "Rolle erstellt" - deleteRole: "Rolle gelöscht" - updateRole: "Rolle aktualisiert" - assignRole: "Zu Rolle zugewiesen" - unassignRole: "Aus Rolle entfernt" - suspend: "Gesperrt" - unsuspend: "Entsperrt" - addCustomEmoji: "Benutzerdefiniertes Emoji hinzugefügt" - updateCustomEmoji: "Benutzerdefiniertes Emoji aktualisiert" - deleteCustomEmoji: "Benutzerdefiniertes Emoji gelöscht" - updateServerSettings: "Servereinstellungen aktualisiert" - updateUserNote: "Moderationsnotiz aktualisiert" - deleteDriveFile: "Datei gelöscht" - deleteNote: "Notiz gelöscht" - createGlobalAnnouncement: "Globale Ankündigung erstellt" - createUserAnnouncement: "Benutzerspezifische Ankündigung erstellt" - updateGlobalAnnouncement: "Globale Ankündigung aktualisiert" - updateUserAnnouncement: "Benutzerspezifische Ankündigung aktualisiert" - deleteGlobalAnnouncement: "Globale Ankündigung gelöscht" - deleteUserAnnouncement: "Benutzerspezifische Ankündigung gelöscht" - resetPassword: "Passwort zurückgesetzt" - suspendRemoteInstance: "Fremde Instanz gesperrt" - unsuspendRemoteInstance: "Fremde Instanz entsperrt" - updateRemoteInstanceNote: "Aktualisierung der Moderationshinweise für fremde Server." - markSensitiveDriveFile: "Datei als sensitiv markiert" - unmarkSensitiveDriveFile: "Datei als nicht sensitiv markiert" - resolveAbuseReport: "Meldung bearbeitet" - forwardAbuseReport: "Meldung weitergeleitet" - updateAbuseReportNote: "Moderationsnotiz einer Meldung aktualisiert" - createInvitation: "Einladung erstellt" - createAd: "Werbung erstellt" - deleteAd: "Werbung gelöscht" - updateAd: "Werbung aktualisiert" - createAvatarDecoration: "Profilbilddekoration erstellt" - updateAvatarDecoration: "Profilbilddekoration aktualisiert" - deleteAvatarDecoration: "Profilbilddekoration gelöscht" - unsetUserAvatar: "Profilbild zurückgesetzt" - unsetUserBanner: "Profilbanner zurückgesetzt" - createSystemWebhook: "System-Webhook erstellt" - updateSystemWebhook: "System-Webhook aktualisiert" - deleteSystemWebhook: "System-Webhook gelöscht" - createAbuseReportNotificationRecipient: "Empfänger für Meldungen erstellt" - updateAbuseReportNotificationRecipient: "Empfänger für Meldungen aktualisiert" - deleteAbuseReportNotificationRecipient: "Empfänger für Meldungen entfernt" - deleteAccount: "Benutzerkonto gelöscht" - deletePage: "Seite gelöscht" - deleteFlash: "Play gelöscht" - deleteGalleryPost: "Galeriebeitrag gelöscht" - deleteChatRoom: "Chatraum gelöscht" - updateProxyAccountDescription: "Beschreibung des Proxy-Benutzerkontos aktualisiert" -_fileViewer: - title: "Dateiinformationen" - type: "Dateityp" - size: "Dateigröße" - url: "URL" - uploadedAt: "Hochgeladen am" - attachedNotes: "Zugehörige Notizen" - thisPageCanBeSeenFromTheAuthor: "Nur der Benutzer, der diese Datei hochgeladen hat, kann diese Seite sehen." -_externalResourceInstaller: - title: "Von externer Seite installieren" - checkVendorBeforeInstall: "Überprüfe vor Installation die Vertrauenswürdigkeit des Vertreibers." - _plugin: - title: "Möchtest du dieses Plugin installieren?" - _theme: - title: "Möchten du dieses Farbschema installieren?" - _meta: - base: "Farbschemavorlage" - _vendorInfo: - title: "Vertreiber" - endpoint: "Referenzierter Endpunkt" - hashVerify: "Hash-Verifikation" - _errors: - _invalidParams: - title: "Ungültige Parameter" - description: "Es fehlen Informationen zum Laden der externen Ressource. Überprüfe die übergebene URL." - _resourceTypeNotSupported: - title: "Diese Ressource wird nicht unterstützt" - description: "Dieser Ressourcentyp wird nicht unterstützt. Bitte kontaktiere den Seitenbesitzer." - _failedToFetch: - title: "Fehler beim Abrufen der Daten" - fetchErrorDescription: "Während der Kommunikation mit der externen Seite ist ein Fehler aufgetreten. Kontaktiere den Seitenbesitzer, falls ein erneutes Probieren dieses Problem nicht löst." - parseErrorDescription: "Während dem Auslesen der externen Daten ist ein Fehler aufgetreten. Kontaktiere den Seitenbesitzer." - _hashUnmatched: - title: "Datenverifizierung fehlgeschlagen" - description: "Die Integritätsprüfung der geladenen Daten ist fehlgeschlagen. Aus Sicherheitsgründen kann die Installation nicht fortgesetzt werden. Kontaktiere den Seitenbesitzer." - _pluginParseFailed: - title: "AiScript-Fehler" - description: "Die angeforderten Daten wurden erfolgreich abgerufen, jedoch trat während des AiScript-Parsings ein Fehler auf. Kontaktiere den Autor des Plugins. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden." - _pluginInstallFailed: - title: "Das Plugin konnte nicht installiert werden" - description: "Während der Installation des Plugin ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden." - _themeParseFailed: - title: "Parsing des Farbschemas fehlgeschlagen" - description: "Die angeforderten Daten wurden erfolgreich abgerufen, jedoch trat während des Farbschema-Parsings ein Fehler auf. Kontaktiere den Autor des Farbschemas. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden." - _themeInstallFailed: - title: "Das Farbschema konnte nicht installiert werden" - description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden." -_dataSaver: - _media: - title: "Laden von Medien verhindern" - description: "Verhindert, dass Bilder/Videos automatisch geladen werden. Ausgeblendete Bilder/Videos werden geladen, wenn du auf sie tippst." - _avatar: - title: "Animierte Profilbilder deaktivieren" - description: "Die Animation von Profilbildern wird angehalten. Da animierte Bilder eine größere Dateigröße haben können als normale Bilder, kann dies den Datenverkehr weiter reduzieren." - _code: - title: "Code-Hervorhebungen ausblenden" - description: "Wenn Code-Hervorhebungen in MFM usw. verwendet werden, werden sie erst geladen, wenn sie angetippt werden. Die Syntaxhervorhebung erfordert das Herunterladen der Definitionsdateien für jede Programmiersprache. Es ist daher zu erwarten, dass die Deaktivierung des automatischen Ladens dieser Dateien die Menge des Datenverkehrs reduziert." -_hemisphere: - N: "Nördliche Erdhalbkugel" - S: "Südliche Erdhalbkugel" - caption: "Wird in einigen Client-Einstellungen zur Bestimmung der Jahreszeit verwendet." -_reversi: - reversi: "Reversi" - gameSettings: "Spieleinstellungen" - chooseBoard: "Spielbrett auswählen" - blackOrWhite: "Schwarz/Weiß" - blackIs: "{name} spielt Schwarz" - rules: "Regeln" - thisGameIsStartedSoon: "Das Spiel wird in Kürze beginnen" - waitingForOther: "Warte auf den Zug des Gegenspielers" - waitingForMe: "Warte auf deinen Zug" - waitingBoth: "Mach dich bereit" - ready: "Bereit" - cancelReady: "Nicht bereit" - opponentTurn: "Dein Gegner ist an der Reihe" - myTurn: "Du bist am Zug" - turnOf: "{name} ist am Zug" - pastTurnOf: "Zug von {name}" - surrender: "Aufgeben" - surrendered: "Aufgegeben" - timeout: "Zeit abgelaufen" - drawn: "Unentschieden" - won: "{name} hat gewonnen" - black: "Schwarz" - white: "Weiß" - total: "Gesamt" - turnCount: " Zug {count}" - myGames: "Meine Runden" - allGames: "Alle Runden" - ended: "Beendet" - playing: "Partie läuft" - isLlotheo: "Der mit weniger Steinen gewinnt (Llotheo)" - loopedMap: "Wiederholendes Spielbrett" - canPutEverywhere: "Steine können überall platziert werden" - timeLimitForEachTurn: "Zeitlimit eines Zugs" - freeMatch: "Freies Spiel" - lookingForPlayer: "Gegner werden gesucht..." - gameCanceled: "Das Spiel wurde abgesagt." - shareToTlTheGameWhenStart: "Spiel in der Chronik teilen, wenn es gestartet wurde" - iStartedAGame: "Das Spiel hat begonnen! #MisskeyReversi" - opponentHasSettingsChanged: "Der Gegner hat seine Einstellungen geändert." - allowIrregularRules: "Irreguläre Regeln (völlig frei)" - disallowIrregularRules: "Keine irregulären Regeln" - showBoardLabels: "Anzeige der Zeilen- und Spaltennummern am Spielbrett" - useAvatarAsStone: "Steine in Benutzeravatare umwandeln" -_offlineScreen: - title: "Offline - keine Verbindung zum Server möglich" - header: "Verbindung zum Server nicht möglich" -_urlPreviewSetting: - title: "Einstellungen der URL-Vorschau" - enable: "URL-Vorschau aktivieren" - timeout: "Zeitüberschreitung beim Abrufen der Vorschau (ms)" - timeoutDescription: "Übersteigt die für die Vorschau benötigte Zeit diesen Wert, wird keine Vorschau generiert." - maximumContentLength: "Maximale Content-Length (Bytes)" - maximumContentLengthDescription: "Wenn die Content-Length diesen Wert überschreitet, wird keine Vorschau erzeugt." - requireContentLength: "Vorschau nur generieren, wenn Content-Length verfügbar ist" - requireContentLengthDescription: "Wenn der Server keine Content-Length zurückgibt, wird keine Vorschau erzeugt." - userAgent: "User-Agent" - userAgentDescription: "Legt den User-Agent fest, der beim Abrufen der Vorschau verwendet werden soll. Bleibt er leer, wird der Standard-User-Agent verwendet." - summaryProxy: "Proxy-Endpunkte, die Vorschaubilder erzeugen" - summaryProxyDescription: "Generierung von Vorschaubildern mit Summaly Proxy anstelle von Misskey selbst." - summaryProxyDescription2: "Die folgenden Parameter werden als Abfrage-Strings mit dem Proxy verknüpft. Wenn der Proxy sie nicht unterstützt, werden die Werte ignoriert." -_mediaControls: - pip: "Bild-in-Bild" - playbackRate: "Wiedergabegeschwindigkeit" - loop: "Endloswiedergabe" -_contextMenu: - title: "Kontextmenü" - app: "Anwendung" - appWithShift: "Anwendung per Umschalttaste" - native: "Natives Browsermenü" -_gridComponent: - _error: - requiredValue: "Dieser Wert ist ein Pflichtfeld" - columnTypeNotSupport: "Die Validierung regulärer Ausdrücke wird nur für Spalten vom Typ \"Text\" unterstützt." - patternNotMatch: "Dieser Wert stimmt nicht mit dem Schema in {pattern} überein" - notUnique: "Dieser Wert muss eindeutig sein" -_roleSelectDialog: - notSelected: "Nicht ausgewählt" -_customEmojisManager: - _gridCommon: - copySelectionRows: "Ausgewählte Zeilen kopieren" - copySelectionRanges: "Auswahl kopieren" - deleteSelectionRows: "Ausgewählte Zeilen löschen" - deleteSelectionRanges: "Zeilen in der Auswahl löschen" - searchSettings: "Sucheinstellungen" - searchSettingCaption: "Detaillierte Suchkriterien festlegen." - searchLimit: "Anzahl der Ergebnisse" - sortOrder: "Sortierung" - registrationLogs: "Registrierungsprotokoll" - registrationLogsCaption: "Protokolle werden beim Aktualisieren oder Löschen von Emojis angezeigt. Sie verschwinden nach dem Aktualisieren oder Löschen, dem Wechsel zu einer neuen Seite oder dem Neuladen." - alertEmojisRegisterFailedDescription: "Emoji konnte nicht aktualisiert oder gelöscht werden. Bitte prüfe das Registrierungsprotokoll für Details." - _logs: - showSuccessLogSwitch: "Erfolgsprotokoll zeigen" - failureLogNothing: "Es gibt kein Fehlerprotokoll." - logNothing: "Keine Protokoll-Einträge." - _remote: - selectionRowDetail: "Details der ausgewählten Zeile" - importSelectionRows: "Ausgewählte Zeilen importieren" - importSelectionRangesRows: "Zeilen in der Auswahl importieren" - importEmojisButton: "Ausgewählte Emojis importieren" - confirmImportEmojisTitle: "Emojis importieren" - confirmImportEmojisDescription: "Importiere {count} Emoji(s), die von entfernten Server empfangen wurden. Bitte achte genau auf die Lizenz der Emojis. Bist du sicher, dass du fortfahren möchtest?" - _local: - tabTitleList: "Hinzugefügte Emojis" - tabTitleRegister: "Emojis hinzufügen" - _list: - emojisNothing: "Es wurden keine Emojis hinzugefügt." - markAsDeleteTargetRows: "Ausgewählte Zeilen als zu löschendes Element markieren" - markAsDeleteTargetRanges: "Zeilen in der Auswahl als zu löschendes Element markieren" - alertUpdateEmojisNothingDescription: "Es wurden keine Emojis geändert." - alertDeleteEmojisNothingDescription: "Es gibt keine zu löschenden Emojis." - confirmMovePage: "Möchten Sie die Seiten verschieben?" - confirmChangeView: "Möchten Sie die Darstellung wechseln?" - confirmUpdateEmojisDescription: "Aktualisiere {count} Emoji(s). Willst du fortfahren?" - confirmDeleteEmojisDescription: "Lösche {count} ausgewählte Emoji(s). Willst du fortfahren?" - confirmResetDescription: "Alle bisher vorgenommenen Änderungen werden zurückgesetzt." - confirmMovePageDesciption: "An den Emojis auf dieser Seite wurden Änderungen vorgenommen.\nWenn du die Seite verlässt, ohne zu speichern, werden alle auf dieser Seite vorgenommenen Änderungen verworfen." - dialogSelectRoleTitle: "Suche nach dem Rollensatz in Emojis" - _register: - uploadSettingTitle: "Upload-Einstellungen" - uploadSettingDescription: "Hier kannst du das Verhalten beim Hochladen von Emojis konfigurieren." - directoryToCategoryLabel: "Gib den Namen des Verzeichnisses in das Feld „Kategorie“ ein" - directoryToCategoryCaption: "Wenn du ein Verzeichnis ziehst und ablegst, gib den Verzeichnisnamen in das Feld „Kategorie“ ein." - confirmRegisterEmojisDescription: "Füge die in der Liste aufgeführten Emojis als neue benutzerdefinierte Emojis hinzu. Bist du sicher? (Um eine Überlastung zu vermeiden, können nur {count} Emoji(s) in einem Vorgang hinzugefügt werden)" - confirmClearEmojisDescription: "Verwerfe die Bearbeitungen und lösche die Emojis aus der Liste. Bist du sicher, dass du fortfahren möchtest?" - confirmUploadEmojisDescription: "Lade die {count} abgelegte(n) Datei(en) in das Drive hoch. Bist du sicher, dass du fortfahren möchtest?" -_embedCodeGen: - title: "Einbettungscode anpassen" - header: "Kopfzeile anzeigen" - autoload: "Automatisch mehr laden (veraltet)" - maxHeight: "Maximale Höhe" - maxHeightDescription: "Der Wert 0 deaktiviert die Einstellung der maximalen Höhe. Gib einen Wert an, um zu verhindern, dass das Widget weiterhin vertikal vergrößert wird." - maxHeightWarn: "Die Begrenzung der maximalen Höhe ist deaktiviert (0). Wenn dies nicht beabsichtigt war, setze die maximale Höhe auf einen Wert fest." - previewIsNotActual: "Die Anzeige weicht von der tatsächlichen Einbettung ab, da sie den auf dem Vorschaufenster angezeigten Bereich überschreitet." - rounded: "Ecken abrunden" - border: "Dem äußeren Rand einen Rahmen hinzufügen" - applyToPreview: "Auf die Vorschau anwenden" - generateCode: "Einbettungscode generieren" - codeGenerated: "Der Code wurde generiert" - codeGeneratedDescription: "Füge den generierten Code in deine Website ein, um den Inhalt einzubetten." -_selfXssPrevention: - warning: "WARNUNG" - title: "„Füge in diesen Bereich etwas ein“ ist eine Betrugsmasche." - description1: "Wenn du hier etwas einfügst, könnte ein böswilliger Benutzer dein Konto übernehmen oder deine persönlichen Daten stehlen." - description2: "Wenn du das nicht genau verstehst, was du einfügst, %csolltest du die Eingabe abbrechen und das Fenster schließen." - description3: "Weitere Informationen findest du hier. {link}" -_followRequest: - recieved: "Anfrage erhalten" - sent: "Anfrage gesendet" -_remoteLookupErrors: - _federationNotAllowed: - title: "Kommunikation mit diesem Server nicht möglich" - description: "Möglicherweise wurde die Kommunikation mit diesem Server deaktiviert oder dieser Server ist blockiert.\nWende dich bitte an den Serveradministrator." - _uriInvalid: - title: "URI ist fehlerhaft" - description: "Es gibt ein Problem mit der von dir eingegebenen URI. Bitte prüfe, ob du Zeichen eingegeben hast, die in der URI nicht verwendet werden können." - _requestFailed: - title: "Anfrage fehlgeschlagen" - description: "Die Kommunikation mit diesem Server ist fehlgeschlagen. Der Server ist möglicherweise nicht erreichbar. Bitte vergewissere dich auch, dass du keine ungültige oder nicht existierende URI eingegeben hast." - _responseInvalid: - title: "Die Antwort ist ungültig" - description: "Die Kommunikation mit dem Server war erfolgreich, aber die erhaltenen Daten waren nicht korrekt. Wenn du Remote-Inhalte über einen Server eines Dritten abfragst, verwende bitte erneut eine URI, die vom Ursprungsserver abgerufen werden kann." - _noSuchObject: - title: "Nicht gefunden" - description: "Die angeforderte Ressource konnte nicht gefunden werden, bitte überprüfe die URI erneut." -_captcha: - verify: "Bitte beantworte das CAPTCHA" - testSiteKeyMessage: "Du kannst die Vorschau prüfen, indem du die Testwerte für den Site- und Secret-Key eingibst. Weitere Informationen findest du auf der folgenden Seite." - _error: - _requestFailed: - title: "CAPTCHA-Anfrage fehlgeschlagen." - text: "Bitte probiere es später noch einmal oder überprüfe die Einstellungen erneut." - _verificationFailed: - title: "CAPTCHA-Prüfung fehlgeschlagen" - text: "Bitte überprüfe nochmals, ob die Einstellungen korrekt sind." - _unknown: - title: "CAPTCHA-Fehler" - text: "Es ist ein unerwarteter Fehler aufgetreten." -_bootErrors: - title: "Laden fehlgeschlagen" - serverError: "Wenn das Problem nach kurzem Warten und erneutem Laden immer noch nicht behoben ist, wende dich bitte an den Serveradministrator und gib die folgende Fehler-ID an." - solution: "Folgendes könnte das Problem lösen." - solution1: "Aktualisiere deinen Browser und dein Betriebssystem auf die neueste Version" - solution2: "Deaktiviere den Werbeblocker" - solution3: "Leere den Browser-Cache" - solution4: "(Tor Browser) Setze dom.webaudio.enabled auf true" - otherOption: "Weitere Optionen" - otherOption1: "Client-Einstellungen und Cache löschen" - otherOption2: "Einfachen Client starten" - otherOption3: "Starte das Reparaturwerkzeug" -_search: - searchScopeAll: "Alle" - searchScopeLocal: "Lokal" - searchScopeServer: "Bestimmter Server" - searchScopeUser: "Spezifischer Benutzer" - pleaseEnterServerHost: "Gib den Server-Host ein" - pleaseSelectUser: "Benutzer auswählen" - serverHostPlaceholder: "Beispiel: misskey.example.com" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index c8aff304d2..41b1ea7c65 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -104,6 +104,7 @@ clickToShow: "Κάντε κλικ για εμφάνιση" add: "Προσθέστε" reaction: "Αντιδράσεις" reactions: "Αντιδράσεις" +reactionSetting: "Αντιδράσεις για εμφάνιση στην επιλογή αντίδρασης" reactionSettingDescription2: "Σύρετε για να αλλάξετε τη σειρά, κάντε κλικ για να διαγράψετε, πατήστε \"+\" για να προσθέσετε." rememberNoteVisibility: "Θυμήσου τις ρυθμίσεις ορατότητας σημειώματος" attachCancel: "Διαγραφή αρχείου" @@ -162,12 +163,14 @@ imageUrl: "URL εικόνας" remove: "Διαγραφή" removed: "Η διαγραφή ολοκληρώθηκε επιτυχώς" saved: "Αποθηκεύτηκε" +messaging: "Συνομιλία" upload: "Ανεβάστε" fromDrive: "Από τον Αποθηκευτικό Χώρο" fromUrl: "Από URL" uploadFromUrl: "Ανεβάστε από URL" explore: "Εξερευνήστε" messageRead: "Διαβάστηκε" +startMessaging: "Ξεκινήστε μία συνομιλία" nUsersRead: "διαβάστηκε από {n}" start: "Ας αρχίσουμε" home: "Κεντρικό" @@ -225,6 +228,7 @@ userList: "Λίστες" about: "Πληροφορίες" moderator: "Συντονιστής" moderation: "Συντονισμός" +cacheClear: "Εκκαθάριση προσωρινής μνήμης" markAsReadAllNotifications: "Όλες οι ειδοποιήσεις διαβάστηκαν" members: "Μέλη" transfer: "Μεταφορά" @@ -283,14 +287,6 @@ searchByGoogle: "Αναζήτηση" file: "Αρχεία" recommended: "Προτεινόμενα" cannotUploadBecauseNoFreeSpace: "Το ανέβασμα απέτυχε λόγω ανεπαρκούς Αποθηκευτικού Χώρου" -icon: "Εικονίδιο" -replies: "Απάντηση" -renotes: "Κοινοποίηση σημειώματος" -postForm: "Φόρμα δημοσίευσης" -information: "Πληροφορίες" -_chat: - members: "Μέλη" - home: "Κεντρικό" _email: _follow: title: "Έχετε ένα νέο ακόλουθο" @@ -304,6 +300,10 @@ _theme: _sfx: note: "Σημειώματα" notification: "Ειδοποιήσεις" + chat: "Συνομιλία" + chatBg: "Συνομιλία (Παρασκήνιο)" + antenna: "Αντένες" + channel: "Ειδοποιήσεις καναλιών" _ago: future: "Μελλοντικό" justNow: "Μόλις τώρα" @@ -324,7 +324,6 @@ _permissions: "write:notifications": "Διαχειριστείτε τις ειδοποιήσεις σας" "read:pages": "Δείτε τις Σελίδες σας" "write:pages": "Επεξεργαστείτε ή διαγράψτε τις σελίδες σας" - "write:chat": "Γράψτε ή διαγράψτε μηνύματα συνομιλίας" _antennaSources: all: "Όλα τα σημειώματα" homeTimeline: "Σημειώματα από μέλη που ακολουθείτε" @@ -358,7 +357,6 @@ _profile: username: "Όνομα μέλους" _exportOrImport: allNotes: "Όλα τα σημειώματα" - clips: "Κλιπ" followingList: "Ακολουθεί" muteList: "Μέλη σε σίγαση" blockingList: "Μπλοκαρισμένα μέλη" @@ -382,7 +380,6 @@ _notification: renote: "Κοινοποίηση σημειώματος" quote: "Παράθεση" reaction: "Αντιδράσεις" - login: "Σύνδεση" _actions: reply: "Απάντηση" renote: "Κοινοποίηση σημειώματος" @@ -397,9 +394,3 @@ _deck: mentions: "Επισημάνσεις" _webhookSettings: name: "Όνομα" -_moderationLogTypes: - suspend: "Αποβολή" -_reversi: - total: "Σύνολο" -_search: - searchScopeLocal: "Τοπικό" diff --git a/locales/en-US.yml b/locales/en-US.yml index 492a9f2887..02c15e5418 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -5,13 +5,9 @@ introMisskey: "Welcome! Misskey is an open source, decentralized microblogging s poweredByMisskeyDescription: "{name} is one of the services powered by the open source platform Misskey (referred to as a \"Misskey instance\")." monthAndDay: "{month}/{day}" search: "Search" -reset: "Reset" notifications: "Notifications" username: "Username" password: "Password" -initialPasswordForSetup: "Initial password for setup" -initialPasswordIsIncorrect: "Initial password for setup is incorrect" -initialPasswordForSetupDescription: "Use the password you entered in the configuration file if you installed Misskey yourself.\n If you are using a Misskey hosting service, use the password provided.\n If you have not set a password, leave it blank to continue." forgotPassword: "Forgot password" fetchingAsApObject: "Fetching from the Fediverse..." ok: "OK" @@ -49,13 +45,10 @@ pin: "Pin to profile" unpin: "Unpin from profile" copyContent: "Copy contents" copyLink: "Copy link" -copyRemoteLink: "Copy remote link" -copyLinkRenote: "Copy renote link" delete: "Delete" deleteAndEdit: "Delete and edit" -deleteAndEditConfirm: "Are you sure you want to redraft this note? This means you will lose all reactions, renotes, and replies to it." +deleteAndEditConfirm: "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it." addToList: "Add to list" -addToAntenna: "Add to antenna" sendMessage: "Send a message" copyRSS: "Copy RSS" copyUsername: "Copy username" @@ -63,9 +56,7 @@ copyUserId: "Copy user ID" copyNoteId: "Copy note ID" copyFileId: "Copy file ID" copyFolderId: "Copy folder ID" -copyProfileUrl: "Copy profile URL" searchUser: "Search for a user" -searchThisUsersNotes: "Search this user’s notes" reply: "Reply" loadMore: "Load more" showMore: "Show more" @@ -81,7 +72,7 @@ import: "Import" export: "Export" files: "Files" download: "Download" -driveFileDeleteConfirm: "Do you want to remove the file \"{name}\"? Some content using this file will also be removed." +driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? It will also vanish from all contents that use it." unfollowConfirm: "Are you sure you want to unfollow {name}?" exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." importRequested: "You've requested an import. This may take a while." @@ -112,16 +103,13 @@ unfollow: "Unfollow" followRequestPending: "Follow request pending" enterEmoji: "Enter an emoji" renote: "Renote" -unrenote: "Remove renote" +unrenote: "Take back renote" renoted: "Renoted." -renotedToX: "Renoted to {name}." cantRenote: "This post can't be renoted." cantReRenote: "A renote can't be renoted." quote: "Quote" inChannelRenote: "Channel-only Renote" inChannelQuote: "Channel-only Quote" -renoteToChannel: "Renote to channel" -renoteToOtherChannel: "Renote to other channel" pinnedNote: "Pinned note" pinned: "Pin to profile" you: "You" @@ -130,16 +118,10 @@ sensitive: "Sensitive" add: "Add" reaction: "Reactions" reactions: "Reactions" -emojiPicker: "Emoji picker" -pinnedEmojisForReactionSettingDescription: "Set the emojis to be pinned and displayed when reacting." -pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker" -emojiPickerDisplay: "Emoji picker display" -overwriteFromPinnedEmojisForReaction: "Override from reaction settings" -overwriteFromPinnedEmojis: "Override from general settings" +reactionSetting: "Reactions to show in the reaction picker" reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add." rememberNoteVisibility: "Remember note visibility settings" attachCancel: "Remove attachment" -deleteFile: "Delete file" markAsSensitive: "Mark as sensitive" unmarkAsSensitive: "Unmark as sensitive" enterFileName: "Enter filename" @@ -160,7 +142,6 @@ editList: "Edit list" selectChannel: "Select a channel" selectAntenna: "Select an antenna" editAntenna: "Edit antenna" -createAntenna: "Create an antenna" selectWidget: "Select a widget" editWidgets: "Edit widgets" editWidgetsExit: "Done" @@ -172,10 +153,7 @@ emojiUrl: "Emoji URL" addEmoji: "Add an emoji" settingGuide: "Recommended settings" cacheRemoteFiles: "Cache remote files" -cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote servers. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." -youCanCleanRemoteFilesCache: "You can clear the cache by clicking the 🗑️ button in the file management view." -cacheRemoteSensitiveFiles: "Cache sensitive remote files" -cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching." +cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." flagAsBot: "Mark this account as a bot" flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot." flagAsCat: "Mark this account as a cat" @@ -187,10 +165,6 @@ addAccount: "Add account" reloadAccountsList: "Reload account list" loginFailed: "Failed to sign in" showOnRemote: "View on remote instance" -continueOnRemote: "Continue on a remote server" -chooseServerOnMisskeyHub: "Choose a server from the Misskey Hub" -specifyServerHost: "Specify a server host directly" -inputHostName: "Enter the domain" general: "General" wallpaper: "Wallpaper" setWallpaper: "Set wallpaper" @@ -201,7 +175,6 @@ followConfirm: "Are you sure that you want to follow {name}?" proxyAccount: "Proxy account" proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead." host: "Host" -selectSelf: "Select myself" selectUser: "Select a user" recipient: "Recipient" annotation: "Comments" @@ -216,11 +189,8 @@ perHour: "Per Hour" perDay: "Per Day" stopActivityDelivery: "Stop sending activities" blockThisInstance: "Block this instance" -silenceThisInstance: "Silence this instance" -mediaSilenceThisInstance: "Media-silence this server" operations: "Operations" software: "Software" -softwareName: "Software" version: "Version" metadata: "Metadata" withNFiles: "{n} file(s)" @@ -237,13 +207,7 @@ clearQueueConfirmText: "Any undelivered notes remaining in the queue will not be clearCachedFiles: "Clear cache" clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?" blockedInstances: "Blocked Instances" -blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance." -silencedInstances: "Silenced instances" -silencedInstancesDescription: "List the host names of the servers that you want to silence, separated by a new line. All accounts belonging to the listed servers will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked servers." -mediaSilencedInstances: "Media-silenced servers" -mediaSilencedInstancesDescription: "List the host names of the servers that you want to media-silence, separated by a new line. All accounts belonging to the listed servers will be treated as sensitive, and can't use custom emojis. This will not affect the blocked servers." -federationAllowedHosts: "Federation allowed servers" -federationAllowedHostsDescription: "Specify the hostnames of the servers you want to allow federation separated by line breaks." +blockedInstancesDescription: "List the hostnames of the instances that you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance." muteAndBlock: "Mutes and Blocks" mutedUsers: "Muted users" blockedUsers: "Blocked users" @@ -251,6 +215,7 @@ noUsers: "There are no users" editProfile: "Edit profile" noteDeleteConfirm: "Are you sure you want to delete this note?" pinLimitExceeded: "You cannot pin any more notes" +intro: "Installation of Misskey has been finished! Please create an admin user." done: "Done" processing: "Processing..." preview: "Preview" @@ -287,8 +252,8 @@ removed: "Successfully deleted" removeAreYouSure: "Are you sure that you want to remove \"{x}\"?" deleteAreYouSure: "Are you sure that you want to delete \"{x}\"?" resetAreYouSure: "Really reset?" -areYouSure: "Are you sure?" saved: "Saved" +messaging: "Chat" upload: "Upload" keepOriginalUploading: "Keep original image" keepOriginalUploadingDescription: "Saves the originally uploaded image as-is. If turned off, a version to display on the web will be generated on upload." @@ -298,11 +263,10 @@ uploadFromUrl: "Upload from a URL" uploadFromUrlDescription: "URL of the file you want to upload" uploadFromUrlRequested: "Upload requested" uploadFromUrlMayTakeTime: "It may take some time until the upload is complete." -uploadNFiles: "Upload {n} files" explore: "Explore" messageRead: "Read" noMoreHistory: "There is no further history" -startChat: "Start chat" +startMessaging: "Start a new chat" nUsersRead: "read by {n}" agreeTo: "I agree to {0}" agree: "Agree" @@ -327,27 +291,23 @@ dark: "Dark" lightThemes: "Light themes" darkThemes: "Dark themes" syncDeviceDarkMode: "Sync Dark Mode with your device settings" -switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" is turned on, Would you like to turn off synchronization and switch modes manually?" drive: "Drive" fileName: "Filename" selectFile: "Select a file" selectFiles: "Select files" selectFolder: "Select a folder" selectFolders: "Select folders" -fileNotSelected: "No file selected" renameFile: "Rename file" folderName: "Folder name" createFolder: "Create a folder" renameFolder: "Rename this folder" deleteFolder: "Delete this folder" -folder: "Folder" addFile: "Add a file" -showFile: "Show files" emptyDrive: "Your Drive is empty" emptyFolder: "This folder is empty" unableToDelete: "Unable to delete" inputNewFileName: "Enter a new filename" -inputNewDescription: "Enter new alt text" +inputNewDescription: "Enter new caption" inputNewFolderName: "Enter a new folder name" circularReferenceFolder: "The destination folder is a subfolder of the folder you wish to move." hasChildFilesOrFolders: "Since this folder is not empty, it can not be deleted." @@ -385,10 +345,12 @@ enableLocalTimeline: "Enable local timeline" enableGlobalTimeline: "Enable global timeline" disablingTimelinesInfo: "Adminstrators and Moderators will always have access to all timelines, even if they are not enabled." registration: "Register" +enableRegistration: "Enable new user registration" invite: "Invite" driveCapacityPerLocalAccount: "Drive capacity per local user" driveCapacityPerRemoteAccount: "Drive capacity per remote user" inMb: "In megabytes" +iconUrl: "Icon URL" bannerUrl: "Banner image URL" backgroundImageUrl: "Background image URL" basicInfo: "Basic info" @@ -402,11 +364,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Enable hCaptcha" hcaptchaSiteKey: "Site key" hcaptchaSecretKey: "Secret key" -mcaptcha: "mCaptcha" -enableMcaptcha: "Enable mCaptcha" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret key" -mcaptchaInstanceUrl: "mCaptcha server URL" recaptcha: "reCAPTCHA" enableRecaptcha: "Enable reCAPTCHA" recaptchaSiteKey: "Site key" @@ -422,11 +379,9 @@ name: "Name" antennaSource: "Antenna source" antennaKeywords: "Keywords to listen to" antennaExcludeKeywords: "Keywords to exclude" -antennaExcludeBots: "Exclude bot accounts" antennaKeywordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." notifyAntenna: "Notify about new notes" withFileAntenna: "Only notes with files" -excludeNotesInSensitiveChannel: "Exclude notes from sensitive channels" enableServiceworker: "Enable Push-Notifications for your Browser" antennaUsersDescription: "List one username per line" caseSensitive: "Case sensitive" @@ -451,15 +406,10 @@ aboutMisskey: "About Misskey" administrator: "Administrator" token: "Token" 2fa: "Two-factor authentication" -setupOf2fa: "Setup two-factor authentification" totp: "Authenticator App" totpDescription: "Use an authenticator app to enter one-time passwords" moderator: "Moderator" moderation: "Moderation" -moderationNote: "Moderation note" -moderationNoteDescription: "You can fill in notes that will be shared only among moderators." -addModerationNote: "Add moderation note" -moderationLogs: "Moderation logs" nUsersMentioned: "Mentioned by {n} users" securityKeyAndPasskey: "Security- and passkeys" securityKey: "Security key" @@ -475,6 +425,7 @@ share: "Share" notFound: "Not found" notFoundDescription: "No page corresponding to this URL could be found." uploadFolder: "Default folder for uploads" +cacheClear: "Clear cache" markAsReadAllNotifications: "Mark all notifications as read" markAsReadAllUnreadNotes: "Mark all notes as read" markAsReadAllTalkMessages: "Mark all messages as read" @@ -492,10 +443,10 @@ retype: "Enter again" noteOf: "Note by {user}" quoteAttached: "Quote" quoteQuestion: "Append as quote?" -attachAsFileQuestion: "The text in clipboard is long. Would you want to attach it as text file?" +noMessagesYet: "No messages yet" +newMessageExists: "There are new messages" onlyOneFileCanBeAttached: "You can only attach one file to a message" signinRequired: "Please register or sign in before continuing" -signinOrContinueOnRemote: "To continue, you need to move your server or sign up / log in to this server." invitations: "Invites" invitationCode: "Invitation code" checking: "Checking..." @@ -517,12 +468,8 @@ uiLanguage: "User interface language" aboutX: "About {x}" emojiStyle: "Emoji style" native: "Native" -menuStyle: "Menu style" -style: "Style" -drawer: "Drawer" -popup: "Pop up" +disableDrawer: "Don't use drawer-style menus" showNoteActionsOnlyHover: "Only show note actions on hover" -showReactionsCount: "See the number of reactions in notes" noHistory: "No history available" signinHistory: "Login history" enableAdvancedMfm: "Enable advanced MFM" @@ -575,22 +522,16 @@ serverLogs: "Server logs" deleteAll: "Delete all" showFixedPostForm: "Display the posting form at the top of the timeline" showFixedPostFormInChannel: "Display the posting form at the top of the timeline (Channels)" -withRepliesByDefaultForNewlyFollowed: "Include replies by newly followed users in the timeline by default" newNoteRecived: "There are new notes" -newNote: "New Note" sounds: "Sounds" sound: "Sounds" -notificationSoundSettings: "Notification sound settings" listen: "Listen" none: "None" showInPage: "Show in page" popout: "Pop-out" volume: "Volume" masterVolume: "Master volume" -notUseSound: "Disable sound" -useSoundOnlyWhenActive: "Output sounds only if Misskey is active" details: "Details" -renoteDetails: "Renote details" chooseEmoji: "Select an emoji" unableToProcess: "The operation could not be completed" recentUsed: "Recently used" @@ -606,16 +547,10 @@ ascendingOrder: "Ascending" descendingOrder: "Descending" scratchpad: "Scratchpad" scratchpadDescription: "The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with Misskey in it." -uiInspector: "UI inspector" -uiInspectorDescription: "You can see the UI component server list on memory. UI component will be generated by Ui:C: function." output: "Output" script: "Script" disablePagesScript: "Disable AiScript on Pages" updateRemoteUser: "Update remote user information" -unsetUserAvatar: "Unset avatar" -unsetUserAvatarConfirm: "Are you sure you want to unset the avatar?" -unsetUserBanner: "Unset banner" -unsetUserBannerConfirm: "Are you sure you want to unset the banner?" deleteAllFiles: "Delete all files" deleteAllFilesConfirm: "Are you sure that you want to delete all files?" removeAllFollowing: "Unfollow all followed users" @@ -640,16 +575,16 @@ serviceworkerInfo: "Must be enabled for push notifications." deletedNote: "Deleted note" invisibleNote: "Invisible note" enableInfiniteScroll: "Automatically load more" -visibility: "Visibility" +visibility: "Visiblility" poll: "Poll" useCw: "Hide content" enablePlayer: "Open video player" disablePlayer: "Close video player" -expandTweet: "Expand post" +expandTweet: "Expand tweet" themeEditor: "Theme editor" description: "Description" -describeFile: "Add alt text" -enterFileDescription: "Enter alt text" +describeFile: "Add caption" +enterFileDescription: "Enter caption" author: "Author" leaveConfirm: "There are unsaved changes. Do you want to discard them?" manage: "Management" @@ -666,7 +601,6 @@ medium: "Medium" small: "Small" generateAccessToken: "Generate access token" permission: "Permissions" -adminPermission: "Admin Permissions" enableAll: "Enable all" disableAll: "Disable all" tokenRequested: "Grant access to account" @@ -683,24 +617,18 @@ smtpHost: "Host" smtpPort: "Port" smtpUser: "Username" smtpPass: "Password" -emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP authentication" +emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP verification" smtpSecure: "Use implicit SSL/TLS for SMTP connections" smtpSecureInfo: "Turn this off when using STARTTLS" testEmail: "Test email delivery" wordMute: "Word mute" -wordMuteDescription: "Minimize notes that contain the specified word or phrase. Minimized notes can be displayed by clicking on them." -hardWordMute: "Hard word mute" -showMutedWord: "Show muted words" -hardWordMuteDescription: "Hide notes that contain the specified word or phrase. Unlike word mute, the note will be completely hidden from view." regexpError: "Regular Expression error" regexpErrorDescription: "An error occurred in the regular expression on line {line} of your {tab} word mutes:" instanceMute: "Instance Mutes" userSaysSomething: "{name} said something" -userSaysSomethingAbout: "{name} said something about \"{word}\"" makeActive: "Activate" display: "Display" copy: "Copy" -copiedToClipboard: "Copied to clipboard" metrics: "Metrics" overview: "Overview" logs: "Logs" @@ -715,21 +643,22 @@ useGlobalSettingDesc: "If turned on, your account's notification settings will b other: "Other" regenerateLoginToken: "Regenerate login token" regenerateLoginTokenDescription: "Regenerates the token used internally during login. Normally this action is not necessary. If regenerated, all devices will be logged out." -theKeywordWhenSearchingForCustomEmoji: "This is the keyword when searching for custom emojis." setMultipleBySeparatingWithSpace: "Separate multiple entries with spaces." fileIdOrUrl: "File ID or URL" behavior: "Behavior" sample: "Sample" abuseReports: "Reports" reportAbuse: "Report" -reportAbuseRenote: "Report renote" reportAbuseOf: "Report {name}" fillAbuseReportDescription: "Please fill in details regarding this report. If it is about a specific note, please include its URL." abuseReported: "Your report has been sent. Thank you very much." reporter: "Reporter" reporteeOrigin: "Reportee Origin" reporterOrigin: "Reporter Origin" +forwardReport: "Forward report to remote instance" +forwardReportIsAnonymous: "Instead of your account, an anonymous system account will be displayed as reporter at the remote instance." send: "Send" +abuseMarkAsResolved: "Mark report as resolved" openInNewTab: "Open in new tab" openInSideView: "Open in side view" defaultNavigationBehaviour: "Default navigation behavior" @@ -747,7 +676,6 @@ createNewClip: "Create new clip" unclip: "Unclip" confirmToUnclipAlreadyClippedNote: "This note is already part of the \"{name}\" clip. Do you want to remove it from this clip instead?" public: "Public" -private: "Private" i18nInfo: "Misskey is being translated into various languages by volunteers. You can help at {link}." manageAccessTokens: "Manage access tokens" accountInfo: "Account Info" @@ -772,7 +700,6 @@ lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", yo alwaysMarkSensitive: "Mark as sensitive by default" loadRawImages: "Load original images instead of showing thumbnails" disableShowingAnimatedImages: "Don't play animated images" -highlightSensitiveMedia: "Highlight sensitive media" verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification." notSet: "Not set" emailVerified: "Email has been verified" @@ -788,6 +715,7 @@ thisIsExperimentalFeature: "This is an experimental feature. Its functionality i developer: "Developer" makeExplorable: "Make account visible in \"Explore\"" makeExplorableDescription: "If you turn this off, your account will not show up in the \"Explore\" section." +showGapBetweenNotesInTimeline: "Show a gap between posts on the timeline" duplicate: "Duplicate" left: "Left" center: "Center" @@ -795,7 +723,6 @@ wide: "Wide" narrow: "Narrow" reloadToApplySetting: "This setting will only apply after a page reload. Reload now?" needReloadToApply: "A reload is required for this to be reflected." -needToRestartServerToApply: "A Misskey restart is required to reflect the change." showTitlebar: "Show title bar" clearCache: "Clear cache" onlineUsersCount: "{n} users are online" @@ -855,7 +782,7 @@ active: "Active" offline: "Offline" notRecommended: "Not recommended" botProtection: "Bot Protection" -instanceBlocking: "Blocked/Silenced Instances" +instanceBlocking: "Blocked Instances" selectAccount: "Select account" switchAccount: "Switch account" enabled: "Enabled" @@ -866,7 +793,6 @@ administration: "Management" accounts: "Accounts" switch: "Switch" noMaintainerInformationWarning: "Maintainer information is not configured." -noInquiryUrlWarning: "Inquiry URL isn’t set" noBotProtectionWarning: "Bot protection is not configured." configure: "Configure" postToGallery: "Create new gallery post" @@ -926,12 +852,11 @@ makeReactionsPublicDescription: "This will make the list of all your past reacti classic: "Classic" muteThread: "Mute thread" unmuteThread: "Unmute thread" -followingVisibility: "Visibility of follows" -followersVisibility: "Visibility of followers" +ffVisibility: "Follows/Followers Visibility" +ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you." continueThread: "View thread continuation" deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" incorrectPassword: "Incorrect password." -incorrectTotp: "The one-time password is incorrect or has expired." voteConfirm: "Confirm your vote for \"{choice}\"?" hide: "Hide" useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile" @@ -956,9 +881,6 @@ oneHour: "One hour" oneDay: "One day" oneWeek: "One week" oneMonth: "One month" -threeMonths: "3 months" -oneYear: "1 year" -threeDays: "3 days" reflectMayTakeTime: "It may take some time for this to be reflected." failedToFetchAccountInformation: "Could not fetch account information" rateLimitExceeded: "Rate limit exceeded" @@ -981,9 +903,8 @@ typeToConfirm: "Please enter {x} to confirm" deleteAccount: "Delete account" document: "Documentation" numberOfPageCache: "Number of cached pages" -numberOfPageCacheDescription: "Increasing this number will improve convenience for but cause more load as more memory usage on the user's device." -logoutConfirm: "Are you sure you want to log out?" -logoutWillClearClientData: "Logging out will erase the settings of the client from the browser. In order to be able to restore the settings upon logging in again, you must enable automatic backup of your settings." +numberOfPageCacheDescription: "Increasing this number will improve convenience for users but cause more server load as well as more memory to be used." +logoutConfirm: "Really log out?" lastActiveDate: "Last used at" statusbar: "Status bar" pleaseSelect: "Select an option" @@ -1002,7 +923,6 @@ failedToUpload: "Upload failed" cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially inappropriate." cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity." cannotUploadBecauseExceedsFileSizeLimit: "This file cannot be uploaded as it exceeds the file size limit." -cannotUploadBecauseUnallowedFileType: "Unable to upload due to unauthorized file type." beta: "Beta" enableAutoSensitive: "Automatic marking as sensitive" enableAutoSensitiveDescription: "Allows automatic detection and marking of sensitive media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide." @@ -1021,7 +941,7 @@ sendPushNotificationReadMessageCaption: "This may increase the power consumption windowMaximize: "Maximize" windowMinimize: "Minimize" windowRestore: "Restore" -caption: "Alt text" +caption: "Caption" loggedInAsBot: "Currently logged in as bot" tools: "Tools" cannotLoad: "Unable to load" @@ -1034,7 +954,6 @@ neverShow: "Don't show again" remindMeLater: "Maybe later" didYouLikeMisskey: "Have you taken a liking to Misskey?" pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!" -correspondingSourceIsAvailable: "The corresponding source code is available at {anchor}" roles: "Roles" role: "Role" noRole: "Role not found" @@ -1044,7 +963,6 @@ assign: "Assign" unassign: "Unassign" color: "Color" manageCustomEmojis: "Manage Custom Emojis" -manageAvatarDecorations: "Manage avatar decorations" youCannotCreateAnymore: "You've hit the creation limit." cannotPerformTemporary: "Temporarily unavailable" cannotPerformTemporaryDescription: "This action cannot be performed temporarily due to exceeding the execution limit. Please wait for a while and then try again." @@ -1062,7 +980,6 @@ thisPostMayBeAnnoyingHome: "Post to home timeline" thisPostMayBeAnnoyingCancel: "Cancel" thisPostMayBeAnnoyingIgnore: "Post anyway" collapseRenotes: "Collapse renotes you've already seen" -collapseRenotesDescription: "Collapse notes that you've reacted to or renoted before." internalServerError: "Internal Server Error" internalServerErrorDescription: "The server has run into an unexpected error." copyErrorInfo: "Copy error details" @@ -1086,11 +1003,6 @@ resetPasswordConfirm: "Really reset your password?" sensitiveWords: "Sensitive words" sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks." sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression." -prohibitedWords: "Prohibited words" -prohibitedWordsDescription: "Enables an error when attempting to post a note containing the set word(s). Multiple words can be set, separated by a new line." -prohibitedWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression." -hiddenTags: "Hidden hashtags" -hiddenTagsDescription: "Select tags which will not shown on trend list.\nMultiple tags could be registered by lines." notesSearchNotAvailable: "Note search is unavailable." license: "License" unfavoriteConfirm: "Really remove from favorites?" @@ -1101,15 +1013,11 @@ retryAllQueuesConfirmTitle: "Really retry all?" retryAllQueuesConfirmText: "This will temporarily increase the server load." enableChartsForRemoteUser: "Generate remote user data charts" enableChartsForFederatedInstances: "Generate remote instance data charts" -enableStatsForFederatedInstances: "Receive remote server stats" showClipButtonInNoteFooter: "Add \"Clip\" to note action menu" -reactionsDisplaySize: "Reaction display size" -limitWidthOfReaction: "Limit the maximum width of reactions and display them in reduced size." +largeNoteReactions: "Enlargen displayed reactions" noteIdOrUrl: "Note ID or URL" video: "Video" videos: "Videos" -audio: "Audio" -audioFiles: "Audio" dataSaver: "Data Saver" accountMigration: "Account Migration" accountMoved: "This user has moved to a new account:" @@ -1130,15 +1038,13 @@ vertical: "Vertical" horizontal: "Horizontal" position: "Position" serverRules: "Server rules" -pleaseConfirmBelowBeforeSignup: "To register on this server, you must review and agree to the following:" +pleaseConfirmBelowBeforeSignup: "Please confirm the below before signing up." pleaseAgreeAllToContinue: "You must agree to all above fields to continue." continue: "Continue" preservedUsernames: "Reserved usernames" preservedUsernamesDescription: "List usernames to reserve separated by linebreaks. These will become unable during normal account creation, but can be used by administrators to manually create accounts. Already existing accounts using these usernames will not be affected." createNoteFromTheFile: "Compose note from this file" archive: "Archive" -archived: "Archived" -unarchive: "Unarchive" channelArchiveConfirmTitle: "Really archive {name}?" channelArchiveConfirmDescription: "An archived channel won't appear in the channel list or search results anymore. New posts can also not be added to it anymore." thisChannelArchived: "This channel has been archived." @@ -1149,9 +1055,6 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)" preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored." options: "Options" specifyUser: "Specific user" -lookupConfirm: "Do you want to look up?" -openTagPageConfirm: "Do you want to open a hashtag page?" -specifyHost: "Specific host" failedToPreviewUrl: "Could not preview" update: "Update" rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction" @@ -1167,366 +1070,6 @@ branding: "Branding" enableServerMachineStats: "Publish server hardware stats" enableIdenticonGeneration: "Enable user identicon generation" turnOffToImprovePerformance: "Turning this off can increase performance." -createInviteCode: "Generate invite" -createWithOptions: "Generate with options" -createCount: "Invite count" -inviteCodeCreated: "Invite generated" -inviteLimitExceeded: "You've exceeded the limit of invites you can generate." -createLimitRemaining: "Invite limit: {limit} remaining" -inviteLimitResetCycle: "This limit will reset to {limit} at {time}." -expirationDate: "Expiration date" -noExpirationDate: "No expiration" -inviteCodeUsedAt: "Invite code used at" -registeredUserUsingInviteCode: "Invite used by" -waitingForMailAuth: "Email verification pending" -inviteCodeCreator: "Invite created by" -usedAt: "Used at" -unused: "Unused" -used: "Used" -expired: "Expired" -doYouAgree: "Agree?" -beSureToReadThisAsItIsImportant: "Please read this important information." -iHaveReadXCarefullyAndAgree: "I have read the text \"{x}\" and agree." -dialog: "Dialog" -icon: "Icon" -forYou: "For you" -currentAnnouncements: "Current announcements" -pastAnnouncements: "Past announcements" -youHaveUnreadAnnouncements: "There are unread announcements." -useSecurityKey: "Please follow your browser's or device's instructions to use your security- or passkey." -replies: "Reply" -renotes: "Renotes" -loadReplies: "Show replies" -loadConversation: "Show conversation" -pinnedList: "Pinned list" -keepScreenOn: "Keep screen on" -verifiedLink: "Link ownership has been verified" -notifyNotes: "Notify about new notes" -unnotifyNotes: "Stop notifying about new notes" -authentication: "Authentication" -authenticationRequiredToContinue: "Please authenticate to continue" -dateAndTime: "Timestamp" -showRenotes: "Show renotes" -edited: "Edited" -notificationRecieveConfig: "Notification Settings" -mutualFollow: "Mutual follow" -followingOrFollower: "Following or follower" -fileAttachedOnly: "Only notes with files" -showRepliesToOthersInTimeline: "Show replies to others in timeline" -hideRepliesToOthersInTimeline: "Hide replies to others from timeline" -showRepliesToOthersInTimelineAll: "Show replies to others from everyone you follow in timeline" -hideRepliesToOthersInTimelineAll: "Hide replies to others from everyone you follow in timeline" -confirmShowRepliesAll: "This operation is irreversible. Would you really like to show replies to others from everyone you follow in your timeline?" -confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?" -externalServices: "External Services" -sourceCode: "Source code" -sourceCodeIsNotYetProvided: "Source code is not yet available. Contact the administrator to fix this problem." -repositoryUrl: "Repository URL" -repositoryUrlDescription: "If you are using Misskey as is (without any changes to the source code), enter https://github.com/misskey-dev/misskey" -repositoryUrlOrTarballRequired: "If you have not published a repository, you must provide a tarball instead. See .config/example.yml for more information." -feedback: "Feedback" -feedbackUrl: "Feedback URL" -impressum: "Impressum" -impressumUrl: "Impressum URL" -impressumDescription: "In some countries, like germany, the inclusion of operator contact information (an Impressum) is legally required for commercial websites." -privacyPolicy: "Privacy Policy" -privacyPolicyUrl: "Privacy Policy URL" -tosAndPrivacyPolicy: "Terms of Service and Privacy Policy" -avatarDecorations: "Avatar decorations" -attach: "Attach" -detach: "Remove" -detachAll: "Remove All" -angle: "Angle" -flip: "Flip" -showAvatarDecorations: "Show avatar decorations" -releaseToRefresh: "Release to refresh" -refreshing: "Refreshing..." -pullDownToRefresh: "Pull down to refresh" -useGroupedNotifications: "Display grouped notifications" -signupPendingError: "There was a problem verifying the email address. The link may have expired." -cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided." -doReaction: "Add reaction" -code: "Code" -reloadRequiredToApplySettings: "Reloading is required to apply the settings." -remainingN: "Remaining: {n}" -overwriteContentConfirm: "Are you sure you want to overwrite the current content?" -seasonalScreenEffect: "Seasonal Screen Effect" -decorate: "Decorate" -addMfmFunction: "Add MFM" -enableQuickAddMfmFunction: "Show advanced MFM picker" -bubbleGame: "Bubble Game" -sfx: "Sound Effects" -soundWillBePlayed: "Sound will be played" -showReplay: "View Replay" -replay: "Replay" -replaying: "Showing replay" -endReplay: "Exit Replay" -copyReplayData: "Copy replay data" -ranking: "Ranking" -lastNDays: "Last {n} days" -backToTitle: "Go back to title" -hemisphere: "Where you live" -withSensitive: "Include notes with sensitive files" -userSaysSomethingSensitive: "Post by {name} contains sensitive content" -enableHorizontalSwipe: "Swipe to switch tabs" -loading: "Loading" -surrender: "Cancel" -gameRetry: "Retry" -notUsePleaseLeaveBlank: "Leave blank if not used" -useTotp: "Enter the One-Time Password" -useBackupCode: "Use the backup codes" -launchApp: "Launch the app" -useNativeUIForVideoAudioPlayer: "Use UI of browser when play video and audio\n" -keepOriginalFilename: "Keep original file name" -keepOriginalFilenameDescription: "If you turn off this setting, files names will be replaced with random string automatically when you upload files." -noDescription: "There is no explanation" -alwaysConfirmFollow: "Always confirm when following" -inquiry: "Contact" -tryAgain: "Please try again later" -confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media" -sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?" -createdLists: "Created lists" -createdAntennas: "Created antennas" -fromX: "From {x}" -genEmbedCode: "Generate embed code" -noteOfThisUser: "Notes by this user" -clipNoteLimitExceeded: "No more notes can be added to this clip." -performance: "Performance" -modified: "Modified" -discard: "Discard" -thereAreNChanges: "There are {n} change(s)" -signinWithPasskey: "Sign in with Passkey" -unknownWebAuthnKey: "Unknown Passkey" -passkeyVerificationFailed: "Passkey verification has failed." -passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled." -messageToFollower: "Message to followers" -target: "Target" -testCaptchaWarning: "This function is intended for CAPTCHA testing purposes.\nDo not use in a production environment." -prohibitedWordsForNameOfUser: "Prohibited words for user names" -prohibitedWordsForNameOfUserDescription: "If any of the strings in this list are included in the user's name, the name will be denied. Users with moderator privileges are not affected by this restriction." -yourNameContainsProhibitedWords: "Your name contains prohibited words" -yourNameContainsProhibitedWordsDescription: "If you wish to use this name, please contact your server administrator." -thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require login to view" -lockdown: "Lockdown" -pleaseSelectAccount: "Select an account" -availableRoles: "Available roles" -acknowledgeNotesAndEnable: "Turn on after understanding the precautions." -federationSpecified: "This server is operated in a whitelist federation. Interacting with servers other than those designated by the administrator is not allowed." -federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers." -confirmOnReact: "Confirm when reacting" -reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?" -markAsSensitiveConfirm: "Do you want to set this media as sensitive?" -unmarkAsSensitiveConfirm: "Do you want to remove the sensitive designation for this media?" -preferences: "Preferences" -accessibility: "Accessibility" -preferencesProfile: "Preferences profile" -copyPreferenceId: "Copy the preference ID" -resetToDefaultValue: "Revert to default" -overrideByAccount: "Override by the account" -untitled: "Untitled" -noName: "No name" -skip: "Skip" -restore: "Restore" -syncBetweenDevices: "Sync between devices" -preferenceSyncConflictTitle: "The configured value exists on the server." -preferenceSyncConflictText: "The sync enabled settings will save their values to the server. However, there are existing values on the server. Which set of values would you like to overwrite?" -preferenceSyncConflictChoiceMerge: "Merge" -preferenceSyncConflictChoiceServer: "Configured value on server" -preferenceSyncConflictChoiceDevice: "Configured value on device" -preferenceSyncConflictChoiceCancel: "Cancel enabling sync" -paste: "Paste" -emojiPalette: "Emoji palette" -postForm: "Posting form" -textCount: "Character count" -information: "About" -chat: "Chat" -migrateOldSettings: "Migrate old client settings" -migrateOldSettings_description: "This should be done automatically but if for some reason the migration was not successful, you can trigger the migration process yourself manually. The current configuration information will be overwritten." -compress: "Compress" -right: "Right" -bottom: "Bottom" -top: "Top" -embed: "Embed" -settingsMigrating: "Settings are being migrated, please wait a moment... (You can also migrate manually later by going to Settings→Others→Migrate old settings)" -readonly: "Read only" -goToDeck: "Return to Deck" -federationJobs: "Federation Jobs" -driveAboutTip: "In Drive, a list of files you've uploaded in the past will be displayed.
\nYou can reuse these files when attaching them to notes, or you can upload files in advance to post later.
\nBe careful when deleting a file, as it will not be available in all places where it was used (such as notes, pages, avatars, banners, etc.).
\nYou can also create folders to organize your files." -scrollToClose: "Scroll to close" -advice: "Advice" -realtimeMode: "Real-time mode" -turnItOn: "Turn on" -turnItOff: "Turn off" -emojiMute: "Mute emoji" -emojiUnmute: "Unmute emoji" -muteX: "Mute {x}" -unmuteX: "Unmute {x}" -abort: "Abort" -tip: "Tips & Tricks" -redisplayAllTips: "Show all “Tips & Tricks” again" -hideAllTips: "Hide all \"Tips & Tricks\"" -_chat: - noMessagesYet: "No messages yet" - newMessage: "New message" - individualChat: "Private Chat" - individualChat_description: "Have a private chat with another person." - roomChat: "Room Chat" - roomChat_description: "A chat room which can have multiple people.\nYou can also invite people who don't allow private chats if they accept the invite." - createRoom: "Create Room" - inviteUserToChat: "Invite users to start chatting" - yourRooms: "Created rooms" - joiningRooms: "Joined rooms" - invitations: "Invite" - noInvitations: "No invitations" - history: "History" - noHistory: "No history available" - noRooms: "No rooms found" - inviteUser: "Invite Users" - sentInvitations: "Sent Invites" - join: "Join" - ignore: "Ignore" - leave: "Leave room" - members: "Members" - searchMessages: "Search messages" - home: "Home" - send: "Send" - newline: "New line" - muteThisRoom: "Mute room" - deleteRoom: "Delete room" - chatNotAvailableForThisAccountOrServer: "Chat is not enabled on this server or for this account." - chatIsReadOnlyForThisAccountOrServer: "Chat is read-only on this instance or this account. You cannot write new messages or create/join chat rooms." - chatNotAvailableInOtherAccount: "The chat function is disabled for the other user." - cannotChatWithTheUser: "Cannot start a chat with this user" - cannotChatWithTheUser_description: "Chat is either unavailable or the other party has not enabled chat." - youAreNotAMemberOfThisRoomButInvited: "You are not a participant in this room, but you have received an invitation. Please accept the invitation to join." - doYouAcceptInvitation: "Do you accept the invitation?" - chatWithThisUser: "Chat with user" - thisUserAllowsChatOnlyFromFollowers: "This user accepts chats from followers only." - thisUserAllowsChatOnlyFromFollowing: "This user accepts chats only from users they follow." - thisUserAllowsChatOnlyFromMutualFollowing: "This user only accepts chats from users who are mutual followers." - thisUserNotAllowedChatAnyone: "This user is not accepting chats from anyone." - chatAllowedUsers: "Who to allow chatting with" - chatAllowedUsers_note: "You can chat with anyone to whom you have sent a chat message regardless of this setting." - _chatAllowedUsers: - everyone: "Everyone" - followers: "Only your followers" - following: "Only users you are following" - mutual: "Mutual followers only" - none: "Nobody" -_emojiPalette: - palettes: "Palette" - enableSyncBetweenDevicesForPalettes: "Enable palette sync between devices" - paletteForMain: "Main palette" - paletteForReaction: "Reaction palette" -_settings: - driveBanner: "You can manage and configure the drive, check usage, and configure file upload settings." - pluginBanner: "You can extend client features with plugins. You can install plugins, configure and manage individually." - notificationsBanner: "You can configure the types and range of notifications from the server and push notifications." - api: "API" - webhook: "Webhook" - serviceConnection: "Service integration" - serviceConnectionBanner: "Manage and configure access tokens and Webhooks to integrate with external apps or services." - accountData: "Account data" - accountDataBanner: "Export and import to manage account data." - muteAndBlockBanner: "You can configure and manage settings to hide content and restrict actions from specific users." - accessibilityBanner: "You can personalize the client's visuals and behavior, and configure settings to optimize usage." - privacyBanner: "You can configure settings related to account privacy, such as content visibility, discoverability, and follow approval." - securityBanner: "You can configure settings related to account security, such as password, login methods, authentication apps, and Passkeys." - preferencesBanner: "You can configure the overall behavior of the client according to your preferences." - appearanceBanner: "You can configure the appearance and display settings for the client according to your preferences." - soundsBanner: "You can configure the sound settings for playback in the client." - timelineAndNote: "Timeline and note" - makeEveryTextElementsSelectable: "Make all text elements selectable" - makeEveryTextElementsSelectable_description: "Enabling this may reduce usability in some situations." - useStickyIcons: "Make icons follow while scrolling" - enableHighQualityImagePlaceholders: "Display placeholders for high quality images" - uiAnimations: "UI Animations" - showNavbarSubButtons: "Show sub-buttons on the navigation bar" - ifOn: "When turned on" - ifOff: "When turned off" - enableSyncThemesBetweenDevices: "Synchronize installed themes across devices" - enablePullToRefresh: "Pull to Refresh" - enablePullToRefresh_description: "When using a mouse, drag while pressing in the scroll wheel." - realtimeMode_description: "Establishes a connection with the server and updates content in real time. This may increase traffic and memory consumption." - contentsUpdateFrequency: "Frequency of content retrieval" - contentsUpdateFrequency_description: "The higher the value the more the content updates but it lowers the performance and increases the traffic and memory consumption." - contentsUpdateFrequency_description2: "When real-time mode is on, content is updated in real time regardless of this setting." - showUrlPreview: "Show URL preview" - _chat: - showSenderName: "Show sender's name" - sendOnEnter: "Press Enter to send" -_preferencesProfile: - profileName: "Profile name" - profileNameDescription: "Set a name that identifies this device." - profileNameDescription2: "Example: \"Main PC\", \"Smartphone\"" - manageProfiles: "Manage Profiles" -_preferencesBackup: - autoBackup: "Auto backup" - restoreFromBackup: "Restore from backup" - noBackupsFoundTitle: "No backups found" - noBackupsFoundDescription: "No auto-created backups were found, but if you have manually saved a backup file, you can import and restore it." - selectBackupToRestore: "Select a backup to restore" - youNeedToNameYourProfileToEnableAutoBackup: "A profile name must be set to enable auto backup." - autoPreferencesBackupIsNotEnabledForThisDevice: "Settings auto backup is not enabled on this device." - backupFound: "Settings backup is found" -_accountSettings: - requireSigninToViewContents: "Require sign-in to view contents" - requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information." - requireSigninToViewContentsDescription2: "Content will not be displayed in URL previews (OGP), embedded in web pages, or on servers that don't support note quotes." - requireSigninToViewContentsDescription3: "These restrictions may not apply to federated content from other remote servers." - makeNotesFollowersOnlyBefore: "Make past notes to be displayed only to followers" - makeNotesFollowersOnlyBeforeDescription: "While this feature is enabled, only followers can see notes past the set date and time or have been visible for a set time. When it is deactivated, the note publication status will also be restored." - makeNotesHiddenBefore: "Make past notes private" - makeNotesHiddenBeforeDescription: "While this feature is enabled, notes that are past the set date and time or have been visible only to you. When it is deactivated, the note publication status will also be restored." - mayNotEffectForFederatedNotes: "Notes federated to a remote server may not be affected." - mayNotEffectSomeSituations: "These restrictions are simplified. They may not apply in some situations, such as when viewing on a remote server or during moderation." - notesHavePassedSpecifiedPeriod: "Note that the specified time has passed" - notesOlderThanSpecifiedDateAndTime: "Notes before the specified date and time" -_abuseUserReport: - forward: "Forward" - forwardDescription: "Forward the report to a remote server as an anonymous system account." - resolve: "Resolve" - accept: "Accept" - reject: "Reject" - resolveTutorial: "If the report's content is legitimate, select \"Accept\" to mark it as resolved.\nIf the report's content is illegitimate, select \"Reject\" to ignore it." -_delivery: - status: "Delivery status" - stop: "Suspended" - resume: "Delivery resume" - _type: - none: "Publishing" - manuallySuspended: "Manually suspended" - goneSuspended: "Server is suspended due to server deletion" - autoSuspendedForNotResponding: "Server is suspended due to no responding" - softwareSuspended: "Suspended as this software is no longer being distributed to" -_bubbleGame: - howToPlay: "How to play" - hold: "Hold" - _score: - score: "Score" - scoreYen: "Amount of money earned" - highScore: "High score" - maxChain: "Maximum number of chains" - yen: "{yen} Yen" - estimatedQty: "{qty} Pieces" - scoreSweets: "{onigiriQtyWithUnit} Onigiri" - _howToPlay: - section1: "Adjust the position and drop the object into the box." - section2: "When two objects of the same type touch each other, they will change into a different object and you score points." - section3: "The game is over when objects overflow from the box. Aim for a high score by fusing objects together while you avoid overflowing the box!" -_announcement: - forExistingUsers: "Existing users only" - forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." - needConfirmationToRead: "Require separate read confirmation" - needConfirmationToReadDescription: "A separate prompt to confirm marking this announcement as read will be displayed if enabled. This announcement will also be excluded from any \"Mark all as read\" functionality." - end: "Archive announcement" - tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete." - readConfirmTitle: "Mark as read?" - readConfirmText: "This will mark the contents of \"{title}\" as read." - shouldNotBeUsedToPresentPermanentInfo: "It's best to use announcements to publish fresh and time-bound information, not for information that will be relevant in the long term." - dialogAnnouncementUxWarn: "Having two or more dialog-style notifications simultaneously can significantly impact the user experience, so please use them carefully." - silence: "No notification" - silenceDescription: "Turning this on will skip the notification of this announcement and the user won't need to read it." _initialAccountSetting: accountCreated: "Your account was successfully created!" letsStartAccountSetup: "For starters, let's set up your profile." @@ -1539,114 +1082,11 @@ _initialAccountSetting: pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device." initialAccountSettingCompleted: "Profile setup complete!" haveFun: "Enjoy {name}!" - youCanContinueTutorial: "You can proceed to a tutorial on how to use {name} (Misskey) or you can exit the setup here and start using it immediately." - startTutorial: "Start Tutorial" + ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Misskey), please visit {link}." skipAreYouSure: "Really skip profile setup?" laterAreYouSure: "Really do profile setup later?" -_initialTutorial: - launchTutorial: "Start Tutorial" - title: "Tutorial" - wellDone: "Well done!" - skipAreYouSure: "Quit Tutorial?" - _landing: - title: "Welcome to the Tutorial" - description: "Here, you can learn the basics of using Misskey and its features." - _note: - title: "What is a Note?" - description: "Posts on Misskey are called 'Notes.' Notes are arranged chronologically on the timeline and are updated in real-time." - reply: "Click on this button to reply to a message. It's also possible to reply to replies, continuing the conversation like a thread." - renote: "You can share that note to your own timeline. You can also quote them with your comments." - reaction: "You can add reactions to the Note. More details will be explained on the next page." - menu: "You can view Note details, copy links, and perform various other actions." - _reaction: - title: "What are Reactions?" - description: "Notes can be reacted to with various emojis. Reactions allow you to express nuances that may not be conveyed with just a 'like.'" - letsTryReacting: "Reactions can be added by clicking the '+' button on the note. Try reacting to this sample note!" - reactToContinue: "Add a reaction to proceed." - reactNotification: "You'll receive real-time notifications when someone reacts to your note." - reactDone: "You can undo a reaction by pressing the '-' button." - _timeline: - title: "The Concept of Timelines" - description1: "Misskey provides multiple timelines based on usage (some may not be available depending on the server's policies)." - home: "You can view notes from accounts you follow." - local: "You can view notes from all users on this server." - social: "Notes from the Home and Local timelines will be displayed." - global: "You can view notes from all connected servers." - description2: "You can switch between timelines at the top of the screen at any time." - description3: "Additionally, there are list timelines and channel timelines. For more details, please refer to {link}." - _postNote: - title: "Note Posting Settings" - description1: "When posting a note on Misskey, various options are available. The posting form looks like this." - _visibility: - description: "You can limit who can view your note." - public: "Your note will be visible for all users." - home: "Public only on the Home timeline. People visiting your profile, via followers, and through renotes can see it." - followers: "Visible to followers only. Only followers can see it and no one else, and it cannot be renoted by others." - direct: "Visible only to specified users, and the recipient will be notified. It can be used as an alternative to direct messaging." - doNotSendConfidencialOnDirect1: "Be careful when sending sensitive information!" - doNotSendConfidencialOnDirect2: "Administrators of the server can see what you write. Be careful with sensitive information when sending direct notes to users on untrusted servers." - localOnly: "Posting with this flag will not federate the note to other servers. Users on other servers will not be able to view these notes directly, regardless of the display settings above." - _cw: - title: "Content Warning" - description: "Instead of the body, the content written in 'comments' field will be displayed. Pressing \"read more\" will reveal the body." - _exampleNote: - cw: "This will surely make you hungry!" - note: "Just had a chocolate-glazed donut 🍩😋" - useCases: "This is used when following the server guidelines, for necessary notes, or for self-restriction of spoiler or sensitive text." - _howToMakeAttachmentsSensitive: - title: "How to Mark Attachments as Sensitive?" - description: "For attachments that are required by server guidelines or that should not be left intact, add a \"sensitive\" flag." - tryThisFile: "Try marking the image attached in this form as sensitive!" - _exampleNote: - note: "Oops, messed up opening the natto lid..." - method: "To mark an attachment as sensitive, click the file thumbnail, open the menu, and click \"Mark as Sensitive.\"" - sensitiveSucceeded: "When attaching files, please set sensitivities in accordance with the server guidelines." - doItToContinue: "Mark the attachment file as sensitive to proceed." - _done: - title: "You've completed the tutorial! 🎉" - description: "The functions introduced here are just a small part. For a more detailed understanding of using Misskey, please refer to {link}." -_timelineDescription: - home: "In the Home timeline, you can see notes from accounts you follow." - local: "In the Local timeline, you can see notes from all users on this server." - social: "The Social timeline displays notes from both the Home and Local timelines." - global: "In the Global timeline, you can see notes from all connected servers." _serverRules: description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended." -_serverSettings: - iconUrl: "Icon URL" - appIconDescription: "Specifies the icon to use when {host} is displayed as an app." - appIconUsageExample: "E.g. As PWA, or when displayed as a home screen bookmark on a phone" - appIconStyleRecommendation: "As the icon may be cropped to a square or circle, an icon with colored margin around the content is recommended." - appIconResolutionMustBe: "The minimum resolution is {resolution}." - manifestJsonOverride: "manifest.json Override" - shortName: "Short name" - shortNameDescription: "A shorthand for the instance's name that can be displayed if the full official name is long." - fanoutTimelineDescription: "Greatly increases performance of timeline retrieval and reduces load on the database when enabled. In exchange, memory usage of Redis will increase. Consider disabling this in case of low server memory or server instability." - fanoutTimelineDbFallback: "Fallback to database" - fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved." - reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase." - inquiryUrl: "Inquiry URL" - inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information." - openRegistration: "Make the account creation open" - openRegistrationWarning: "Opening registration carries risks. It is recommended to only enable it if you have a system in place to continuously monitor the server and respond immediately in case of any issues." - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "If no moderator activity is detected for a while, this setting will be automatically turned off to prevent spam." - deliverSuspendedSoftware: "Suspended Software" - deliverSuspendedSoftwareDescription: "You can specify a range of names and versions of the server's software to stop delivery for vulnerability or other reasons. This version information is provided by the server and is not guaranteed to be reliable. A semver range specification can be used to specify the version, but specifying >= 2024.3.1 will not include custom versions such as 2024.3.1-custom.0, so it is recommended that a prerelease specification be used, such as >= 2024.3.1-0" - singleUserMode: "Single user mode" - singleUserMode_description: "If you are the only user of this server, enabling this mode will optimize its performance." - signToActivityPubGet: "Sign ActivityPub GET requests" - signToActivityPubGet_description: "Normally, this should be enabled. Disabling it may improve issues related to federation, but on the other hand it could disable federation towards some other servers." - proxyRemoteFiles: "Proxy remote files" - proxyRemoteFiles_description: "When enabled, the server will proxy and serve remote files. This is useful for generating image thumbnails and protecting user privacy." - allowExternalApRedirect: "Allow redirects for queries via ActivityPub" - allowExternalApRedirect_description: "If enabled, other servers can query third-party content through this server but this may result in content spoofing." - userGeneratedContentsVisibilityForVisitor: "Visibility of user-generated content to guests" - userGeneratedContentsVisibilityForVisitor_description: "This is useful for preventing problems caused by inappropriate remote content that is not well moderated from being unintentionally published on the Internet via your own server." - userGeneratedContentsVisibilityForVisitor_description2: "Unconditionally publishing all content on the server to the Internet, including remote content received by the server is risky. This is especially important for guests who are unaware of the distributed nature of the content, as they may mistakenly believe that even remote content is content created by users on the server." - _userGeneratedContentsVisibilityForVisitor: - all: "Everything is public" - localOnly: "Only local content is published, remote content is kept private" - none: "Everything is private" _accountMigration: moveFrom: "Migrate another account to this one" moveFromSub: "Create alias to another account" @@ -1901,19 +1341,6 @@ _achievements: title: "Brain Diver" description: "Post the link to Brain Diver" flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "Test overflow" - description: "Trigger the notification test repeatedly within an extremely short time" - _tutorialCompleted: - title: "Misskey Elementary Course Diploma" - description: "Tutorial completed" - _bubbleGameExplodingHead: - title: "🤯" - description: "The biggest object in the bubble game" - _bubbleGameDoubleExplodingHead: - title: "Double🤯" - description: "Two of the biggest objects in the bubble game at the same time" - flavor: "You can fill a lunch box like this 🤯 🤯 a bit." _role: new: "New role" edit: "Edit role" @@ -1924,9 +1351,7 @@ _role: assignTarget: "Assignment type" descriptionOfAssignTarget: "Manual to manually change who is part of this role and who is not.\nConditional to have users be automatically assigned and removed from this role based on a condition." manual: "Manual" - manualRoles: "Manual roles" conditional: "Conditional" - conditionalRoles: "Conditional roles" condition: "Condition" isConditionalRole: "This is a conditional role." isPublic: "Public role" @@ -1943,8 +1368,6 @@ _role: descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled." displayOrder: "Position" descriptionOfDisplayOrder: "The higher the number, the higher its UI position." - preserveAssignmentOnMoveAccount: "Preserve role assignment during migration" - preserveAssignmentOnMoveAccount_description: "When turned on, this role will be carried over to the destination account when an account with this role is migrated." canEditMembersByModerator: "Allow moderators to edit the list of members for this role" descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." priority: "Priority" @@ -1956,17 +1379,10 @@ _role: gtlAvailable: "Can view the global timeline" ltlAvailable: "Can view the local timeline" canPublicNote: "Can send public notes" - mentionMax: "Maximum number of mentions in a note" canInvite: "Can create instance invite codes" - inviteLimit: "Invite limit" - inviteLimitCycle: "Invite limit cooldown" - inviteExpirationTime: "Invite expiration interval" canManageCustomEmojis: "Can manage custom emojis" - canManageAvatarDecorations: "Manage avatar decorations" driveCapacity: "Drive capacity" - maxFileSize: "Upload-able max file size" alwaysMarkNsfw: "Always mark files as NSFW" - canUpdateBioMedia: "Can edit an icon or a banner image" pinMax: "Maximum number of pinned notes" antennaMax: "Maximum number of antennas" wordMuteMax: "Maximum number of characters allowed in word mutes" @@ -1979,26 +1395,9 @@ _role: descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " canHideAds: "Can hide ads" canSearchNotes: "Usage of note search" - canUseTranslator: "Translator usage" - avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" - canImportAntennas: "Allow importing antennas" - canImportBlocking: "Allow importing blocking" - canImportFollowing: "Allow importing following" - canImportMuting: "Allow importing muting" - canImportUserLists: "Allow importing lists" - chatAvailability: "Allow Chat" - uploadableFileTypes: "Uploadable file types" - uploadableFileTypes_caption: "Specifies the allowed MIME/file types. Multiple MIME types can be specified by separating them with a new line, and wildcards can be specified with an asterisk (*). (e.g., image/*)" - uploadableFileTypes_caption2: "Some files types might fail to be detected. To allow such files, add {x} to the specification." _condition: - roleAssignedTo: "Assigned to manual roles" isLocal: "Local user" isRemote: "Remote user" - isCat: "Cat Users" - isBot: "Bot Users" - isSuspended: "Suspended user" - isLocked: "Private accounts" - isExplorable: "Effective user of \"make an account discoverable\"" createdLessThan: "Less than X has passed since account creation" createdMoreThan: "More than X has passed since account creation" followersLessThanOrEq: "Has X or fewer followers" @@ -2024,7 +1423,6 @@ _emailUnavailable: disposable: "Disposable email addresses may not be used" mx: "This email server is invalid" smtp: "This email server is not responding" - banned: "You cannot register with this email address" _ffVisibility: public: "Public" followers: "Visible to followers only" @@ -2045,10 +1443,6 @@ _ad: reduceFrequencyOfThisAd: "Show this ad less" hide: "Hide" timezoneinfo: "The day of the week is determined from the server's timezone." - adsSettings: "Ad settings" - notesPerOneAd: "Real-time update ad placement interval (Notes per ad)" - setZeroToDisable: "Set this value to 0 to disable real-time update ads" - adsTooClose: "The current ad interval may significantly worsen the user experience due to being too low." _forgotPassword: enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it." ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead." @@ -2067,8 +1461,6 @@ _plugin: install: "Install plugins" installWarn: "Please do not install untrustworthy plugins." manage: "Manage plugins" - viewSource: "View source" - viewLog: "Show log" _preferencesBackups: list: "Created backups" saveNew: "Save new backup" @@ -2098,13 +1490,10 @@ _aboutMisskey: contributors: "Main contributors" allContributors: "All contributors" source: "Source code" - original: "Original" - thisIsModifiedVersion: "{name} uses a modified version of the original Misskey." translation: "Translate Misskey" donate: "Donate to Misskey" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" patrons: "Patrons" - projectMembers: "Project members" _displayOfSensitiveMedia: respect: "Hide media marked as sensitive" ignore: "Display media marked as sensitive" @@ -2129,7 +1518,6 @@ _channel: notesCount: "{n} Notes" nameAndDescription: "Name and description" nameOnly: "Name only" - allowRenoteToExternal: "Allow renote and quote outside the channel" _menuDisplay: sideFull: "Side" sideIcon: "Side (Icons)" @@ -2139,6 +1527,11 @@ _wordMute: muteWords: "Muted words" muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription2: "Surround keywords with slashes to use regular expressions." + softDescription: "Hide notes that fulfil the set conditions from the timeline." + hardDescription: "Prevents notes fulfilling the set conditions from being added to the timeline. In addition, these notes will not be added to the timeline even if the conditions are changed." + soft: "Soft" + hard: "Hard" + mutedNotes: "Muted notes" _instanceMute: instanceMuteDescription: "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance." instanceMuteDescription2: "Separate with newlines" @@ -2153,7 +1546,6 @@ _theme: installed: "{name} has been installed" installedThemes: "Installed themes" builtinThemes: "Built-in themes" - instanceTheme: "Server theme" alreadyInstalled: "This theme is already installed" invalid: "The format of this theme is invalid" make: "Make a theme" @@ -2186,6 +1578,7 @@ _theme: header: "Header" navBg: "Sidebar background" navFg: "Sidebar text" + navHoverFg: "Sidebar text (Hover)" navActive: "Sidebar text (Active)" navIndicator: "Sidebar indicator" link: "Link" @@ -2202,28 +1595,30 @@ _theme: infoFg: "Information text" infoWarnBg: "Warning background" infoWarnFg: "Warning text" + cwBg: "CW button background" + cwFg: "CW button text" + cwHoverBg: "CW button background (Hover)" toastBg: "Notification background" toastFg: "Notification text" buttonBg: "Button background" buttonHoverBg: "Button background (Hover)" inputBorder: "Input field border" + listItemHoverBg: "List item background (Hover)" + driveFolderBg: "Drive folder background" + wallpaperOverlay: "Wallpaper overlay" badge: "Badge" messageBg: "Chat background" + accentDarken: "Accent (Darkened)" + accentLighten: "Accent (Lightened)" fgHighlighted: "Highlighted Text" _sfx: note: "New note" noteMy: "Own note" notification: "Notifications" - reaction: "On choosing a reaction" - chatMessage: "Chat Messages" -_soundSettings: - driveFile: "Use an audio file in Drive." - driveFileWarn: "Select an audio file from Drive." - driveFileTypeWarn: "This file is not supported" - driveFileTypeWarnDescription: "Select an audio file" - driveFileDurationWarn: "The audio is too long." - driveFileDurationWarnDescription: "Long audio may disrupt using Misskey. Still continue?" - driveFileError: "It couldn't load the sound. Please change the setting." + chat: "Chat" + chatBg: "Chat (Background)" + antenna: "Antennas" + channel: "Channel notifications" _ago: future: "Future" justNow: "Just now" @@ -2235,32 +1630,36 @@ _ago: monthsAgo: "{n}mo ago" yearsAgo: "{n}y ago" invalid: "None" -_timeIn: - seconds: "In {n}s" - minutes: "In {n}m" - hours: "In {n}h" - days: "In {n}d" - weeks: "In {n}w" - months: "In {n}mo" - years: "In {n}y" _time: second: "Second(s)" minute: "Minute(s)" hour: "Hour(s)" day: "Day(s)" +_timelineTutorial: + title: "How to use Misskey" + step1_1: "This is the \"timeline\". All \"notes\" submitted on {name} will be chronologically displayed here." + step1_2: "There are a few different timelines. For example, the \"Home timeline\" will contain notes of users you follow, and the \"Local timeline\" will contain notes from all users of {name}." + step2_1: "Let's try posting a note next. You can do so by pressing the button with a pencil icon." + step2_2: "How about writing a self-introduction, or just \"Hello {name}!\" if you don't feel like it?" + step3_1: "Finished posting your first note?" + step3_2: "Your first note should now be displayed on your timeline." + step4_1: "You can also attach \"Reactions\" to notes." + step4_2: "To attach a reaction, press the \"+\" mark on a note and choose an emoji you'd like to react with." _2fa: alreadyRegistered: "You have already registered a 2-factor authentication device." registerTOTP: "Register authenticator app" + passwordToTOTP: "Enter your password" step1: "First, install an authentication app (such as {a} or {b}) on your device." step2: "Then, scan the QR code displayed on this screen." - step2Uri: "Enter the following URI if you are using a desktop program" + step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app." + step2Url: "You can also enter this URL if you're using a desktop program:" step3Title: "Enter an authentication code" - step3: "Enter the authentication code (token) provided by your app to finish setup." - setupCompleted: "Setup complete" + step3: "Enter the token provided by your app to finish setup." step4: "From now on, any future login attempts will ask for such a login token." securityKeyNotSupported: "Your browser does not support security keys." registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key." securityKeyInfo: "Besides fingerprint or PIN authentication, you can also setup authentication via hardware security keys that support FIDO2 to further secure your account." + chromePasskeyNotSupported: "Chrome passkeys are currently not supported." registerSecurityKey: "Register a security or pass key" securityKeyName: "Enter a key name" tapSecurityKey: "Please follow your browser to register the security or pass key" @@ -2271,12 +1670,6 @@ _2fa: renewTOTPConfirm: "This will cause verification codes from your previous app to stop working" renewTOTPOk: "Reconfigure" renewTOTPCancel: "Cancel" - checkBackupCodesBeforeCloseThisWizard: "Before you close this window, please note the following backup codes." - backupCodes: "Backup codes" - backupCodesDescription: "You can use these codes to gain access to your account in case of becoming unable to use your two-factor authentificator app. Each can only be used once. Please keep them in a safe place." - backupCodeUsedWarning: "A backup code has been used. Please reconfigure two-factor authentification as soon as possible if you are no longer able to use it." - backupCodesExhaustedWarning: "All backup codes have been used. Should you lose access to your two-factor authentification app, you will be unable to access this account. Please reconfigure two-factor authentification." - moreDetailedGuideHere: "Here is detailed guide" _permissions: "read:account": "View your account information" "write:account": "Edit your account information" @@ -2298,10 +1691,10 @@ _permissions: "read:reactions": "View your reactions" "write:reactions": "Edit your reactions" "write:votes": "Vote on a poll" - "read:pages": "View your Pages" - "write:pages": "Edit or delete your Pages" - "read:page-likes": "View list of liked Pages" - "write:page-likes": "Edit list of liked Pages" + "read:pages": "View your pages" + "write:pages": "Edit or delete your pages" + "read:page-likes": "View your likes on pages" + "write:page-likes": "Edit your likes on pages" "read:user-groups": "View your user groups" "write:user-groups": "Edit or delete your user groups" "read:channels": "View your channels" @@ -2310,60 +1703,6 @@ _permissions: "write:gallery": "Edit your gallery" "read:gallery-likes": "View your list of liked gallery posts" "write:gallery-likes": "Edit your list of liked gallery posts" - "read:flash": "View Play" - "write:flash": "Edit Plays" - "read:flash-likes": "View list of liked Plays" - "write:flash-likes": "Edit list of liked Plays" - "read:admin:abuse-user-reports": "View user reports" - "write:admin:delete-account": "Delete user account" - "write:admin:delete-all-files-of-a-user": "Delete all files of a user" - "read:admin:index-stats": "View database index stats" - "read:admin:table-stats": "View database table stats" - "read:admin:user-ips": "View user IP addresses" - "read:admin:meta": "View instance metadata" - "write:admin:reset-password": "Reset user password" - "write:admin:resolve-abuse-user-report": "Resolve user report" - "write:admin:send-email": "Send email" - "read:admin:server-info": "View server info" - "read:admin:show-moderation-log": "View moderation log" - "read:admin:show-user": "View private user info" - "write:admin:suspend-user": "Suspend user" - "write:admin:unset-user-avatar": "Remove user avatar" - "write:admin:unset-user-banner": "Remove user banner" - "write:admin:unsuspend-user": "Unsuspend user" - "write:admin:meta": "Manage instance metadata" - "write:admin:user-note": "Manage moderation note" - "write:admin:roles": "Manage roles" - "read:admin:roles": "View roles" - "write:admin:relays": "Manage relays" - "read:admin:relays": "View relays" - "write:admin:invite-codes": "Manage invite codes" - "read:admin:invite-codes": "View invite codes" - "write:admin:announcements": "Manage announcements" - "read:admin:announcements": "View announcements" - "write:admin:avatar-decorations": "Can manage avatar decorations" - "read:admin:avatar-decorations": "View avatar decorations" - "write:admin:federation": "Manage federation data" - "write:admin:account": "Manage user account" - "read:admin:account": "View user account" - "write:admin:emoji": "Manage emoji" - "read:admin:emoji": "View emoji" - "write:admin:queue": "Manage job queue" - "read:admin:queue": "View job queue info" - "write:admin:promo": "Manage promotion notes" - "write:admin:drive": "Manage user drive" - "read:admin:drive": "View user drive info" - "read:admin:stream": "Use WebSocket API for Admin" - "write:admin:ad": "Manage ads" - "read:admin:ad": "View ads" - "write:invite-codes": "Create invite codes" - "read:invite-codes": "Get invite codes" - "write:clip-favorite": "Manage favorited clips" - "read:clip-favorite": "View favorited clips" - "read:federation": "Get federation data" - "write:report-abuse": "Report violation" - "write:chat": "Compose or delete chat messages" - "read:chat": "Browse Chat" _auth: shareAccessTitle: "Granting application permissions" shareAccess: "Would you like to authorize \"{name}\" to access this account?" @@ -2372,17 +1711,13 @@ _auth: permissionAsk: "This application requests the following permissions" pleaseGoBack: "Please go back to the application" callback: "Returning to the application" - accepted: "Access granted" denied: "Access denied" - scopeUser: "Operate as the following user" pleaseLogin: "Please log in to authorize applications." - byClickingYouWillBeRedirectedToThisUrl: "When access is granted, you will automatically be redirected to the following URL" _antennaSources: all: "All notes" homeTimeline: "Notes from followed users" users: "Notes from specific users" userList: "Notes from a specified list of users" - userBlacklist: "All notes except for those of one or more specified users" _weekday: sunday: "Sunday" monday: "Monday" @@ -2421,8 +1756,6 @@ _widgets: _userList: chooseList: "Select a list" clicker: "Clicker" - birthdayFollowings: "Today's Birthdays" - chat: "Chat" _cw: hide: "Hide" show: "Show content" @@ -2484,22 +1817,15 @@ _profile: metadataContent: "Content" changeAvatar: "Change avatar" changeBanner: "Change banner" - verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field." - avatarDecorationMax: "You can add up to {max} decorations." - followedMessage: "Message when you are followed" - followedMessageDescription: "You can set a short message to be displayed to the recipient when they follow you." - followedMessageDescriptionForLockedAccount: "If you have set up that follow requests require approval, this will be displayed when you grant a follow request." _exportOrImport: allNotes: "All notes" favoritedNotes: "Favorite notes" - clips: "Clip" followingList: "Followed users" muteList: "Muted users" blockingList: "Blocked users" userLists: "User lists" excludeMutingUsers: "Exclude muted users" excludeInactiveUsers: "Exclude inactive users" - withReplies: "Include replies from imported users in the timeline" _charts: federation: "Federation" apRequest: "Requests" @@ -2546,11 +1872,13 @@ _play: title: "Title" script: "Script" summary: "Description" - visibilityDescription: "Putting it private means it won't be visible on your profile, but anyone that has the URL can still access it." _pages: newPage: "Create a new Page" editPage: "Edit this Page" readPage: "Viewing this Page's source" + created: "Page successfully created" + updated: "Page successfully edited" + deleted: "Page successfully deleted" pageSetting: "Page settings" nameAlreadyExists: "The specified Page URL already exists" invalidNameTitle: "The specified Page URL is invalid" @@ -2578,7 +1906,6 @@ _pages: eyeCatchingImageSet: "Set thumbnail" eyeCatchingImageRemove: "Delete thumbnail" chooseBlock: "Add a block" - enterSectionTitle: "Enter a section title" selectType: "Select a type" contentBlocks: "Content" inputBlocks: "Input" @@ -2589,8 +1916,6 @@ _pages: section: "Section" image: "Images" button: "Button" - dynamic: "Dynamic Blocks" - dynamicDescription: "This block has been abolished. Please use {play} from now on." note: "Embedded note" _note: id: "Note ID" @@ -2610,28 +1935,11 @@ _notification: youReceivedFollowRequest: "You've received a follow request" yourFollowRequestAccepted: "Your follow request was accepted" pollEnded: "Poll results have become available" - newNote: "New note" unreadAntennaNote: "Antenna {name}" - roleAssigned: "Role given" - chatRoomInvitationReceived: "You have been invited to a chat room" emptyPushNotificationMessage: "Push notifications have been updated" achievementEarned: "Achievement unlocked" - testNotification: "Test notification" - checkNotificationBehavior: "Check notification appearance" - sendTestNotification: "Send test notification" - notificationWillBeDisplayedLikeThis: "Notifications look like this" - reactedBySomeUsers: "{n} users reacted" - likedBySomeUsers: "{n} users liked your note" - renotedBySomeUsers: "Renote from {n} users" - followedBySomeUsers: "Followed by {n} users" - flushNotification: "Clear notifications" - exportOfXCompleted: "Export of {x} has been completed" - login: "Someone logged in" - createToken: "An access token has been created" - createTokenDescription: "If you have no idea, delete the access token through \"{text}\"." _types: all: "All" - note: "New notes" follow: "New followers" mention: "Mentions" reply: "Replies" @@ -2641,13 +1949,7 @@ _notification: pollEnded: "Polls ending" receiveFollowRequest: "Received follow requests" followRequestAccepted: "Accepted follow requests" - roleAssigned: "Role given" - chatRoomInvitationReceived: "Invited to chat room" achievementEarned: "Achievement unlocked" - exportCompleted: "The export has been completed" - login: "Sign In" - createToken: "Create access token" - test: "Notification test" app: "Notifications from linked apps" _actions: followBack: "followed you back" @@ -2656,11 +1958,7 @@ _notification: _deck: alwaysShowMainColumn: "Always show main column" columnAlign: "Align columns" - columnGap: "Margin between columns" - deckMenuPosition: "Deck menu position" - navbarPosition: "Navigation bar position" addColumn: "Add column" - newNoteNotificationSettings: "Notification setting for new notes" configureColumn: "Column settings" swapLeft: "Swap with the left column" swapRight: "Swap with the right column" @@ -2672,12 +1970,8 @@ _deck: newProfile: "New profile" deleteProfile: "Delete profile" introduction: "Create the perfect interface for you by arranging columns freely!" - introduction2: "Click on the + on the right of the screen to add new columns whenever you want." + introduction2: "Click on the + on the right of the screen to add new colums whenever you want." widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget." - useSimpleUiForNonRootPages: "Use simple UI for navigated pages" - usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled" - flexible: "Auto-adjust width" - enableSyncBetweenDevicesForProfiles: "Enable profile information sync between devices" _columns: main: "Main" widgets: "Widgets" @@ -2689,7 +1983,6 @@ _deck: mentions: "Mentions" direct: "Direct notes" roleTimeline: "Role Timeline" - chat: "Chat" _dialog: charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}." charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}." @@ -2701,10 +1994,9 @@ _drivecleaner: orderByCreatedAtAsc: "Ascending Dates" _webhookSettings: createWebhook: "Create Webhook" - modifyWebhook: "Modify Webhook" name: "Name" secret: "Secret" - trigger: "Trigger" + events: "Webhook Events" active: "Enabled" _events: follow: "When following a user" @@ -2714,406 +2006,3 @@ _webhookSettings: renote: "When renoted" reaction: "When receiving a reaction" mention: "When being mentioned" - _systemEvents: - abuseReport: "When received a new report" - abuseReportResolved: "When resolved report" - userCreated: "When user is created" - inactiveModeratorsWarning: "When moderators have been inactive for a while" - inactiveModeratorsInvitationOnlyChanged: "When a moderator has been inactive for a while, and the server is changed to invitation-only" - deleteConfirm: "Are you sure you want to delete the Webhook?" - testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data." -_abuseReport: - _notificationRecipient: - createRecipient: "Add recipient for reports" - modifyRecipient: "Edit a recipient for reports" - recipientType: "Notification type" - _recipientType: - mail: "Email" - webhook: "Webhook" - _captions: - mail: "Send the email to moderators' email addresses when you receive reports." - webhook: "Send a notification to System Webhook when you receive or resolve reports." - keywords: "Keywords" - notifiedUser: "Users to notify" - notifiedWebhook: "Webhook to use" - deleteConfirm: "Are you sure that you want to delete the notification recipient?" -_moderationLogTypes: - createRole: "Role created" - deleteRole: "Role deleted" - updateRole: "Role updated" - assignRole: "Assigned to role" - unassignRole: "Removed from role" - suspend: "Suspended" - unsuspend: "Unsuspended" - addCustomEmoji: "Custom emoji added" - updateCustomEmoji: "Custom emoji updated" - deleteCustomEmoji: "Custom emoji deleted" - updateServerSettings: "Server settings updated" - updateUserNote: "Moderation note updated" - deleteDriveFile: "File deleted" - deleteNote: "Note deleted" - createGlobalAnnouncement: "Global announcement created" - createUserAnnouncement: "User announcement created" - updateGlobalAnnouncement: "Global announcement updated" - updateUserAnnouncement: "User announcement updated" - deleteGlobalAnnouncement: "Global announcement deleted" - deleteUserAnnouncement: "User announcement deleted" - resetPassword: "Password reset" - suspendRemoteInstance: "Remote instance suspended" - unsuspendRemoteInstance: "Remote instance unsuspended" - updateRemoteInstanceNote: "Moderation note updated for remote instance." - markSensitiveDriveFile: "File marked as sensitive" - unmarkSensitiveDriveFile: "File unmarked as sensitive" - resolveAbuseReport: "Report resolved" - forwardAbuseReport: "Report forwarded" - updateAbuseReportNote: "Moderation note of a report updated" - createInvitation: "Invite generated" - createAd: "Ad created" - deleteAd: "Ad deleted" - updateAd: "Ad updated" - createAvatarDecoration: "Avatar decoration created" - updateAvatarDecoration: "Avatar decoration updated" - deleteAvatarDecoration: "Avatar decoration deleted" - unsetUserAvatar: "User avatar unset" - unsetUserBanner: "User banner unset" - createSystemWebhook: "System Webhook created" - updateSystemWebhook: "System Webhook updated" - deleteSystemWebhook: "System Webhook deleted" - createAbuseReportNotificationRecipient: "Recipient for reports created" - updateAbuseReportNotificationRecipient: "Recipient for reports updated" - deleteAbuseReportNotificationRecipient: "Recipient for reports deleted" - deleteAccount: "Account deleted" - deletePage: "Page deleted" - deleteFlash: "Play deleted" - deleteGalleryPost: "Gallery post deleted" - deleteChatRoom: "Deleted Chat Room" - updateProxyAccountDescription: "Update the description of the proxy account" -_fileViewer: - title: "File details" - type: "File type" - size: "Filesize" - url: "URL" - uploadedAt: "Uploaded at" - attachedNotes: "Attached notes" - thisPageCanBeSeenFromTheAuthor: "This page can only be seen by the user who uploaded this file." -_externalResourceInstaller: - title: "Install from external site" - checkVendorBeforeInstall: "Make sure the distributor of this resource is trustworthy before installation." - _plugin: - title: "Do you want to install this plugin?" - _theme: - title: "Do you want to install this theme?" - _meta: - base: "Base color scheme" - _vendorInfo: - title: "Distributor information" - endpoint: "Referenced endpoint" - hashVerify: "Hash verification" - _errors: - _invalidParams: - title: "Invalid parameters" - description: "There is not enough information to load data from an external site. Please confirm the entered URL." - _resourceTypeNotSupported: - title: "This external resource is not supported" - description: "The type of this external resource is not supported. Please contact the site administrator." - _failedToFetch: - title: "Failed to fetch data" - fetchErrorDescription: "An error occurred communicating with the external site. If trying again does not fix this issue, please contact the site administrator." - parseErrorDescription: "An error occurred processing the data loaded from the external site. Please contact the site administrator." - _hashUnmatched: - title: "Data verification failed" - description: "An error occurred verifying the integrity of the fetched data. As a security measure, installation cannot continue. Please contact the site administrator." - _pluginParseFailed: - title: "AiScript Error" - description: "The requested data was fetched successfully, but an error occurred during AiScript parsing. Please contact the plugin author. Error details can be viewed in the Javascript console." - _pluginInstallFailed: - title: "Plugin installation failed" - description: "A problem occurred during plugin installation. Please try again. Error details can be viewed in the Javascript console." - _themeParseFailed: - title: "Theme parsing failed" - description: "The requested data was fetched successfully, but an error occurred during theme parsing. Please contact the theme author. Error details can be viewed in the Javascript console." - _themeInstallFailed: - title: "Failed to install theme" - description: "A problem occurred during theme installation. Please try again. Error details can be viewed in the Javascript console." -_dataSaver: - _media: - title: "Loading Media" - description: "Prevents images/videos from being loaded automatically. Hidden images/videos will be loaded when tapped." - _avatar: - title: "Avatar image" - description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic." - _urlPreviewThumbnail: - title: "Hide URL preview thumbnails" - description: "URL preview thumbnail images will no longer be loaded." - _disableUrlPreview: - title: "Disable URL preview" - description: "Disables the URL preview function. Unlike thumbnail images, this function reduces the loading of the linked information itself." - _code: - title: "Code highlighting" - description: "If code highlighting notations are used in MFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data." -_hemisphere: - N: "Northern Hemisphere" - S: "Southern Hemisphere" - caption: "Used in some client settings to determine season." -_reversi: - reversi: "Reversi" - gameSettings: "Game settings" - chooseBoard: "Choose a board" - blackOrWhite: "Black/White" - blackIs: "{name} is playing Black" - rules: "Rules" - thisGameIsStartedSoon: "The game will begin shortly" - waitingForOther: "Waiting for opponent's turn" - waitingForMe: "Waiting for your turn" - waitingBoth: "Get ready" - ready: "Ready" - cancelReady: "Not ready" - opponentTurn: "Opponent's turn" - myTurn: "Your turn" - turnOf: "It's {name}'s turn" - pastTurnOf: "{name}'s turn" - surrender: "Surrender" - surrendered: "Surrendered" - timeout: "Out of time" - drawn: "Draw" - won: "{name} wins" - black: "Black" - white: "White" - total: "Total" - turnCount: "Turn {count}" - myGames: "My rounds" - allGames: "All rounds" - ended: "Ended" - playing: "Currently playing" - isLlotheo: "The one with fewer stones wins (Llotheo)" - loopedMap: "Looping map" - canPutEverywhere: "Tiles are placeable everywhere" - timeLimitForEachTurn: "Time limit for turn" - freeMatch: "Free Match" - lookingForPlayer: "Finding opponent..." - gameCanceled: "The game has been cancelled." - shareToTlTheGameWhenStart: "Share Game to timeline when started" - iStartedAGame: "The game has begun! #MisskeyReversi" - opponentHasSettingsChanged: "The opponent has changed their settings." - allowIrregularRules: "Irregular rules (completely free)" - disallowIrregularRules: "No irregular rules" - showBoardLabels: "Display row and column numbering on the board" - useAvatarAsStone: "Turn stones into user avatars" -_offlineScreen: - title: "Offline - cannot connect to the server" - header: "Unable to connect to the server" -_urlPreviewSetting: - title: "URL preview settings" - enable: "Enable URL preview" - allowRedirect: "Allow URL preview redirection" - allowRedirectDescription: "If a URL has a redirection set, you can enable this feature to follow the redirection and display a preview of the redirected content. Disabling this will save server resources, but redirected content will not be displayed." - timeout: "Time out when getting preview (ms)" - timeoutDescription: "If it takes longer than this value to get the preview, the preview won’t be generated." - maximumContentLength: "Maximum Content-Length (bytes)" - maximumContentLengthDescription: "If Content-Length is higher than this value, the preview won't be generated." - requireContentLength: "Generate the preview only if you could get Content-Length" - requireContentLengthDescription: "If other server doesn't return Content-Length, the preview won't be generated." - userAgent: "User-Agent" - userAgentDescription: "Sets the User-Agent to be used when retrieving previews. If left blank, the default User-Agent will be used." - summaryProxy: "Proxy endpoints that generate previews" - summaryProxyDescription: "Not Misskey itself, but generate previews using Summaly Proxy." - summaryProxyDescription2: "The following parameters are linked to the proxy as a query string. If the proxy does not support them, the values are ignored." -_mediaControls: - pip: "Picture in Picture" - playbackRate: "Playback Speed" - loop: "Loop playback" -_contextMenu: - title: "Context menu" - app: "Application" - appWithShift: "Application with shift key" - native: "Native" -_gridComponent: - _error: - requiredValue: "This value is required" - columnTypeNotSupport: "Validation with regular expression is supported only for type:text columns." - patternNotMatch: "This value doesn't match the pattern in {pattern}" - notUnique: "This value must be unique" -_roleSelectDialog: - notSelected: "Not selected" -_customEmojisManager: - _gridCommon: - copySelectionRows: "Copy selected rows" - copySelectionRanges: "Copy selection" - deleteSelectionRows: "Delete selected rows" - deleteSelectionRanges: "Delete rows in the selection" - searchSettings: "Search settings" - searchSettingCaption: "Set detailed search criteria." - searchLimit: "" - sortOrder: "Sort order" - registrationLogs: "Registration log" - registrationLogsCaption: "Logs will be displayed when updating or deleting Emojis. They will disappear after updating or deleting them, moving to a new page, or reloading." - alertEmojisRegisterFailedDescription: "Failed to update or delete Emojis. Please check the registration log for details." - _logs: - showSuccessLogSwitch: "Show success log" - failureLogNothing: "There is no failure log." - logNothing: "There is no log." - _remote: - selectionRowDetail: "Selected row's detail" - importSelectionRows: "Import selected rows" - importSelectionRangesRows: "Import rows in the selection" - importEmojisButton: "Import checked Emojis" - confirmImportEmojisTitle: "Import Emojis" - confirmImportEmojisDescription: "Import {count} Emoji(s) received from the remote server. Please pay close attention to the license of the Emoji. Are you sure to continue?" - _local: - tabTitleList: "Registered emojis" - tabTitleRegister: "Emoji registration" - _list: - emojisNothing: "There are no registered Emojis." - markAsDeleteTargetRows: "Mark selected rows as a target to delete" - markAsDeleteTargetRanges: "Mark rows in the selection as a target to delete" - alertUpdateEmojisNothingDescription: "There are no updated Emojis." - alertDeleteEmojisNothingDescription: "There are no Emojis to be deleted." - confirmMovePage: "" - confirmChangeView: "" - confirmUpdateEmojisDescription: "Update {count} Emoji(s). Are you sure to continue?" - confirmDeleteEmojisDescription: "Delete checked {count} Emoji(s). Are you sure to continue?" - confirmResetDescription: "" - confirmMovePageDesciption: "Changes have been made to the Emojis on this page.\nIf you leave the page without saving, all changes made on this page will be discarded." - dialogSelectRoleTitle: "Search by role set in Emojis" - _register: - uploadSettingTitle: "Upload settings" - uploadSettingDescription: "On this screen, you can configure the behavior when uploading Emojis." - directoryToCategoryLabel: "Enter the directory name in the \"category\" field" - directoryToCategoryCaption: "When you drag and drop a directory, enter the directory name in the \"category\" field." - confirmRegisterEmojisDescription: "Register the Emojis from the list as new custom Emojis. Are you sure to continue? (To avoid overload, only {count} Emoji(s) can be registered in a single operation)" - confirmClearEmojisDescription: "Discard the edits and clear the Emojis from the list. Are you sure to continue?" - confirmUploadEmojisDescription: "Upload the dragged and dropped {count} file(s) to the drive. Are you sure to continue?" -_embedCodeGen: - title: "Customize embed code" - header: "Show header" - autoload: "Automatically load more (deprecated)" - maxHeight: "Max height" - maxHeightDescription: "Setting it to 0 disables the max height setting. Specify some value to prevent the widget from continuing to expand vertically." - maxHeightWarn: "The max height limit is disabled (0). If this was not intended, set the max height to some value." - previewIsNotActual: "The display differs from the actual embedding because it exceeds the range displayed on the preview screen." - rounded: "Make it rounded" - border: "Add a border to the outer frame" - applyToPreview: "Apply to the preview" - generateCode: "Generate embed code" - codeGenerated: "The code has been generated" - codeGeneratedDescription: "Paste the generated code into your website to embed the content." -_selfXssPrevention: - warning: "WARNING" - title: "\"Paste something on this screen\" is all a scam." - description1: "If you paste something here, a malicious user could hijack your account or steal your personal information." - description2: "If you do not understand exactly what you are trying to paste, %cstop working right now and close this window." - description3: "For more information, please refer to this. {link}" -_followRequest: - recieved: "Received request" - sent: "Sent request" -_remoteLookupErrors: - _federationNotAllowed: - title: "Unable to communicate with this server" - description: "Communication with this server may have been disabled or this server may be blocked.\nPlease contact the server administrator." - _uriInvalid: - title: "URI is invalid" - description: "There is a problem with the URI you entered. Please check if you entered characters that cannot be used in the URI." - _requestFailed: - title: "Request failed" - description: "Communication with this server failed. The server may be down. Also, please make sure that you have not entered an invalid or nonexistent URI." - _responseInvalid: - title: "Response is invalid" - description: "It could communicate with this server, but the data obtained was incorrect." - _noSuchObject: - title: "Not found" - description: "The requested resource was not found, please recheck the URI." -_captcha: - verify: "Please verify the CAPTCHA" - testSiteKeyMessage: "You can check the preview by entering the test values for the site and secret keys.\nPlease see the following page for details." - _error: - _requestFailed: - title: "Failed to request CAPTCHA" - text: "Please run it after a while or check the settings again." - _verificationFailed: - title: "Failed to validate CAPTCHA" - text: "Please check again if the settings are correct." - _unknown: - title: "CAPTCHA error" - text: "An unexpected error occurred." -_bootErrors: - title: "Failed to load" - serverError: "If the problem persists after waiting a moment and reloading, please contact the server administrator with the following Error ID." - solution: "The following may solve the problem." - solution1: "Update your browser and OS to the latest version" - solution2: "Disable ad blocker" - solution3: "Clear the browser cache" - solution4: "Set the dom.webaudio.enabled to true for Tor Browser" - otherOption: "Other options" - otherOption1: "Delete client settings and cache" - otherOption2: "Start the simple client" - otherOption3: "Launch the repair tool" -_search: - searchScopeAll: "All" - searchScopeLocal: "Local" - searchScopeServer: "Specific server" - searchScopeUser: "Specific user" - pleaseEnterServerHost: "Enter the server host" - pleaseSelectUser: "Select user" - serverHostPlaceholder: "Example: misskey.example.com" -_serverSetupWizard: - installCompleted: "Misskey installation is now complete!" - firstCreateAccount: "To begin, create an administrator account." - accountCreated: "Administrator account has been created!" - serverSetting: "Server Settings" - youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "This wizard makes it easier to configure the server settings." - settingsYouMakeHereCanBeChangedLater: "The settings that were changed via this wizard can be adjusted later." - howWillYouUseMisskey: "How will you use Misskey?" - _use: - single: "Single User server" - single_description: "Use it alone as your own server." - single_youCanCreateMultipleAccounts: "Multiple accounts can be created as needed, even when operated as a single user server." - group: "Group server" - group_description: "Invite other trusted users to use it with more than one user." - open: "Public server" - open_description: "Allow anyone to register." - openServerAdvice: "Accepting a large number of unknown users involves risk. We recommend that you operate with a reliable moderation system to handle any problems." - openServerAntiSpamAdvice: "To prevent your server from becoming a stepping stone for spam, you should also pay close attention to security by enabling anti-bot functions such as reCAPTCHA." - howManyUsersDoYouExpect: "How many users do you expect?" - _scale: - small: "Less than 100 (small scale)" - medium: "More than 100 and less than 1000 users (medium size)" - large: "More than 1000 (Large scale)" - largeScaleServerAdvice: "Large servers may require advanced infrastructure knowledge, such as load balancing and database replication." - doYouConnectToFediverse: "Do you want to connect to the Fediverse?" - doYouConnectToFediverse_description1: "When connected to a network of distributed servers (Fediverse) content can be exchanged with other servers." - doYouConnectToFediverse_description2: "Connecting with the Fediverse is also called \"federation\"" - youCanConfigureMoreFederationSettingsLater: "Advanced settings such as specifying federated servers can be configured later." - adminInfo: "Administrator information" - adminInfo_description: "Sets the administrator information used to receive inquiries." - adminInfo_mustBeFilled: "Must be entered if public server or federation is on." - followingSettingsAreRecommended: "The following settings are recommended" - applyTheseSettings: "Apply these settings" - skipSettings: "Skip settings" - settingsCompleted: "Setup is now complete!" - settingsCompleted_description: "Thank you for your time. Now that everything is ready, you can start using the server right away." - settingsCompleted_description2: "The server settings can be changed from the “Control Panel”" - donationRequest: "Donation Request" - _donationRequest: - text1: "Misskey is a free software developed by volunteers." - text2: "We would appreciate your support so that we can continue to develop this software further into the future." - text3: "There are also special benefits for supporters!" -_uploader: - compressedToX: "Compressed to {x}" - savedXPercent: "Saving {x}%" - abortConfirm: "Some files have not been uploaded, do you want to abort?" - doneConfirm: "Some files have not been uploaded, do you want to continue anyway?" - maxFileSizeIsX: "The maximum file size that can be uploaded is {x}" - allowedTypes: "Uploadable file types" - tip: "The file has not yet been uploaded so this dialog allows you to confirm, rename, compress, and crop the file before uploading. When ready, you can start uploading by pressing the “Upload” button." -_clientPerformanceIssueTip: - title: "Performance tips" - makeSureDisabledAdBlocker: "Disable your adblocker" - makeSureDisabledAdBlocker_description: "Adblockers can affect performance, please make sure that adblockers are not enabled by your system or browser features/extensions." - makeSureDisabledCustomCss: "Disable custom CSS" - makeSureDisabledCustomCss_description: "Overriding styles can affect performance. Please make sure that custom CSS or extensions that override styles are not enabled." - makeSureDisabledAddons: "Disable extensions" - makeSureDisabledAddons_description: "Some extensions may interfere with client behavior and affect performance. Please disable your browser extensions and see if this improves the situation." -_clip: - tip: "Clip is a feature that allows you to organize your notes." -_userLists: - tip: "Lists can contain any user you specify when creating, the created list can then be displayed as a timeline showing only the specified users." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 480ade4153..6f64339820 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -5,14 +5,10 @@ introMisskey: "¡Bienvenido/a! Misskey es un servicio de microblogging descentra poweredByMisskeyDescription: "{name} es uno de los servicios (también llamado instancia) que usa la plataforma de código abierto Misskey" monthAndDay: "{day}/{month}" search: "Buscar" -reset: "Reiniciar" notifications: "Notificaciones" username: "Nombre de usuario" password: "Contraseña" -initialPasswordForSetup: "Contraseña para iniciar la inicialización" -initialPasswordIsIncorrect: "La contraseña para iniciar la configuración inicial es incorrecta." -initialPasswordForSetupDescription: "Si ha instalado Misskey usted mismo, utilice la contraseña introducida en el archivo de configuración.\nSi utiliza un servicio de alojamiento de Misskey o similar, utilice la contraseña proporcionada.\nSi no ha establecido una contraseña, déjela en blanco para continuar." -forgotPassword: "Olvidé mi contraseña" +forgotPassword: "Olvidé mi Contraseña" fetchingAsApObject: "Buscando en el fediverso" ok: "OK" gotIt: "¡Lo tengo!" @@ -24,8 +20,8 @@ noNotes: "No hay notas" noNotifications: "No hay notificaciones" instance: "Instancia" settings: "Configuración" -notificationSettings: "Ajustes de notificaciones" -basicSettings: "Configuración básica" +notificationSettings: "Configurar las notificaciones" +basicSettings: "Configuración Básica" otherSettings: "Configuración avanzada" openInWindow: "Abrir en una ventana" profile: "Perfil" @@ -49,23 +45,16 @@ pin: "Fijar al perfil" unpin: "Desfijar" copyContent: "Copiar contenido" copyLink: "Copiar enlace" -copyRemoteLink: "Copiar enlace remoto" -copyLinkRenote: "Copiar enlace de renota" delete: "Borrar" deleteAndEdit: "Borrar y editar" deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta nota y editarla? Perderás todas las reacciones, renotas y respuestas." addToList: "Agregar a lista" -addToAntenna: "Añadir a la antena" sendMessage: "Enviar un mensaje" copyRSS: "Copiar RSS" copyUsername: "Copiar nombre de usuario" copyUserId: "Copiar ID del usuario" copyNoteId: "Copiar ID de la nota" -copyFileId: "Copiar ID del archivo" -copyFolderId: "Copiar ID de carpeta" -copyProfileUrl: "Copiar la URL del perfil" searchUser: "Buscar un usuario" -searchThisUsersNotes: "" reply: "Responder" loadMore: "Ver más" showMore: "Ver más" @@ -114,14 +103,11 @@ enterEmoji: "Ingresar emojis" renote: "Renotar" unrenote: "Quitar renota" renoted: "Renotado" -renotedToX: "{name} usuarios han renotado。" cantRenote: "No se puede renotar este post" cantReRenote: "No se puede renotar una renota" quote: "Citar" inChannelRenote: "Renota sólo del canal" inChannelQuote: "Cita sólo del canal" -renoteToChannel: "Renotar a otro canal" -renoteToOtherChannel: "Renotar a otro canal" pinnedNote: "Nota fijada" pinned: "Fijar al perfil" you: "Tú" @@ -130,16 +116,10 @@ sensitive: "Marcado como sensible" add: "Agregar" reaction: "Reacción" reactions: "Reacción" -emojiPicker: "Selector de emojis" -pinnedEmojisForReactionSettingDescription: "Puedes seleccionar reacciones para fijarlos en el selector" -pinnedEmojisSettingDescription: "Puedes seleccionar emojis para fijarlos en el selector" -emojiPickerDisplay: "Mostrar el selector de emojis" -overwriteFromPinnedEmojisForReaction: "Sobreescribir las reacciones fijadas" -overwriteFromPinnedEmojis: "Sobreescribir los emojis fijados" +reactionSetting: "Reacciones para mostrar en el menú de reacciones" reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir." rememberNoteVisibility: "Recordar visibilidad" attachCancel: "Quitar adjunto" -deleteFile: "Archivo eliminado" markAsSensitive: "Marcar como sensible" unmarkAsSensitive: "Desmarcar como sensible" enterFileName: "Ingrese el nombre del archivo" @@ -160,7 +140,6 @@ editList: "Editar lista" selectChannel: "Seleccionar canal" selectAntenna: "Seleccionar antena" editAntenna: "Editar antena" -createAntenna: "Crear una antena" selectWidget: "Seleccionar widget" editWidgets: "Editar widgets" editWidgetsExit: "Terminar edición" @@ -173,9 +152,6 @@ addEmoji: "Agregar emoji" settingGuide: "Configuración sugerida" cacheRemoteFiles: "Mantener en cache los archivos remotos" cacheRemoteFilesDescription: "Si desactiva esta configuración, Los archivos remotos se cargarán desde el link directo sin usar la caché. Con eso se puede ahorrar almacenamiento del servidor, pero eso aumentará el tráfico al no crear miniaturas." -youCanCleanRemoteFilesCache: "Puedes vaciar la caché pulsando en el botón 🗑️ en el administrador de archivos." -cacheRemoteSensitiveFiles: "Cachear archivos remotos sensibles" -cacheRemoteSensitiveFilesDescription: "Cuando esta opción está desactivada, los archivos remotos sensibles son cargador directamente de la instancia origen sin ser cacheados." flagAsBot: "Esta cuenta es un bot" flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar cadenas infinitas de reacciones, y ajustará los sistemas internos de Misskey para que trate a esta cuenta como un bot." flagAsCat: "Esta cuenta es un gato" @@ -187,10 +163,6 @@ addAccount: "Agregar Cuenta" reloadAccountsList: "Recargar lista de cuentas" loginFailed: "Error al iniciar sesión." showOnRemote: "Ver en una instancia remota" -continueOnRemote: "Ver en una instancia remota" -chooseServerOnMisskeyHub: "Elegir un servidor en Misskey Hub" -specifyServerHost: "Especifica una instancia directamente" -inputHostName: "Introduzca el dominio" general: "General" wallpaper: "Fondo de pantalla" setWallpaper: "Establecer fondo de pantalla" @@ -201,7 +173,6 @@ followConfirm: "¿Desea seguir a {name}?" proxyAccount: "Cuenta proxy" proxyAccountDescription: "Una cuenta proxy es una cuenta que actúa como un seguidor remoto de un usuario bajo ciertas condiciones. Por ejemplo, cuando un usuario añade un usuario remoto a una lista, si ningún usuario local sigue al usuario agregado a la lista, la instancia no puede obtener su actividad. Así que la cuenta proxy sigue al usuario añadido a la lista" host: "Host" -selectSelf: "Elígete a ti mismo" selectUser: "Elegir usuario" recipient: "Recipiente" annotation: "Anotación" @@ -216,11 +187,8 @@ perHour: "por hora" perDay: "por día" stopActivityDelivery: "Dejar de enviar actividades" blockThisInstance: "Bloquear instancia" -silenceThisInstance: "Silenciar esta instancia" -mediaSilenceThisInstance: "Silencia la Multimedia(Imágenes,videos...) para este servidor" operations: "Operaciones" software: "Software" -softwareName: "Nombre del software" version: "Versión" metadata: "Metadatos" withNFiles: "{n} archivos" @@ -238,12 +206,6 @@ clearCachedFiles: "Limpiar caché" clearCachedFilesConfirm: "¿Desea borrar todos los archivos remotos cacheados?" blockedInstances: "Instancias bloqueadas" blockedInstancesDescription: "Seleccione los hosts de las instancias que desea bloquear, separadas por una linea nueva. Las instancias bloqueadas no podrán comunicarse con esta instancia." -silencedInstances: "Instancias silenciadas" -silencedInstancesDescription: "Listar los hostname de las instancias que quieres silenciar. Todas las cuentas de las instancias listadas serán tratadas como silenciadas, solo podrán hacer peticiones de seguimiento, y no podrán mencionar cuentas locales si no las siguen. Esto no afecta a las instancias bloqueadas." -mediaSilencedInstances: "Servidores silenciados (Multimedia)" -mediaSilencedInstancesDescription: "Listar las instancias que quieres silenciar. Todas las cuentas de las instancias listadas serán tratadas como silenciadas, solo podrán hacer peticiones de seguimiento, y no podrán mencionar cuentas locales si no las siguen. Esto no afecta a las instancias bloqueadas." -federationAllowedHosts: "Servidores federados" -federationAllowedHostsDescription: "Establezca los nombres de los servidores que pueden federarse, separados por una nueva línea." muteAndBlock: "Silenciar y bloquear" mutedUsers: "Usuarios silenciados" blockedUsers: "Usuarios bloqueados" @@ -251,11 +213,12 @@ noUsers: "No hay usuarios" editProfile: "Editar perfil" noteDeleteConfirm: "¿Desea borrar esta nota?" pinLimitExceeded: "Ya no se pueden fijar más posts" +intro: "¡La instalación de Misskey ha terminado! Crea el usuario administrador." done: "Terminado" processing: "Procesando" preview: "Vista previa" default: "Predeterminado" -defaultValueIs: "Por defecto: {value}" +defaultValueIs: "Predeterminado" noCustomEmojis: "No hay emojis personalizados" noJobs: "No hay trabajos" federating: "Federando" @@ -287,8 +250,8 @@ removed: "Borrado" removeAreYouSure: "¿Desea borrar \"{x}\"?" deleteAreYouSure: "¿Desea borrar \"{x}\"?" resetAreYouSure: "¿Desea reestablecer?" -areYouSure: "¿Estás conforme?" saved: "Guardado" +messaging: "Chat" upload: "Subir" keepOriginalUploading: "Mantener la imagen original" keepOriginalUploadingDescription: "Mantener la versión original al cargar imágenes. Si está desactivado, el navegador generará imágenes para la publicación web en el momento de recargar la página" @@ -298,11 +261,10 @@ uploadFromUrl: "Subir desde una URL" uploadFromUrlDescription: "URL del fichero que quieres subir" uploadFromUrlRequested: "Subida solicitada" uploadFromUrlMayTakeTime: "Subir el fichero puede tardar un tiempo." -uploadNFiles: "Subir {n} archivos" explore: "Explorar" messageRead: "Ya leído" noMoreHistory: "El historial se ha acabado" -startChat: "Nuevo Chat" +startMessaging: "Iniciar chat" nUsersRead: "Leído por {n} personas" agreeTo: "De acuerdo con {0}" agree: "De acuerdo." @@ -322,27 +284,23 @@ location: "Lugar" theme: "Tema" themeForLightMode: "Tema para usar en Modo Linterna" themeForDarkMode: "Tema para usar en Modo Oscuro" -light: "Claro" +light: "Linterna" dark: "Oscuro" lightThemes: "Tema claro" darkThemes: "Tema oscuro" syncDeviceDarkMode: "Sincronice el Modo Oscuro con la configuración de su dispositivo" -switchDarkModeManuallyWhenSyncEnabledConfirm: "{x} está activado ¿Te gustaría desactivar la sincronización y cambiar al modo manual?" drive: "Drive" fileName: "Nombre de archivo" selectFile: "Elegir archivo" selectFiles: "Elegir archivos" selectFolder: "Seleccione una carpeta" selectFolders: "Seleccione carpetas" -fileNotSelected: "Archivo no seleccionado." renameFile: "Renombrar archivo" folderName: "Nombre de la carpeta" createFolder: "Crear carpeta" renameFolder: "Renombrar carpeta" deleteFolder: "Borrar carpeta" -folder: "Carpeta" addFile: "Agregar archivo" -showFile: "Examinar archivos" emptyDrive: "El drive está vacío" emptyFolder: "La carpeta está vacía" unableToDelete: "No se puede borrar" @@ -355,7 +313,6 @@ copyUrl: "Copiar URL" rename: "Renombrar" avatar: "Avatar" banner: "Banner" -displayOfSensitiveMedia: "Mostrar contenido sensible" whenServerDisconnected: "Cuando se pierda la conexión con el servidor" disconnectedFromServer: "Desconectado del servidor" reload: "Recargar" @@ -385,10 +342,12 @@ enableLocalTimeline: "Habilitar linea de tiempo local" enableGlobalTimeline: "Habilitar linea de tiempo global" disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia el administrador y los moderadores pueden seguir usándolos" registration: "Registro" +enableRegistration: "Permitir nuevos registros" invite: "Invitar" driveCapacityPerLocalAccount: "Capacidad del drive por usuario local" driveCapacityPerRemoteAccount: "Capacidad del drive por usuario remoto" inMb: "En megabytes" +iconUrl: "URL de la imagen del avatar" bannerUrl: "URL de la imagen del banner" backgroundImageUrl: "URL de la imagen de fondo" basicInfo: "Información básica" @@ -402,11 +361,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Habilitar hCaptcha" hcaptchaSiteKey: "Clave del sitio" hcaptchaSecretKey: "Clave secreta" -mcaptcha: "mCaptcha" -enableMcaptcha: "Activar mCaptcha" -mcaptchaSiteKey: "Clave del sitio" -mcaptchaSecretKey: "Clave secreta" -mcaptchaInstanceUrl: "URL del servidor mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "activar reCAPTCHA" recaptchaSiteKey: "Clave del sitio" @@ -422,11 +376,9 @@ name: "Nombre" antennaSource: "Origen de la antena" antennaKeywords: "Palabras clave para recibir" antennaExcludeKeywords: "Palabras clave para excluir" -antennaExcludeBots: "Excluir bots" antennaKeywordsDescription: "Separar con espacios es una declaración AND, separar con una linea nueva es una declaración OR" notifyAntenna: "Notificar nueva nota" withFileAntenna: "Sólo notas con archivos adjuntados" -excludeNotesInSensitiveChannel: "Excluir notas en canales sensibles" enableServiceworker: "Activar ServiceWorker" antennaUsersDescription: "Elegir nombres de usuarios separados por una linea nueva" caseSensitive: "Distinguir mayúsculas de minúsculas" @@ -451,15 +403,10 @@ aboutMisskey: "Sobre Misskey" administrator: "Administrador" token: "Token" 2fa: "Autenticación de doble factor" -setupOf2fa: "Configurar la autenticación de dos factores" totp: "Aplicación autentícadora" totpDescription: "Ingresa una contaseña de un sólo uso usando la aplicación autenticadora" moderator: "Moderador" moderation: "Moderación" -moderationNote: "Nota de moderación" -moderationNoteDescription: "Puedes rellenar notas que solo se comparten entre moderadores." -addModerationNote: "Añadir nota de moderación" -moderationLogs: "Log de moderación" nUsersMentioned: "{n} usuarios mencionados" securityKeyAndPasskey: "Clave de seguridad / clave de paso" securityKey: "Clave de seguridad" @@ -475,6 +422,7 @@ share: "Compartir" notFound: "No se encuentra" notFoundDescription: "No se encontró la página correspondiente a la URL elegida" uploadFolder: "Carpeta de subidas por defecto" +cacheClear: "Borrar caché" markAsReadAllNotifications: "Marcar todas las notificaciones como leídas" markAsReadAllUnreadNotes: "Marcar todas las notas como leídas" markAsReadAllTalkMessages: "Marcar todos los chats como leídos" @@ -492,10 +440,10 @@ retype: "Ingrese de nuevo" noteOf: "Notas de {user}" quoteAttached: "Cita añadida" quoteQuestion: "¿Quiere añadir una cita?" -attachAsFileQuestion: "El texto del portapapeles es demasiado grande ¿Desea adjuntarlo como archivo de texto?" +noMessagesYet: "Aún no hay chat" +newMessageExists: "Tienes un mensaje nuevo" onlyOneFileCanBeAttached: "Solo se puede añadir un archivo al mensaje" signinRequired: "Iniciar sesión" -signinOrContinueOnRemote: "Para continuar, tendrá que ir a su servidor o registrarse e iniciar sesión en este servidor" invitations: "Invitar" invitationCode: "Código de invitación" checking: "Comprobando" @@ -517,12 +465,8 @@ uiLanguage: "Idioma de visualización de la interfaz" aboutX: "Acerca de {x}" emojiStyle: "Estilo de emoji" native: "Nativo" -menuStyle: "Diseño del menú" -style: "Diseño" -drawer: "Cajón de Aplicaciones" -popup: "Ventana emergente" +disableDrawer: "No mostrar los menús en cajones" showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor" -showReactionsCount: "Mostrar el número de reacciones en las notas" noHistory: "No hay datos en el historial" signinHistory: "Historial de ingresos" enableAdvancedMfm: "Habilitar MFM avanzado" @@ -575,22 +519,16 @@ serverLogs: "Registros del servidor" deleteAll: "Eliminar todos" showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo" showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronología (Canales)" -withRepliesByDefaultForNewlyFollowed: "Incluir por defecto respuestas de usuarios recién seguidos en la línea de tiempo" newNoteRecived: "Tienes una nota nueva" -newNote: "Nueva nota" sounds: "Sonidos" sound: "Sonidos" -notificationSoundSettings: "Configuración del sonido de las notificaciones" listen: "Escuchar" none: "Ninguna" showInPage: "Mostrar en la página" popout: "Popout" volume: "Volumen" masterVolume: "Volumen principal" -notUseSound: "Sin sonido" -useSoundOnlyWhenActive: "Sonar solo cuando Misskey esté activo" details: "Detalles" -renoteDetails: "Detalles(Renota)" chooseEmoji: "Elije un emoji" unableToProcess: "La operación no se puede llevar a cabo" recentUsed: "Usado recientemente" @@ -606,16 +544,10 @@ ascendingOrder: "Ascendente" descendingOrder: "Descendente" scratchpad: "Scratch pad" scratchpadDescription: "Scratchpad proporciona un entorno experimental para AiScript. Puede escribir, ejecutar y verificar los resultados que interactúan con Misskey." -uiInspector: "Inspector de UI" -uiInspectorDescription: "Puedes visualizar una lista de elementos UI presentes en la memoria. Los componentes de la interfaz de usuario son generados por las funciones UI:C:" output: "Salida" script: "Script" disablePagesScript: "Deshabilitar AiScript en Páginas" updateRemoteUser: "Actualizar información de usuario remoto" -unsetUserAvatar: "Quitar avatar" -unsetUserAvatarConfirm: "¿Confirmas que quieres quitar tu avatar?" -unsetUserBanner: "Quitar banner" -unsetUserBannerConfirm: "¿Confirmas que quieres quitar tu banner?" deleteAllFiles: "Borrar todos los archivos" deleteAllFilesConfirm: "¿Desea borrar todos los archivos?" removeAllFollowing: "Retener todos los siguientes" @@ -666,7 +598,6 @@ medium: "Mediano" small: "Pequeño" generateAccessToken: "Generar token de acceso" permission: "Permisos" -adminPermission: "Permiso de administrador" enableAll: "Activar todo" disableAll: "Desactivar todo" tokenRequested: "Permiso de acceso a la cuenta" @@ -688,19 +619,13 @@ smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP" smtpSecureInfo: "Apagar cuando se use STARTTLS" testEmail: "Prueba de envío" wordMute: "Silenciar palabras" -wordMuteDescription: "Minimiza las notas que contienen la palabra o frase especificada. Las notas minimizadas pueden visualizarse haciendo clic sobre ellas." -hardWordMute: "Filtro de palabra fuerte" -showMutedWord: "Mostrar palabras silenciadas." -hardWordMuteDescription: "Oculta las notas que contienen la palabra o frase especificada. A diferencia de Silenciar palabra, la nota quedará completamente oculta a la vista." regexpError: "Error de la expresión regular" regexpErrorDescription: "Ocurrió un error en la expresión regular en la linea {line} de las palabras muteadas {tab}" instanceMute: "Instancias silenciadas" userSaysSomething: "{name} dijo algo" -userSaysSomethingAbout: "{name} dijo algo sobre {word}" makeActive: "Activar" display: "Apariencia" copy: "Copiar" -copiedToClipboard: "Texto copiado al portapapeles" metrics: "Métricas" overview: "Resumen" logs: "Registros" @@ -715,21 +640,22 @@ useGlobalSettingDesc: "Al activarse, se usará la configuración de notificacion other: "Otro" regenerateLoginToken: "Regenerar token de login" regenerateLoginTokenDescription: "Regenerar el token usado internamente durante el login. No siempre es necesario hacerlo. Al hacerlo de nuevo, se deslogueará en todos los dispositivos." -theKeywordWhenSearchingForCustomEmoji: "Palabra clave para buscar el emoji personalizado." setMultipleBySeparatingWithSpace: "Puedes añadir mas de uno, separado por espacios." fileIdOrUrl: "Id del archivo o URL" behavior: "Comportamiento" sample: "Muestra" abuseReports: "Reportes" reportAbuse: "Reportar" -reportAbuseRenote: "Reportar renota" reportAbuseOf: "Reportar a {name}" fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una nota en particular, ingrese la URL de esta." abuseReported: "Se ha enviado el reporte. Muchas gracias." reporter: "Reportador" reporteeOrigin: "Reportar a" reporterOrigin: "Origen del reporte" +forwardReport: "Transferir un informe a una instancia remota" +forwardReportIsAnonymous: "No puede ver su información de la instancia remota y aparecerá como una cuenta anónima del sistema" send: "Enviar" +abuseMarkAsResolved: "Marcar reporte como resuelto" openInNewTab: "Abrir en una Nueva Pestaña" openInSideView: "Abrir en una vista al costado" defaultNavigationBehaviour: "Navegación por defecto" @@ -747,7 +673,6 @@ createNewClip: "Crear clip nuevo" unclip: "Quitar clip" confirmToUnclipAlreadyClippedNote: "Esta nota ya está incluida en el clip \"{name}\". ¿Quiere quitar la nota del clip?" public: "Público" -private: "Privado" i18nInfo: "Misskey está siendo traducido a varios idiomas gracias a voluntarios. Se puede colaborar traduciendo en {link}" manageAccessTokens: "Administrar tokens de acceso" accountInfo: "Información de la Cuenta" @@ -772,7 +697,6 @@ lockedAccountInfo: "A menos que configures la visibilidad de tus notas como \"S alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto" loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas" disableShowingAnimatedImages: "No reproducir imágenes animadas" -highlightSensitiveMedia: "Resaltar medios marcados como sensibles" verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por favor, acceda al enlace proporcionado en el correo electrónico para completar la configuración." notSet: "Sin especificar" emailVerified: "Su dirección de correo electrónico ha sido verificada." @@ -788,6 +712,7 @@ thisIsExperimentalFeature: "Se trata de una función experimental. Las especific developer: "Desarrolladores" makeExplorable: "Hacer visible la cuenta en \"Explorar\"" makeExplorableDescription: "Si desactiva esta opción, su cuenta no aparecerá en la sección \"Explorar\"." +showGapBetweenNotesInTimeline: "Mostrar un intervalo entre notas en la línea de tiempo" duplicate: "Duplicar" left: "Izquierda" center: "Centrar" @@ -795,7 +720,6 @@ wide: "Ancho" narrow: "Estrecho" reloadToApplySetting: "Esta configuración sólo se aplicará después de recargar la página. ¿Recargar ahora?" needReloadToApply: "Se requiere un reinicio para la aplicar los cambios" -needToRestartServerToApply: "Se requiere un reinicio para la aplicar los cambios" showTitlebar: "Mostrar la barra de título" clearCache: "Limpiar caché" onlineUsersCount: "{n} usuarios en línea" @@ -866,7 +790,6 @@ administration: "Administrar" accounts: "Cuentas" switch: "Cambiar" noMaintainerInformationWarning: "No se ha establecido la información del administrador" -noInquiryUrlWarning: "No se ha guardado la URL de consulta." noBotProtectionWarning: "La protección contra los bots no está configurada" configure: "Configurar" postToGallery: "Crear una nueva publicación en la galería" @@ -924,14 +847,13 @@ manageAccounts: "Administrar cuenta" makeReactionsPublic: "Hacer el historial de reacciones público" makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán públicamente visibles." classic: "Clásico" -muteThread: "Silenciar hilo" +muteThread: "Ocultar hilo" unmuteThread: "Mostrar hilo" -followingVisibility: "Visibilidad de seguidos" -followersVisibility: "Visibilidad de seguidores" +ffVisibility: "Visibilidad de seguidores y seguidos" +ffVisibilityDescription: "Puedes configurar quien puede ver a quienes sigues y quienes te siguen" continueThread: "Ver la continuación del hilo" deleteAccountConfirm: "La cuenta será borrada. ¿Está seguro?" incorrectPassword: "La contraseña es incorrecta" -incorrectTotp: "La contraseña de un solo uso es incorrecta o ha caducado." voteConfirm: "¿Confirma su voto a {choice}?" hide: "Ocultar" useDrawerReactionPickerForMobile: "Mostrar panel de reacciones en móviles" @@ -956,9 +878,6 @@ oneHour: "1 hora" oneDay: "1 día" oneWeek: "1 semana" oneMonth: "1 mes" -threeMonths: "Tres meses" -oneYear: "Un año" -threeDays: "Tres días" reflectMayTakeTime: "Puede pasar un tiempo hasta que se reflejen los cambios" failedToFetchAccountInformation: "No se pudo obtener información de la cuenta" rateLimitExceeded: "Se excedió el límite de peticiones" @@ -983,7 +902,6 @@ document: "Documento" numberOfPageCache: "Cantidad de páginas cacheadas" numberOfPageCacheDescription: "Al aumentar el número mejora la conveniencia pero tambien puede aumentar la carga y la memoria a usarse" logoutConfirm: "¿Cerrar sesión?" -logoutWillClearClientData: "Al cerrar la sesión, la información de configuración del cliente se borra del navegador. Para garantizar que la información de configuración se pueda restaurar al volver a iniciar sesión, active la copia de seguridad automática de la configuración." lastActiveDate: "Utilizado por última vez el" statusbar: "Barra de estado" pleaseSelect: "Selecciona una opción" @@ -1002,7 +920,6 @@ failedToUpload: "La subida falló" cannotUploadBecauseInappropriate: "Este archivo no se puede subir debido a que algunas partes han sido detectadas comoNSFW." cannotUploadBecauseNoFreeSpace: "La subida falló debido a falta de espacio libre en la unidad del usuario." cannotUploadBecauseExceedsFileSizeLimit: "Este archivo supera el peso máximo y no puede ser subido." -cannotUploadBecauseUnallowedFileType: "Incapaz de subir el archivo debido a que es un tipo de archivo no autorizado." beta: "Beta" enableAutoSensitive: "Marcar automáticamente contenido NSFW" enableAutoSensitiveDescription: "Permite la detección y marcado automático de contenido NSFW usando 'Machine Learning' cuando sea posible. Incluso si esta opción está desactivada, puede ser activado para toda la instancia." @@ -1034,7 +951,6 @@ neverShow: "No mostrar de nuevo" remindMeLater: "Recordar después" didYouLikeMisskey: "¿Te gusta Misskey?" pleaseDonate: "{host} usa el software gratuito Misskey. Por favor ¡Considera donar al proyecto principal para que podamos continuar!" -correspondingSourceIsAvailable: "El código fuente correspondiente se encuentra disponible en {anchor}" roles: "Roles" role: "Rol" noRole: "Rol no encontrado" @@ -1044,7 +960,6 @@ assign: "Asignar" unassign: "Quitar" color: "Color" manageCustomEmojis: "Administrar emojis personalizados" -manageAvatarDecorations: "Administrar decoraciones de avatar" youCannotCreateAnymore: "Has llegado al límite de creaciones." cannotPerformTemporary: "Temporalmente no disponible" cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se excedió el límite de ejecución. Espera un poco y prueba de nuevo." @@ -1062,7 +977,6 @@ thisPostMayBeAnnoyingHome: "Publicar en línea de tiempo 'Inicio'" thisPostMayBeAnnoyingCancel: "detener" thisPostMayBeAnnoyingIgnore: "Publicar de todos modos" collapseRenotes: "Colapsar renotas que ya hayas visto" -collapseRenotesDescription: "Contrae notas a las que ya has reaccionado o renotado " internalServerError: "Error interno del servidor" internalServerErrorDescription: "El servidor tuvo un error inesperado." copyErrorInfo: "Copiar detalles del error" @@ -1080,17 +994,10 @@ reactionAcceptance: "Aceptación de reacciones" likeOnly: "Sólo 'me gusta'" likeOnlyForRemote: "Sólo reacciones de instancias remotas" nonSensitiveOnly: "Solo no sensible" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Sólo no contenido sensible (sólo me gusta en remoto)" rolesAssignedToMe: "Roles asignados a mí" resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?" sensitiveWords: "Palabras sensibles" sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea" -sensitiveWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares." -prohibitedWords: "Palabras explícitas" -prohibitedWordsDescription: "Activa un error cuando se intenta publicar una nota que contiene una o varias palabras prohibidas. Se pueden establecer varias palabras, una por línea." -prohibitedWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares." -hiddenTags: "Hashtags ocultos" -hiddenTagsDescription: "Selecciona las etiquetas que no se mostrarán en tendencias. Una etiqueta por línea." notesSearchNotAvailable: "No se puede buscar una nota" license: "Licencia" unfavoriteConfirm: "¿Desea quitar de favoritos?" @@ -1101,432 +1008,34 @@ retryAllQueuesConfirmTitle: "Desea ¿reintentar inmediatamente todas las colas?" retryAllQueuesConfirmText: "La carga del servidor está incrementándose temporalmente " enableChartsForRemoteUser: "Generar gráficas de usuarios remotos." enableChartsForFederatedInstances: "Generar gráficos de servidores remotos" -enableStatsForFederatedInstances: "Activar las estadísticas de las instancias remotas federadas" showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas" -reactionsDisplaySize: "Tamaño de las reacciones" -limitWidthOfReaction: "Limitar ancho de las reacciones" +largeNoteReactions: "Agrandar las reacciones de las notas" noteIdOrUrl: "ID o URL de la nota" video: "Video" videos: "Video" -audio: "Sonido" -audioFiles: "Sonido" dataSaver: "Ahorro de datos" accountMigration: "Migración de cuenta" accountMoved: "Este usuario se movió a una nueva cuenta:" accountMovedShort: "Esta cuenta ha sido migrada." -operationForbidden: "Operación prohibida" -forceShowAds: "Siempre mostrar anuncios" addMemo: "Añadir nota" editMemo: "Editar nota" reactionsList: "Lista de reacciones" renotesList: "Renotas" -notificationDisplay: "Notificaciones" -leftTop: "Arriba a la izquierda" -rightTop: "Arriba a la derecha" -leftBottom: "Abajo a la izquierda" -rightBottom: "Abajo a la derecha" stackAxis: "Dirección de apilado" -vertical: "Vertical" horizontal: "Horizontal" position: "Posición" serverRules: "Reglas del servidor" -pleaseConfirmBelowBeforeSignup: "Por favor confirma antes de continuar el registro" -pleaseAgreeAllToContinue: "Tienes que estar de acuerdo con los campos anteriores para contnuar." continue: "Continuar" preservedUsernames: "Nombre de usuario reservado" -preservedUsernamesDescription: "La lista de nombres de usuario para reservar tienen que separarse con saltos de línea.\nEstos estarán indisponibles durante la creación de cuentas, pero pueden ser usados para que los administradores puedan crear esas cuentas manualmente. Las cuentas existentes con esos nombres de usuario no se verán afectadas." -createNoteFromTheFile: "Componer una nota desde éste archivo" archive: "Archivo" -archived: "Archivado" -unarchive: "Desarchivar" channelArchiveConfirmTitle: "¿Seguro de archivar {name}?" -channelArchiveConfirmDescription: "Un canal archivado no aparecerá en la lista de canales ni en los resultados. Las nuevas publicaciones tampoco serán añadidas." -thisChannelArchived: "El canal ha sido archivado." -displayOfNote: "Mostrar notas" -initialAccountSetting: "Configración inicial de su cuenta\nか\nConfigración de inicio" youFollowing: "Siguiendo" -preventAiLearning: "Rechazar el uso en el Aprendizaje de Máquinas. (IA Generativa)" -preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los textos publicados o imágenes en el aprendizaje automático (IA Predictiva / Generativa). Ésto se logra añadiendo una marca respuesta HTML con la cadena \"noai\" al cantenido. Una prevención total no podría lograrse sólo usando ésta marca, ya que puede ser simplemente ignorada." options: "Opción" -specifyUser: "Especificar usuario" -lookupConfirm: "¿Quiere informarse?" -openTagPageConfirm: "¿Quieres abrir la página de etiquetas?" -specifyHost: "Especificar Host" -failedToPreviewUrl: "No se pudo generar la vista previa" update: "Actualizar" -rolesThatCanBeUsedThisEmojiAsReaction: "Roles que pueden usar este emoji como reacción" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Si no se especifican roles, cualquiera podrá usar éste emoji como reacción." -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Éstos roles deben ser públicos." -cancelReactionConfirm: "¿Realmente quieres eliminar la reacción?" -changeReactionConfirm: "¿Realmente quieres cambiar la reacción?" -later: "Ahora no" -goToMisskey: "ir a Misskey" -additionalEmojiDictionary: "Diccionario adicional de Emoji" installed: "Instalado" branding: "Marca" enableServerMachineStats: "Publicar estadísticas de hardware del servidor" enableIdenticonGeneration: "Activar generación de identicon por usuario" -turnOffToImprovePerformance: "Desactivar esto puede aumentar el rendimiento." -createInviteCode: "Generar invitación" -createWithOptions: "Generar con opciones" -createCount: "Conteo de invitaciones" -inviteCodeCreated: "Invitación generada" -inviteLimitExceeded: "Has excedido el límite de invitaciones que puedes generar." -createLimitRemaining: "Límite de invitaciones: quedan {limit}" -inviteLimitResetCycle: "El límite ha sido reiniciado a {limit} por {time}." -expirationDate: "Fecha de caducidad" -noExpirationDate: "Sin caducidad" -inviteCodeUsedAt: "Código de invitación usado el" -registeredUserUsingInviteCode: "Invitación usada por" -waitingForMailAuth: "Verificación de correo pendiente" -inviteCodeCreator: "Invitación creada por" -usedAt: "Usada el" -unused: "Sin usar" -used: "Usada" -expired: "Caducada" -doYouAgree: "¿Está de acuerdo?" -beSureToReadThisAsItIsImportant: "Por favor lea esto que es importante" -iHaveReadXCarefullyAndAgree: "He leído el texto {x} y estoy de acuerdo" -dialog: "Diálogo" -icon: "Avatar" -forYou: "Para ti" -currentAnnouncements: "Anuncios actuales" -pastAnnouncements: "Anuncios anteriores" -youHaveUnreadAnnouncements: "Hay anuncios sin leer" -useSecurityKey: "Por favor, sigue las instrucciones de tu dispositivo o navegador para usar tu clave de seguridad o tu clave de paso." -replies: "Responder" -renotes: "Renotar" -loadReplies: "Ver respuestas" -loadConversation: "Ver conversación" -pinnedList: "Lista fijada" -keepScreenOn: "Mantener pantalla encendida" -verifiedLink: "Propiedad del enlace verificada" -notifyNotes: "Notificar nuevas notas" -unnotifyNotes: "Dejar de notificar nuevas notas" -authentication: "Autenticación" -authenticationRequiredToContinue: "Por favor, autentifícate para continuar" -dateAndTime: "Fecha y hora" -showRenotes: "Mostrar renotas" -edited: "Editado" -notificationRecieveConfig: "Ajustes de Notificaciones" -mutualFollow: "Os seguís mutuamente" -followingOrFollower: "Siguiendo o seguidor" -fileAttachedOnly: "Solo notas con archivos" -showRepliesToOthersInTimeline: "Mostrar respuestas a otros en la línea de tiempo" -hideRepliesToOthersInTimeline: "Ocultar respuestas a otros en la línea de tiempo" -showRepliesToOthersInTimelineAll: "Muestra tus respuestas a otros usuarios que sigues en la línea de tiempo" -hideRepliesToOthersInTimelineAll: "Ocultar tus respuestas a otros usuarios que sigues en la línea de tiempo" -confirmShowRepliesAll: "Esta operación es irreversible. ¿Confirmas que quieres mostrar tus respuestas a otros usuarios que sigues en tu línea de tiempo?" -confirmHideRepliesAll: "Esta operación es irreversible. ¿Confirmas que quieres ocultar tus respuestas a otros usuarios que sigues en tu línea de tiempo?" -externalServices: "Servicios Externos" -sourceCode: "Código fuente" -sourceCodeIsNotYetProvided: "El código fuente aún no está disponible. Contacta con el administrador para solucionarlo." -repositoryUrl: "URL del repositorio" -repositoryUrlDescription: "Si estás usando Misskey tal cual (sin cambios en el código fuente), entra en https://github.com/misskey-dev/misskey" -repositoryUrlOrTarballRequired: "Si no has publicado un repositorio aún, deberás publicar un tarball en su lugar. Mira el archivo .config/example.yml para más información." -feedback: "Comentarios" -feedbackUrl: "URL de comentarios" -impressum: "Impressum" -impressumUrl: "Impressum URL" -impressumDescription: "En algunos países, como Alemania, la inclusión del operador de datos (el Impressum) es requerido legalmente para sitios web comerciales." -privacyPolicy: "Política de Privacidad" -privacyPolicyUrl: "URL de la Política de Privacidad" -tosAndPrivacyPolicy: "Condiciones de Uso y Política de Privacidad" -avatarDecorations: "Decoraciones de avatar" -attach: "Acoplar" -detach: "Quitar" -detachAll: "Quitar todo" -angle: "Ángulo" -flip: "Echar de un capirotazo" -showAvatarDecorations: "Mostrar decoraciones de avatar" -releaseToRefresh: "Soltar para recargar" -refreshing: "Recargando..." -pullDownToRefresh: "Tira hacia abajo para recargar" -useGroupedNotifications: "Mostrar notificaciones agrupadas" -signupPendingError: "Ha habido un problema al verificar tu dirección de correo electrónico. Es posible que el enlace haya caducado." -cwNotationRequired: "Si se ha activado \"ocultar contenido\", es necesario proporcionar una descripción." -doReaction: "Añadir reacción" -code: "Código" -reloadRequiredToApplySettings: "Es necesario recargar para que se aplique la configuración." -remainingN: "Faltan: {n}" -overwriteContentConfirm: "¿Quieres sustituir todo el contenido actual?" -seasonalScreenEffect: "Efectos de pantalla asociados a estaciones" -decorate: "Decorar" -addMfmFunction: "Añadir función MFM" -enableQuickAddMfmFunction: "Activar acceso rápido para añadir funciones MFM" -bubbleGame: "Bubble Game" -sfx: "Efectos de sonido" -soundWillBePlayed: "Se reproducirán efectos sonoros" -showReplay: "Ver reproducción" -replay: "Reproducir" -replaying: "Reproduciendo" -endReplay: "Terminar reproducción" -copyReplayData: "Copiar datos de reproducción" -ranking: "Clasificación" -lastNDays: "Últimos {n} días" -backToTitle: "Regresar al inicio" -hemisphere: "Región" -withSensitive: "Mostrar notas que contengan material sensible" -userSaysSomethingSensitive: "La publicación de {name} contiene material sensible" -enableHorizontalSwipe: "Deslice para cambiar de pestaña" -loading: "Cargando" -surrender: "detener" -gameRetry: "Reintentar" -notUsePleaseLeaveBlank: "Dejar en blanco si no se usa" -useTotp: "Introduce la contraseña de un solo uso" -useBackupCode: "Usar códigos de respaldo" -launchApp: "Ejecutar la app" -useNativeUIForVideoAudioPlayer: "Usar la interfaz del navegador cuando se reproduce audio y vídeo" -keepOriginalFilename: "Mantener el nombre original del archivo" -keepOriginalFilenameDescription: "Si desactivas esta opción, los nombres de los archivos serán remplazados por una cadena de caracteres aleatoria cuando subas los archivos." -noDescription: "No hay descripción" -alwaysConfirmFollow: "Confirmar siempre cuando se sigue a alguien" -inquiry: "Contacto" -tryAgain: "Por favor , inténtalo de nuevo" -confirmWhenRevealingSensitiveMedia: "Confirmación cuando se revele contenido sensible" -sensitiveMediaRevealConfirm: "Esto puede contener contenido sensible. ¿Estás seguro/a de querer mostrarlo?" -createdLists: "Listas creadas" -createdAntennas: "Antenas creadas" -fromX: "De {x}" -genEmbedCode: "Obtener el código para incrustar" -noteOfThisUser: "Notas de este usuario" -clipNoteLimitExceeded: "No se pueden añadir más notas a este clip." -performance: "Rendimiento" -modified: "Modificado" -discard: "Descartar" -thereAreNChanges: "Hay {n} cambio(s)" -signinWithPasskey: "Iniciar sesión con clave de acceso" -unknownWebAuthnKey: "Esto no se ha registrado llave maestra." -passkeyVerificationFailed: "La verificación de la clave de acceso ha fallado." -passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificación de la clave de acceso ha sido satisfactoria pero se ha deshabilitado el inicio de sesión sin contraseña." -messageToFollower: "Mensaje a seguidores" -target: "Para" -testCaptchaWarning: "Esta función está pensada para probar CAPTCHAs.No utilizar en un entorno de producción." -prohibitedWordsForNameOfUser: "Palabras prohibidas para nombres de usuario" -prohibitedWordsForNameOfUserDescription: "Si alguna de las cadenas de esta lista está incluida en el nombre del usuario, el nombre será denegado. Los usuarios con privilegios de moderador no se ven afectados por esta restricción." -yourNameContainsProhibitedWords: "Tu nombre contiene palabras prohibidas" -yourNameContainsProhibitedWordsDescription: "Si deseas usar este nombre, por favor contacta con tu administrador/a de tu servidor" -thisContentsAreMarkedAsSigninRequiredByAuthor: " Establecido por el autor: requiere iniciar sesión para ver" -lockdown: "Bloqueo" -pleaseSelectAccount: "Seleccione una cuenta, por favor." -availableRoles: "Roles disponibles " -acknowledgeNotesAndEnable: "Activar después de comprender las precauciones" -federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador." -federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores" -confirmOnReact: "Confirmar la reacción" -reactAreYouSure: "¿Quieres añadir una reacción «{emoji}»?" -markAsSensitiveConfirm: "¿Desea establecer este medio multimedia(Imagen,vídeo...) como sensible?" -unmarkAsSensitiveConfirm: "¿Desea eliminar la designación de sensible para este adjunto?" -preferences: "Preferencias" -accessibility: "Accesibilidad" -preferencesProfile: "Configuración del perfil" -copyPreferenceId: "Copiar ID de la configuración" -resetToDefaultValue: "Revertir a valor predeterminado" -overrideByAccount: "Anulado por la cuenta" -untitled: "Sin título" -noName: "No hay nombre." -skip: "Saltar" -restore: "Restaurar" -syncBetweenDevices: "Sincronizar entre dispositivos" -preferenceSyncConflictTitle: "Los valores configurados existen en el servidor." -preferenceSyncConflictText: "Los ajustes de sincronización activados guardarán sus valores en el servidor. Sin embargo, hay valores existentes en el servidor. ¿Qué conjunto de valores desea sobrescribir?" -preferenceSyncConflictChoiceMerge: "Fusionar" -preferenceSyncConflictChoiceServer: "Valores de configuración del servidor" -preferenceSyncConflictChoiceDevice: "Valor configurado en el dispositivo" -preferenceSyncConflictChoiceCancel: "Cancelar la activación de la sincronización" -paste: "Pegar" -emojiPalette: "Paleta emoji" -postForm: "Formulario" -textCount: "caracteres" -information: "Información" -chat: "Chat" -migrateOldSettings: "Migrar la configuración anterior" -migrateOldSettings_description: "Esto debería hacerse automáticamente, pero si por alguna razón la migración no ha tenido éxito, puede activar usted mismo el proceso de migración manualmente. Se sobrescribirá la información de configuración actual." -compress: "Comprimir" -right: "Derecha" -bottom: "Abajo" -top: "Arriba" -embed: "Insertar" -settingsMigrating: "La configuración está siendo migrada, por favor espera un momento... (También puedes migrar manualmente más tarde yendo a Ajustes otros migrar configuración antigua" -readonly: "Solo Lectura" -goToDeck: "Volver al Deck" -federationJobs: "Trabajos de Federación" -driveAboutTip: "En Drive, aparecerá una lista de los archivos que has subido en el pasado.
\nPuedes reutilizar estos archivos al adjuntarlos a notas, o puedes subir archivos por adelantado para publicarlos más tarde.
\nTen cuidado al eliminar un archivo, ya que no estará disponible en todos los lugares donde se utilizó (como notas, páginas, avatares, banners, etc.).
\nTambién puedes crear carpetas para organizar tus archivos." -scrollToClose: "Desliza para cerrar" -advice: "Consejos" -realtimeMode: "Modo en tiempo real" -turnItOn: "Activar" -turnItOff: "Desactivar" -emojiMute: "Silenciar emojis" -emojiUnmute: "No Silenciar emojis" -muteX: "Silenciar {x}" -unmuteX: "Dejar de silenciar {x}" -abort: "Abortar" -tip: "Consejos y trucos" -redisplayAllTips: "Volver a mostrar todos \"Trucos y consejos\"" -hideAllTips: "Ocultar todos los \"Trucos y consejos\"" -_chat: - noMessagesYet: "Aún no hay mensajes" - newMessage: "Mensajes nuevos" - individualChat: "Chat individual" - individualChat_description: "Mantén una conversación privada con otra persona." - roomChat: "Sala de Chat" - roomChat_description: "Una sala de chat que puede tener varias personas.\nTambién puedes invitar a personas que no permiten chats privados si aceptan la invitación." - createRoom: "Crear sala" - inviteUserToChat: "Invitar usuarios para empezar a chatear" - yourRooms: "Salas creadas" - joiningRooms: "Salas que te has unido" - invitations: "Invitar" - noInvitations: "No hay invitación." - history: "Historial" - noHistory: "No hay datos en el historial" - noRooms: "Sala no encontrada" - inviteUser: "Invitar usuarios" - sentInvitations: "Invitaciones enviadas" - join: "Unirse" - ignore: "Ignorar" - leave: "Dejar sala" - members: "Miembros" - searchMessages: "Buscar mensajes" - home: "Inicio" - send: "Enviar" - newline: "Nueva línea" - muteThisRoom: "Silenciar esta sala" - deleteRoom: "Borrar sala" - chatNotAvailableForThisAccountOrServer: "El chat no está habilitado en este servidor ni para esta cuenta." - chatIsReadOnlyForThisAccountOrServer: "El chat es de sólo lectura en esta instancia o esta cuenta. No puedes escribir nuevos mensajes ni crear/unirte a salas de chat." - chatNotAvailableInOtherAccount: "La función de chat está desactivada para el otro usuario." - cannotChatWithTheUser: "No se puede iniciar un chat con este usuario" - cannotChatWithTheUser_description: "El chat no está disponible o la otra parte no ha habilitado el chat." - youAreNotAMemberOfThisRoomButInvited: "No eres participante en esta sala, pero has recibido una invitación. Por favor, acepta la invitación para unirte." - doYouAcceptInvitation: "¿Aceptas la invitación?" - chatWithThisUser: "Chatear" - thisUserAllowsChatOnlyFromFollowers: "Este usuario sólo acepta chats de seguidores." - thisUserAllowsChatOnlyFromFollowing: "Este usuario sólo acepta chats de los usuarios a los que sigue." - thisUserAllowsChatOnlyFromMutualFollowing: "Este usuario sólo acepta chats de usuarios que son seguidores mutuos." - thisUserNotAllowedChatAnyone: "Este usuario no acepta chats de nadie." - chatAllowedUsers: "A quién permitir chatear." - chatAllowedUsers_note: "Puedes chatear con cualquier persona a la que hayas enviado un mensaje de chat, independientemente de esta configuración." - _chatAllowedUsers: - everyone: "Todos" - followers: "Sólo sus propios seguidores." - following: "Solo usuarios que sigues" - mutual: "Solo seguidores mutuos" - none: "Nadie" -_emojiPalette: - palettes: "Paleta\n" - enableSyncBetweenDevicesForPalettes: "Activar la sincronización de paletas entre dispositivos" - paletteForMain: "Paleta principal" - paletteForReaction: "Paleta de reacción" -_settings: - driveBanner: "Puedes gestionar y configurar la unidad, comprobar su uso y configurar los ajustes de carga de archivos." - pluginBanner: "Puedes ampliar las funciones del cliente con plugins. Puedes instalar plugins, configurarlos y gestionarlos individualmente." - notificationsBanner: "Puede configurar los tipos y el alcance de las notificaciones del servidor y las notificaciones push." - api: "API" - webhook: "Webhook" - serviceConnection: "Integraciones" - serviceConnectionBanner: "Gestione y configure tokens de acceso y Webhooks para integrarse con aplicaciones o servicios externos." - accountData: "Datos de la cuenta" - accountDataBanner: "Exportación e importación para gestionar los datos de la cuenta." - muteAndBlockBanner: "Puedes configurar y gestionar ajustes para ocultar contenidos y restringir acciones a usuarios específicos." - accessibilityBanner: "Puedes personalizar los visuales y el comportamiento del cliente, y configurar los ajustes para optimizar el uso." - privacyBanner: "Puedes configurar opciones relacionadas con la privacidad de la cuenta, como la visibilidad del contenido, la posibilidad de descubrir la cuenta y la aprobación de seguimiento." - securityBanner: "Puedes configurar opciones relacionadas con la seguridad de la cuenta, como la contraseña, los métodos de inicio de sesión, las aplicaciones de autenticación y Passkeys." - preferencesBanner: "Puedes configurar el comportamiento general del cliente según tus preferencias." - appearanceBanner: "Puedes configurar el aspecto y la visualización del cliente según tus preferencias." - soundsBanner: "Puedes configurar los ajustes de sonido para la reproducción en el cliente." - timelineAndNote: "Líneas del tiempo y notas" - makeEveryTextElementsSelectable: "Hacer que todos los elementos de texto sean seleccionables" - makeEveryTextElementsSelectable_description: "Activar esta opción puede reducir la usabilidad en algunas situaciones." - useStickyIcons: "Hacer que los iconos te sigan cuando desplaces" - enableHighQualityImagePlaceholders: "Mostrar marcadores de posición para imágenes de alta calidad" - uiAnimations: "Animaciones de la interfaz de usuario" - showNavbarSubButtons: "Mostrar los sub-botones en la barra de navegación." - ifOn: "Si está activado" - ifOff: "Si está desactivado" - enableSyncThemesBetweenDevices: "Sincronizar los temas instalados entre dispositivos." - enablePullToRefresh: "Tirar para actualizar" - enablePullToRefresh_description: "Si utiliza un ratón, arrastre mientras pulsa la rueda de desplazamiento." - realtimeMode_description: "Establece una conexión con el servidor y actualiza el contenido en tiempo real. Esto puede aumentar el tráfico y el consumo de memoria." - contentsUpdateFrequency: "Frecuencia de adquisición del contenido." - contentsUpdateFrequency_description: "Cuanto mayor sea el valor, más se actualiza el contenido, pero disminuye el rendimiento y aumenta el tráfico y el consumo de memoria." - contentsUpdateFrequency_description2: "Cuando el modo en tiempo real está activado, el contenido se actualiza en tiempo real independientemente de esta configuración." - showUrlPreview: "Mostrar la vista previa de la URL" - _chat: - showSenderName: "Mostrar el nombre del remitente" - sendOnEnter: "Intro para enviar" -_preferencesProfile: - profileName: "Nombre de perfil" - profileNameDescription: "Establece un nombre que identifique al dispositivo" - profileNameDescription2: "Por ejemplo: \"PC Principal\",\"Teléfono\"" - manageProfiles: "Administrar perfiles" -_preferencesBackup: - autoBackup: "Respaldo automático" - restoreFromBackup: "Restaurar desde copia de seguridad" - noBackupsFoundTitle: "No se encontró una copia de seguridad" - noBackupsFoundDescription: "No se han encontrado copias de seguridad creadas automáticamente, pero si has guardado manualmente un archivo de copia de seguridad, puedes importarlo y restaurarlo." - selectBackupToRestore: "Selecciona una copia de seguridad para restaurar" - youNeedToNameYourProfileToEnableAutoBackup: "Se debe establecer un nombre de perfil para activar la copia de seguridad automática." - autoPreferencesBackupIsNotEnabledForThisDevice: "La copia de seguridad automática de los ajustes no está activada en este dispositivo." - backupFound: "Copia de seguridad de los ajustes encontrada " -_accountSettings: - requireSigninToViewContents: "Se requiere iniciar sesión para ver el contenido" - requireSigninToViewContentsDescription1: "Requiere iniciar sesión para ver todas las notas y otros contenidos que hayas creado. Se espera que esto evite que los rastreadores recopilen información." - requireSigninToViewContentsDescription2: "El contenido no se mostrará en vistas previas de URL (OGP), incrustado en páginas web o en servidores que no admitan citas de notas." - requireSigninToViewContentsDescription3: "Estas restricciones pueden no aplicarse a los contenidos federados de otros servidores remotos." - makeNotesFollowersOnlyBefore: "Hacer que las notas antiguas sólo se muestren a los seguidores" - makeNotesFollowersOnlyBeforeDescription: "Mientras esta función esté activada, sólo los seguidores podrán ver las notas que hayan superado la fecha y hora establecidas o que hayan estado visibles durante un tiempo determinado. Cuando se desactive, también se restablecerá el estado de publicación de la nota." - makeNotesHiddenBefore: "Hacer privadas las notas antiguas " - makeNotesHiddenBeforeDescription: "Mientras esta función esté activada, las notas que hayan pasado la fecha y hora fijadas o hayan transcurrido el tiempo establecido sólo serán visibles para ti (se harán privadas). Si la desactivas, también se restablecerá el estado público de las notas." - mayNotEffectForFederatedNotes: "Notas federadas por un servidor remoto pueden no verse afectadas." - mayNotEffectSomeSituations: "Estas restricciones son simplificadas. Pueden no aplicarse en algunas situaciones, como cuando se visualiza en un servidor remoto o durante la moderación." - notesHavePassedSpecifiedPeriod: "Ten en cuenta que el tiempo especificado ha pasado" - notesOlderThanSpecifiedDateAndTime: "Notas antes de la fecha y hora especificadas" -_abuseUserReport: - forward: "Reenviar" - forwardDescription: "Reenvía el informe a un servidor/instancia remoto como cuenta anónima del sistema." - resolve: "Resuelto" - accept: "Acepte" - reject: "repudio" - resolveTutorial: "Si el contenido del informe es legítimo, selecciona \"Aceptar\" para marcarlo como resuelto.\nSi el contenido del informe es ilegítimo, selecciona \"Rechazar\" para ignorarlo." -_delivery: - status: "Estado de la entrega" - stop: "Suspendido" - resume: "Resumen de entrega" - _type: - none: "Publicando" - manuallySuspended: "Suspendido manualmente" - goneSuspended: "El servidor se ha suspendido debido a la eliminación del servidor" - autoSuspendedForNotResponding: "El servidor se suspende debido a que el servidor no responde." - softwareSuspended: "Suspendido porque este software ya no se distribuye a" -_bubbleGame: - howToPlay: "Cómo jugar" - hold: "Mantener" - _score: - score: "Puntos" - scoreYen: "Cantidad de dinero ganada" - highScore: "Puntuación más alta" - maxChain: "Número máximo de cadenas" - yen: "{yen} Yenes" - estimatedQty: "{qty} Piezas" - scoreSweets: "{onigiriQtyWithUnit} Onigiris" - _howToPlay: - section1: "Ajuste la posición y deje caer el objeto en la caja" - section2: "Cuando dos objetos del mismo tipo se tocan, cambian a otro tipo y consigues puntos" - section3: "El juego termina cuando la caja se desborda de objetos. ¡Intenta conseguir una puntuación alta al juntar objetos mientras evitas desbordar la caja!" -_announcement: - forExistingUsers: "Solo para usuarios registrados" - forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán." - needConfirmationToRead: "Requerir confirmación de lectura aparte" - needConfirmationToReadDescription: "Si se habilita esta opción, se pedirá una confirmación de lectura aparte. Además, este anuncio será excluido de cualquier funcionalidad de \"Marcar todos como leídos\"." - end: "Anuncios archivados" - tooManyActiveAnnouncementDescription: "Tener demasiados anuncios activos empeora la experiencia de usuario. Por favor, considera archivar aquellos anuncios que hayan quedado obsoletos." - readConfirmTitle: "¿Marcar como leído?" - readConfirmText: "Esto marcará el contenido de \"{title}\" como leído." - shouldNotBeUsedToPresentPermanentInfo: "Dado que puede impactar en la experiencia de usuario de forma significativa, es recomendable usar notificaciones en el flujo de información en vez de información persistente." - dialogAnnouncementUxWarn: "Mostrar dos o más notificaciones en formato diálogo a la vez puede impactar en la experiencia de usuario de forma significativa, úsalos con cuidado." - silence: "Silenciar notificaciones" - silenceDescription: "Si lo activas, no enviarás notificación sobre este anuncio y el usuario no tendrá que leerlo." _initialAccountSetting: accountCreated: "¡La cuenta ha sido creada!" letsStartAccountSetup: "Para empezar, creemos tu perfil." @@ -1536,131 +1045,14 @@ _initialAccountSetting: theseSettingsCanEditLater: "Puedes cambiar estos ajustes más tarde." youCanEditMoreSettingsInSettingsPageLater: "Desde la pestaña de \"Configuración\" puedes modificar más ajustes. Asegúrate de visitarla después." followUsers: "Comienza a seguir a usuarios que te interesen para construir tu línea de tiempo." - pushNotificationDescription: "Habilitar las notificaciones push te permitirá recibir notificaciones de {name} directamente en tu dispositivo." - initialAccountSettingCompleted: "¡Configuración del perfil completada!" - haveFun: "¡Disfruta de {name}!" - youCanContinueTutorial: "Puedes proceder a un tutorial sobre cómo usar {name} (Misskey) o puedes terminar la instalación aquí y empezar a usarlo ya mismo." - startTutorial: "Comenzar tutorial" - skipAreYouSure: "¿Realmente quieres saltarte la configuración del perfil?" - laterAreYouSure: "¿Realmente quieres configurar tu perfil después?" -_initialTutorial: - launchTutorial: "Comenzar tutorial" - title: "Tutorial" - wellDone: "¡Bien hecho!" - skipAreYouSure: "¿Salir del tutorial?" - _landing: - title: "Bienvenid@ al tutorial" - description: "Aquí podrás aprender las nociones básicas sobre cómo usar Misskey y sus funciones." - _note: - title: "¿Qué es una nota?" - description: "Las publicaciones en Misskey se llaman 'Notas'. Las notas se ordenan de forma cronológica en la línea de tiempo y se actualizan en tiempo real." - reply: "Pulsa en este botón para contestar a un mensaje. También es posible contestar a otras contestaciones, continuando así la conversación como un hilo." - renote: "Puedes compartir esa nota en tu propia línea de tiempo. También puedes añadir una cita con tus comentarios." - reaction: "Puedes añadir reacciones a la Nota. Se explicarán más detalles en la siguiente página." - menu: "Puedes ver los detalles de la Nota, copiar enlaces, y realizar otras acciones." - _reaction: - title: "¿Qué son las reacciones?" - description: "Se puede reaccionar a las Notas con diferentes emojis. Las reacciones te permiten expresar matices que no se pueden transmitir con un simple 'me gusta'." - letsTryReacting: "Puedes añadir reacciones pulsando en el botón '+' de la nota. ¡Intenta reaccionar a esta nota de ejemplo!" - reactToContinue: "Añade una reacción para continuar." - reactNotification: "Recibirás notificaciones en tiempo real cuando alguien reaccione a tu nota." - reactDone: "Puedes deshacer una reacción pulsando en el botón '-'." - _timeline: - title: "El concepto de Línea de tiempo" - description1: "Misskey proporciona múltiples líneas de tiempo basadas en su uso (algunas pueden no estar disponibles dependiendo de las políticas de la instancia)." - home: "Puedes ver los posts de las cuentas que sigues." - local: "Puedes ver los posts de todos los usuarios de este servidor." - social: "Se ven los posts de la línea de tiempo de inicio junto con los de la línea de tiempo local." - global: "Puedes ver notas de todos los servidores conectados." - description2: "Puedes cambiar la línea de tiempo en la parte superior de la pantalla cuando quieras." - description3: "Además, hay listas de líneas de tiempo y listas de canales. Para más detalle, por favor visita este enlace: {link}" - _postNote: - title: "Ajustes de publicación de nota" - description1: "Cuando publicas una nota en Misskey, hay varias opciones disponibles. El formulario tiene este aspecto." - _visibility: - description: "Puedes limitar quién puede ver tu nota." - public: "Tu nota será visible para todos los usuarios." - home: "Publicar solo en la línea de tiempo de Inicio. La nota se verá en tu perfil, la verán tus seguidores y también cuando sea renotada." - followers: "Visible solo para seguidores. Sólo tus seguidores podrán ver la nota, y no podrá ser renotada por otras personas." - direct: "Visible sólo para usuarios específicos, y el destinatario será notificado. Puede usarse como alternativa a la mensajería directa." - doNotSendConfidencialOnDirect1: "¡Ten cuidado cuando vayas a enviar información sensible!" - doNotSendConfidencialOnDirect2: "Los administradores del servidor pueden leer lo que escribes. Ten cuidado cuando envíes información sensible en notas directas en servidores no confiables." - localOnly: "Publicando con esta opción seleccionada, la nota no se federará hacia otros servidores. Los usuarios de otros servidores no podrán ver estas notas directamente, sin importar los ajustes seleccionados más arriba." - _cw: - title: "Alerta de contenido (CW)" - description: "En lugar de mostrarse el contenido de la nota, se mostrará lo que escribas en el campo \"comentarios\". Pulsando en \"leer más\" desplegará el contenido de la nota." - _exampleNote: - cw: "¡Esto te hará tener hambre!" - note: "Acabo de comerme un donut de chocolate glaseado 🍩😋" - useCases: "Esto se usa cuando las normas del servidor lo requieren, o para ocultar spoilers o contenido sensible." - _howToMakeAttachmentsSensitive: - title: "¿Cómo puedo marcar adjuntos como contenido sensible?" - description: "Cuando las normas del servidor lo requieran, o el contenido lo requiera, marca la opción de \"contenido sensible\" para el adjunto." - tryThisFile: "¡Prueba a marcar la imagen adjunta como contenido sensible!" - _exampleNote: - note: "Ups, la he liado al abrir la tapa del natto..." - method: "Para marcar un adjunto como sensible, haz clic en la miniatura, abre el menú, y haz clic en \"Marcar como sensible\"." - sensitiveSucceeded: "Cuando adjuntes archivos, por favor, ten en cuenta las normas del servidor para marcarlos como contenido sensible." - doItToContinue: "Marca el archivo adjunto como sensible para continuar." - _done: - title: "¡Has completado el tutorial! 🎉" - description: "Las funciones que mostramos aquí son sólo una pequeña parte. Para más detalles sobre el funcionamiento de Misskey, pulsa en este enlace: {link}" -_timelineDescription: - home: "En la línea de tiempo de Inicio puedes ver las notas de las cuentas a las que sigues." - local: "En la línea de tiempo Local puedes ver las notas de todos los usuarios del servidor." - social: "En la línea de tiempo Social verás las notas de Inicio y Local a la vez." - global: "En la línea de tiempo Global verás las notas de todos los servidores conectados." -_serverRules: - description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado." -_serverSettings: - iconUrl: "URL del ícono" - appIconDescription: "Indica el icono que se va a usar cuando {host} se muestre como una app." - appIconUsageExample: "Por ejemplo, como PWA o cuando se muestre como un marcador en la pantalla inicial del dispositivo" - appIconStyleRecommendation: "Como el icono puede ser recortado como un cuadrado o un círculo, se recomienda un icono con un margen coloreado alrededor del contenido." - appIconResolutionMustBe: "La resolución mínima es {resolution}." - manifestJsonOverride: "Sobreescribir manifest.json" - shortName: "Nombre corto" - shortNameDescription: "Forma corta del nombre de la instancia que puede mostrarse si el nombre completo es demasiado largo." - fanoutTimelineDescription: "Incrementa el rendimiento de forma significativa cuando se obtienen las líneas de tiempo y reduce la carga en la base de datos. A cambio, el uso de la memoria en Redis incrementará. Considera desactivar esta opción en caso de que tu servidor tenga poca memoria o detectes inestabilidad." - fanoutTimelineDbFallback: "Cargar desde la base de datos" - fanoutTimelineDbFallbackDescription: "Cuando esta opción está habilitada, la carga de peticiones adicionales de la línea de tiempo se hará desde la base de datos cuando éstas no se encuentren en la caché. Al deshabilitar esta opción se reduce la carga del servidor, pero limita el número de líneas de tiempo que pueden obtenerse." - reactionsBufferingDescription: "Cuando se activa, el rendimiento durante la creación de reacciones mejorará considerablemente, reduciendo la carga de la base de datos. Sin embargo, aumentará el uso de memoria de Redis." - inquiryUrl: "URL de consulta " - inquiryUrlDescription: "Especifica una URL para el formulario de consulta al responsable del servidor o una página web para la información de contacto." - openRegistration: "Registros Abiertos" - openRegistrationWarning: "Abrir registros conlleva riesgos. Se recomienda solo habilitarlos si tienes un sistema en el cual puedes monitorear continuamente el servidor y respondes inmediatamente en caso de que haya cualquier problema." - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no se ha detectado por un tiempo actividad de un moderador, este ajuste será automáticamente desactivado para prevenir el spam. " - deliverSuspendedSoftware: "Software suspendido." - deliverSuspendedSoftwareDescription: "Puede especificar un rango de nombres y versiones del software del servidor para detener la entrega, por ejemplo, debido a vulnerabilidades. Esta información sobre la versión la proporciona el servidor y su fiabilidad no está garantizada. Se puede utilizar una especificación de rango para especificar una versión, pero se recomienda especificar una versión previa, como >= 2024.3.1-0, ya que especificar >= 2024.3.1 no incluirá versiones personalizadas como 2024.3.1-custom.0." - singleUserMode: "Modo de usuario único" - singleUserMode_description: "Si eres el único usuario de este servidor, activar este modo optimizará su rendimiento." - signToActivityPubGet: "Firmar solicitudes GET de Activitypub." - signToActivityPubGet_description: "Normalmente, debería estar activada. Deshabilitarlo puede mejorar los problemas relacionados con la federación, pero por otro lado podría deshabilitar la federación hacia otros servidores." - proxyRemoteFiles: "Proxy de archivos remotos" - proxyRemoteFiles_description: "Cuando se activa, el servidor proxy sirve archivos remotos. Esto es útil para generar miniaturas de imágenes y proteger la privacidad del usuario." - allowExternalApRedirect: "Permitir redirecciones para consultas vía ActivityPub" - allowExternalApRedirect_description: "Si se activa, otros servidores pueden consultar contenidos de terceros a través de este servidor, pero esto puede dar lugar a la suplantación de contenidos." - userGeneratedContentsVisibilityForVisitor: "Visibilidad de contenido generado por un usuario a invitados" - userGeneratedContentsVisibilityForVisitor_description: "Esto es útil para evitar problemas causados por contenidos remotos inapropiados que no estén bien moderados y que se publiquen involuntariamente en Internet a través de su propio servidor." - userGeneratedContentsVisibilityForVisitor_description2: "Publicar incondicionalmente todo el contenido del servidor en Internet, incluido el contenido remoto recibido por el servidor, es arriesgado. Esto es especialmente importante para los invitados que desconocen la naturaleza distribuida del contenido, ya que pueden creer erróneamente que incluso el contenido remoto es contenido creado por usuarios en el servidor." - _userGeneratedContentsVisibilityForVisitor: - all: "Todo es público." - localOnly: "Sólo se publica el contenido local, el remoto se mantiene privado" - none: "Todo es privado" _accountMigration: moveFrom: "Trasladar de otra cuenta a ésta" - moveFromSub: "Crear un alias para otra cuenta." - moveFromLabel: "Cuenta desde la que se realiza el traslado #{n}" + moveFromLabel: "Cuenta desde la que se realiza el traslado:" moveFromDescription: "Si quieres transferir seguidores de otra cuenta a esta cuenta y trasladarlos, tendrás que crear un alias aquí. Asegúrate de crearlo antes de realizar el traslado. Introduce la cuenta desde la que estás moviendo los seguidores así: @person@instance.com" moveTo: "Mover esta cuenta a una nueva" moveToLabel: "Cuenta destino:" - moveCannotBeUndone: "La migración de la cuenta no puede ser revertida." moveAccountDescription: "Esta operación no puede deshacerse. En primer lugar, asegúrese de haber creado un alias para esta cuenta en la cuenta a la que se va a trasladar. Después de crear el alias, introduzca la cuenta a la que se está trasladando de la siguiente manera: @person@instance.com" - moveAccountHowTo: "Para migrar, primero crea un alias para ésta cuenta en la cuenta a donde te moverás.\nDespués de crear el alias, ingresa la cuenta a mover de la siguiente forma:\n@usuario@servidor.ejempo.com" - startMigration: "Migrar" migrationConfirm: "¿Estás seguro de que quieres mover esta cuenta a {account}? Una vez trasladada, no podrás deshacer el traslado y no podrás volver a utilizar la cuenta original.\n\nAdemás, compruebe que ha configurado un alias en el destino del traslado." - movedAndCannotBeUndone: "\nLa migración decuenta ha sido completada.\nNo se puede revertir éste proceso." - postMigrationNote: "Ésta cuenta dejará de seguir a todas las cuentas en las siguientes 24 horas después de que finalice la migración.\nEl número de seguidos y seguidores serán 0. Para evitar que Para evitar que tus seguidores dejen de ver las publicaciones, todas serán marcadas como \"sólo seguidores\"." movedTo: "Cuenta destino:" _achievements: earnedAt: "Desbloqueado el" @@ -1835,7 +1227,6 @@ _achievements: description: "30 minutos dedicados a Misskey" _client60min: title: "Viendo mucho Misskey." - description: "Dejar abierto Misskey por al menos 60 minutos" _noteDeletedWithin1min: title: "Ah... Mejor no..." description: "Borrar una nota antes que de pase 1 minuto" @@ -1901,19 +1292,6 @@ _achievements: title: "Brain Diver" description: "Publicaste un vínculo a \"Brain Diver\"" flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "Sobrecarga de pruebas" - description: "Envía muchas notificaciones de prueba en un corto espacio de tiempo" - _tutorialCompleted: - title: "Diploma del Curso Básico de Misskey" - description: "Tutorial completado" - _bubbleGameExplodingHead: - title: "🤯" - description: "El objeto más grande en el juego de burbujas" - _bubbleGameDoubleExplodingHead: - title: "Doble 🤯" - description: "Dos de los objetos más grandes en el juego de burbujas al mismo tiempo" - flavor: "Puedes llenar el bento un poco de esta forma 🤯 🤯." _role: new: "Crear rol" edit: "Editar rol" @@ -1924,9 +1302,7 @@ _role: assignTarget: "Asignar objetivo" descriptionOfAssignTarget: "Manual Para cambiar manualmente lo que se incluye en este rol.\nCondicional configura una condición, y los usuarios que cumplan la condición serán incluídos automáticamente." manual: "manual" - manualRoles: "Roles manuales" conditional: "condicional" - conditionalRoles: "Roles condicionales" condition: "condición" isConditionalRole: "Esto es un rol condicional" isPublic: "Publicar rol" @@ -1939,12 +1315,8 @@ _role: iconUrl: "URL del ícono" asBadge: "Mostrar como emblema" descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo." - isExplorable: "Hacer el rol explorable" - descriptionOfIsExplorable: "La línea de tiempo de éste rol y la lista de usuarios serán públicos si se activa.." displayOrder: "Posición" descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz." - preserveAssignmentOnMoveAccount: "Preservar los roles asignados durante la migración" - preserveAssignmentOnMoveAccount_description: "Si está activada, este rol se transferirá a la cuenta de destino cuando se migre una cuenta con este rol." canEditMembersByModerator: "Permitir a los moderadores editar los miembros" descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo." priority: "Prioridad" @@ -1956,17 +1328,9 @@ _role: gtlAvailable: "Explorar la línea de tiempo global" ltlAvailable: "Explorar la línea de tiempo local" canPublicNote: "Permitir la publicación" - mentionMax: "Número máximo de menciones en una nota" canInvite: "Puede crear códigos de invitación" - inviteLimit: "Límite de invitaciones" - inviteLimitCycle: "Enfriamiento del límite de invitaciones" - inviteExpirationTime: "Intervalo de caducidad de invitaciones" canManageCustomEmojis: "Administrar emojis personalizados" - canManageAvatarDecorations: "Administrar decoraciones de avatar" driveCapacity: "Capacidad del drive" - maxFileSize: "Tamaño máximo de archivo que se puede cargar." - alwaysMarkNsfw: "Siempre marcar archivos como NSFW" - canUpdateBioMedia: "Puede editar un icono o una imagen de fondo (banner)" pinMax: "Máximo de notas fijadas" antennaMax: "Máximo de antenas" wordMuteMax: "Máximo de caracteres en palabras silenciadas" @@ -1979,26 +1343,9 @@ _role: descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos" canHideAds: "Puede ocultar anuncios" canSearchNotes: "Uso de la búsqueda de notas" - canUseTranslator: "Uso de traductor" - avatarDecorationLimit: "Número máximo de decoraciones de avatar" - canImportAntennas: "Permitir la importación de antenas" - canImportBlocking: "Permitir la importación de bloqueos" - canImportFollowing: "Permitir la importación de seguidos" - canImportMuting: "Permitir la importación de silenciados" - canImportUserLists: "Permitir la importación de listas" - chatAvailability: "Permitir Chats" - uploadableFileTypes: "Tipos de archivos que se pueden cargar." - uploadableFileTypes_caption: "Especifica los tipos MIME/archivos permitidos. Se pueden especificar varios tipos MIME separándolos con una nueva línea, y se pueden especificar comodines con un asterisco (*). (por ejemplo, image/*)" - uploadableFileTypes_caption2: "Es posible que no se detecten algunos tipos de archivos. Para permitir estos archivos, añade {x} a la especificación." _condition: - roleAssignedTo: "Asignado a roles manuales" isLocal: "Usuario local" isRemote: "Usuario remoto" - isCat: "Usuarios Gato" - isBot: "Usuarios Bot" - isSuspended: "Usuario suspendido" - isLocked: "Cuentas privadas" - isExplorable: "Hacer que la cuenta sea visible en las búsquedas" createdLessThan: "Menos de X han pasado desde la creación de la cuenta" createdMoreThan: "Más de X han pasado desde la creación de la cuenta" followersLessThanOrEq: "Tiene X o menos seguidores" @@ -2024,7 +1371,6 @@ _emailUnavailable: disposable: "No es un correo reutilizable" mx: "Servidor de correo inválido" smtp: "Servidor de correo no disponible" - banned: "Email no disponible" _ffVisibility: public: "Publicar" followers: "Visible solo para seguidores" @@ -2044,11 +1390,6 @@ _ad: back: "Deseleccionar" reduceFrequencyOfThisAd: "Mostrar menos este anuncio." hide: "No mostrar" - timezoneinfo: "El día de la semana está determidado por la zona horaria del servidor." - adsSettings: "Ajustes de anuncios" - notesPerOneAd: "Intervalo de actualización de anuncios en tiempo real (Notas por cada anuncio)" - setZeroToDisable: "Establece este valor a 0 para deshabilitar la actualización de anuncios en tiempo real" - adsTooClose: "El intervalo de anuncios actual puede empeorar la experiencia del usuario por ser demasiado bajo." _forgotPassword: enterEmail: "Ingrese el correo usado para registrar la cuenta. Se enviará un link para resetear la contraseña." ifNoEmail: "Si no utilizó un correo para crear la cuenta, contáctese con el administrador." @@ -2067,8 +1408,6 @@ _plugin: install: "Instalar plugins" installWarn: "Por favor no instale plugins que no son de confianza" manage: "Gestionar plugins" - viewSource: "Ver la fuente" - viewLog: "Ver log" _preferencesBackups: list: "Respaldos creados" saveNew: "Guardar nuevo respaldo" @@ -2098,17 +1437,10 @@ _aboutMisskey: contributors: "Principales colaboradores" allContributors: "Todos los colaboradores" source: "Código fuente" - original: "Original" - thisIsModifiedVersion: "{name} usa una versión modificada de Misskey." translation: "Traducir Misskey" donate: "Donar a Misskey" morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰" patrons: "Patrocinadores" - projectMembers: "Miembros del proyecto" -_displayOfSensitiveMedia: - respect: "Esconder medios marcados como sensibles" - ignore: "Mostrar medios marcados como sensibles" - force: "Esconder todala multimedia" _instanceTicker: none: "No mostrar" remote: "Mostrar a usuarios remotos" @@ -2127,9 +1459,6 @@ _channel: following: "Siguiendo" usersCount: "{n} participantes" notesCount: "{n} notas" - nameAndDescription: "Nombre y descripción" - nameOnly: "Sólo nombre" - allowRenoteToExternal: "Permitir renotas y menciones fuera del canal" _menuDisplay: sideFull: "Horizontal" sideIcon: "Horizontal (ícono)" @@ -2139,6 +1468,11 @@ _wordMute: muteWords: "Palabras que silenciar" muteWordsDescription: "Separar con espacios indica una declaracion And, separar con lineas nuevas indica una declaracion Or。" muteWordsDescription2: "Encerrar las palabras clave entre numerales para usar expresiones regulares" + softDescription: "Ocultar en la linea de tiempo las notas que cumplen las condiciones" + hardDescription: "Evitar que se agreguen a la linea de tiempo las notas que cumplen las condiciones. Las notas no agregadas seguirán quitadas aunque cambien las condiciones." + soft: "Suave" + hard: "Duro" + mutedNotes: "Notas silenciadas" _instanceMute: instanceMuteDescription: "Silencia todas las notas y reposts de la instancias seleccionadas, incluyendo respuestas a los usuarios de las mismas" instanceMuteDescription2: "Separar por líneas" @@ -2153,7 +1487,6 @@ _theme: installed: "{name} ha sido instalado" installedThemes: "Temas instalados" builtinThemes: "Temas integrados" - instanceTheme: "Tema del servidor (o también denominado: tema de la instancia)" alreadyInstalled: "Este tema ya está instalado" invalid: "El formato del tema no es válido" make: "Crear tema" @@ -2186,6 +1519,7 @@ _theme: header: "Cabezal" navBg: "Fondo de la barra lateral" navFg: "Texto de la barra lateral" + navHoverFg: "Texto de la barra lateral (hover)" navActive: "Texto de la barra lateral (activo)" navIndicator: "Indicador de la barra lateral" link: "Vínculo" @@ -2202,28 +1536,30 @@ _theme: infoFg: "Texto de información" infoWarnBg: "Fondo de advertencias" infoWarnFg: "Texto de advertencias" + cwBg: "Fondo del botón CW" + cwFg: "Texto del botón CW" + cwHoverBg: "Fondo del botón CW (hover)" toastBg: "Fondo de notificaciones" toastFg: "Texto de notificaciones" buttonBg: "Fondo de botón" buttonHoverBg: "Fondo de botón (hover)" inputBorder: "Borde de los campos de entrada" + listItemHoverBg: "Fondo de elemento de listas (hover)" + driveFolderBg: "Fondo de capeta del drive" + wallpaperOverlay: "Transparencia del fondo de pantalla" badge: "Medalla" messageBg: "Fondo de chat" + accentDarken: "Acento (oscuro)" + accentLighten: "Acento (claro)" fgHighlighted: "Texto resaltado" _sfx: note: "Notas" noteMy: "Nota (a mí mismo)" notification: "Notificaciones" - reaction: "Al seleccionar una reacción" - chatMessage: "Mensajes del Chat" -_soundSettings: - driveFile: "Usar un archivo de audio en Drive" - driveFileWarn: "Selecciona un archivo de audio en Drive." - driveFileTypeWarn: "Este archivo es incompatible" - driveFileTypeWarnDescription: "Selecciona un archivo de audio" - driveFileDurationWarn: "La duración del audio es demasiado larga." - driveFileDurationWarnDescription: "Usar un audio de larga duración puede llegar a molestar mientras usas Misskey. ¿Quieres continuar?" - driveFileError: "No puedo cargar el sonido. Por favor cambia la configuración." + chat: "Chat" + chatBg: "Chat (Fondo)" + antenna: "Antena receptora" + channel: "Notificaciones del canal" _ago: future: "Futuro" justNow: "Justo ahora" @@ -2235,32 +1571,29 @@ _ago: monthsAgo: "Hace {n} meses" yearsAgo: "Hace {n} años" invalid: "No hay nada que ver aqui" -_timeIn: - seconds: "En {n} segundos" - minutes: "En {n}m" - hours: "En {n}h" - days: "En {n}d" - weeks: "En {n}sem." - months: "En {n}M" - years: "En {n} años" _time: second: "Segundos" minute: "Minutos" hour: "Horas" day: "Días" +_timelineTutorial: + step4_1: "También puedes añadir \"Reacciones\" a notas." + step4_2: "Para añadir una reacción selecciona el botón \"+\" en la nota y escoge el emoji que quieras para reaccionar." _2fa: alreadyRegistered: "Ya has completado la configuración." registerTOTP: "Registrar aplicación autenticadora" + passwordToTOTP: "Ingresa tu contraseña" step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra." step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla." - step2Uri: "Si usas una aplicación de escritorio, introduce en ella la siguiente URL." + step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app.\nTocar este código QR te permitirá registrar la autenticación 2FA a tu llave de seguridad o aplicación autenticadora." + step2Url: "En una aplicación de escritorio se puede ingresar la siguiente URL:" step3Title: "Ingresa un código de autenticación" step3: "Para terminar, ingrese el token mostrado en la aplicación." - setupCompleted: "Configuración completada" step4: "Ahora cuando inicie sesión, ingrese el mismo token" securityKeyNotSupported: "Tu navegador no soporta claves de autenticación." registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key.\npor favor. configura una aplicación de autenticación para registrar una llave de seguridad." securityKeyInfo: "Se puede configurar el inicio de sesión usando una clave de seguridad de hardware que soporte FIDO2 o con un certificado de huella digital o con un PIN" + chromePasskeyNotSupported: "Las llaves de seguridad de Chrome no son soportadas por el momento." registerSecurityKey: "Registrar una llave de seguridad" securityKeyName: "Ingresa un nombre para la clave" tapSecurityKey: "Por favor, sigue tu navegador para registrar una llave de seguridad" @@ -2271,12 +1604,6 @@ _2fa: renewTOTPConfirm: "This will cause verification codes from your previous app to stop working\nEsto hará que los códigos de verificación de la aplicación anterior dejen de funcionar" renewTOTPOk: "Reconfigurar" renewTOTPCancel: "No gracias" - checkBackupCodesBeforeCloseThisWizard: "Por favor, copia los siguientes códigos de respaldo antes de finalizar el asistente." - backupCodes: "Códigos de Respaldo" - backupCodesDescription: "En caso de que no puedas usar tu aplicación de autenticación, podrás usar los códigos de respaldo que figuran abajo para acceder a tu cuenta. Asegúrate de guardar en lugar seguro los códigos de respaldo. Cada uno de los códigos de respaldo es de un solo uso." - backupCodeUsedWarning: "Has usado todos los códigos de respaldo. Si dejas de tener acceso a tu aplicación de autenticación, no podrás volver a iniciar sesión en tu cuenta. Por favor, reconfigura tu aplicación de autenticación lo antes posible." - backupCodesExhaustedWarning: "Has usado todos los códigos de respaldo. Si dejas de tener acceso a tu aplicación de autenticación, no podrás volver a iniciar sesión en la cuenta que figura arriba. Por favor, reconfigura tu aplicación de autenticación lo antes posible." - moreDetailedGuideHere: "Guía detallada" _permissions: "read:account": "Ver información de la cuenta" "write:account": "Editar información de la cuenta" @@ -2310,60 +1637,6 @@ _permissions: "write:gallery": "Editar galería" "read:gallery-likes": "Ver favoritos de la galería" "write:gallery-likes": "Editar favoritos de la galería" - "read:flash": "Ver Play" - "write:flash": "Editar Plays" - "read:flash-likes": "Ver los Play que me gustan" - "write:flash-likes": "Editar lista de Play que me gustan" - "read:admin:abuse-user-reports": "Ver reportes de usuarios" - "write:admin:delete-account": "Eliminar cuentas de usuario" - "write:admin:delete-all-files-of-a-user": "Eliminar todos los archivos de un usuario" - "read:admin:index-stats": "Ver datos indexados" - "read:admin:table-stats": "Ver estadísticas de las tablas de la base de datos" - "read:admin:user-ips": "Ver dirección IP de usuario" - "read:admin:meta": "Ver metadatos de la instancia" - "write:admin:reset-password": "Restablecer contraseñas de usuario" - "write:admin:resolve-abuse-user-report": "Resolución de reportes de usuario" - "write:admin:send-email": "Enviar email" - "read:admin:server-info": "Ver información del servidor" - "read:admin:show-moderation-log": "Ver log de moderación" - "read:admin:show-user": "Ver información privada de usuario" - "write:admin:suspend-user": "Suspender cuentas de usuario" - "write:admin:unset-user-avatar": "Quitar avatares de usuario" - "write:admin:unset-user-banner": "Quitar banner de usuarios" - "write:admin:unsuspend-user": "Quitar suspensión de cuentas de usuario" - "write:admin:meta": "Edición de metadatos de la instancia" - "write:admin:user-note": "Moderación de notas" - "write:admin:roles": "Edición de roles de usuario" - "read:admin:roles": "Ver roles de usuario" - "write:admin:relays": "Edición de relays" - "read:admin:relays": "Ver relays" - "write:admin:invite-codes": "Edición de códigos de invitación" - "read:admin:invite-codes": "Ver códigos de invitación" - "write:admin:announcements": "Edición de anuncios" - "read:admin:announcements": "Ver anuncios" - "write:admin:avatar-decorations": "Edición de decoración de avatares" - "read:admin:avatar-decorations": "Ver decoraciones de avatar" - "write:admin:federation": "Edición de federación de instancias" - "write:admin:account": "Edición de cuentas de usuario" - "read:admin:account": "Ver cuentas de usuario" - "write:admin:emoji": "Edición de emojis" - "read:admin:emoji": "Ver emojis" - "write:admin:queue": "Edición de cola de tareas" - "read:admin:queue": "Ver cola de tareas" - "write:admin:promo": "Edición de promociones" - "write:admin:drive": "Edición de Drive de usuarios" - "read:admin:drive": "Ver Drive de usuarios" - "read:admin:stream": "Usar la API de Websocket para administradores" - "write:admin:ad": "Edición de anuncios" - "read:admin:ad": "Ver anuncios" - "write:invite-codes": "Crear códigos de invitación" - "read:invite-codes": "Ver códigos de invitación" - "write:clip-favorite": "Marcar me gusta en clips" - "read:clip-favorite": "Ver los clips que me gustan" - "read:federation": "Ver instancias federadas" - "write:report-abuse": "Crear reportes de usuario" - "write:chat": "Administrar chat" - "read:chat": "Explorar Chats" _auth: shareAccessTitle: "Permisos de la aplicación" shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?" @@ -2372,17 +1645,13 @@ _auth: permissionAsk: "Esta aplicación requiere los siguientes permisos" pleaseGoBack: "Por favor, vuelve a la aplicación" callback: "Volviendo a la aplicación" - accepted: "Acceso concedido." denied: "Acceso denegado" - scopeUser: "Operar como el siguiente usuario" pleaseLogin: "Se requiere un inicio de sesión para darle permisos a la aplicación" - byClickingYouWillBeRedirectedToThisUrl: "Cuando el acceso es concedido, serás automáticamente redireccionado a la siguiente URL" _antennaSources: all: "Todas las notas" homeTimeline: "Notas de los usuarios que sigues" users: "Notas de un usuario o varios" userList: "Notas de los usuarios de una lista" - userBlacklist: "Todas las notas excepto aquellas de uno o más usuarios especificados" _weekday: sunday: "Domingo" monday: "Lunes" @@ -2421,8 +1690,6 @@ _widgets: _userList: chooseList: "Seleccione una lista" clicker: "Cliqueador" - birthdayFollowings: "Hoy cumplen años" - chat: "Chat" _cw: hide: "Ocultar" show: "Ver más" @@ -2484,22 +1751,15 @@ _profile: metadataContent: "Contenido" changeAvatar: "Cambiar avatar" changeBanner: "Cambiar banner" - verifiedLinkDescription: "Introduciendo una URL que contiene un enlace a tu perfil, se puede mostrar un icono de verificación de propiedad al lado del campo." - avatarDecorationMax: "Puedes añadir un máximo de {max} decoraciones de avatar." - followedMessage: "Mensaje cuando te han seguido" - followedMessageDescription: "Puedes establecer un mensaje de bienvenida para nuevos seguidores." - followedMessageDescriptionForLockedAccount: "Si apruebas manualmente seguidores, el mensaje se mostrará al seguidor en el momento de la aprobación." _exportOrImport: allNotes: "Todas las notas" favoritedNotes: "Notas favoritas" - clips: "Clip" followingList: "Siguiendo" muteList: "Silenciados" blockingList: "Bloqueados" userLists: "Listas" excludeMutingUsers: "Excluir usuarios silenciados" excludeInactiveUsers: "Excluir usuarios inactivos" - withReplies: "Incluir respuestas de los usuarios importados en la línea de tiempo" _charts: federation: "Federación" apRequest: "Pedidos" @@ -2546,11 +1806,13 @@ _play: title: "Título" script: "Script" summary: "Descripción" - visibilityDescription: "Poniéndola como privada significa que no será visible en tu perfil, pero cualquiera que tenga la URL aún podrá acceder a ella." _pages: newPage: "Crear página" editPage: "Editar página" readPage: "Viendo la fuente" + created: "La página fue creada" + updated: "La página fue actualizada" + deleted: "La página borrada" pageSetting: "Configurar página" nameAlreadyExists: "La URL de la página especificada ya existe" invalidNameTitle: "URL inválida" @@ -2578,7 +1840,6 @@ _pages: eyeCatchingImageSet: "Elegir imagen llamativa" eyeCatchingImageRemove: "Borrar imagen llamativa" chooseBlock: "Agregar bloque" - enterSectionTitle: "Escribe el título de la sección" selectType: "Elegir tipo" contentBlocks: "Contenido" inputBlocks: "Entrada" @@ -2589,8 +1850,6 @@ _pages: section: "Sección" image: "Imagen" button: "Botón" - dynamic: "Bloques Dinámicos" - dynamicDescription: "Los bloques dinámicos están obsoletos. A partir de ahora, utiliza {play} por favor." note: "Nota embebida" _note: id: "Id de la nota" @@ -2610,27 +1869,11 @@ _notification: youReceivedFollowRequest: "Has mandado una solicitud de seguimiento" yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada" pollEnded: "Estan disponibles los resultados de la encuesta" - newNote: "Nueva nota" unreadAntennaNote: "Antena {name}" - roleAssigned: "Rol asignado" - chatRoomInvitationReceived: "Invitado a la sala de chat." emptyPushNotificationMessage: "Se han actualizado las notificaciones push" achievementEarned: "Logro desbloqueado" - testNotification: "Notificación de prueba" - checkNotificationBehavior: "Comprobar comportamiento de la notificación" - sendTestNotification: "Enviar notificación de prueba" - notificationWillBeDisplayedLikeThis: "Las notificaciones tendrán este aspecto" - reactedBySomeUsers: "{n} usuarios han reaccionado" - likedBySomeUsers: "{n} usuarios les gustó tu nota" - renotedBySomeUsers: "{n} usuarios han renotado" - followedBySomeUsers: "Seguido por {n} usuarios" - flushNotification: "Limpiar notificaciones" - exportOfXCompleted: "La exportación de {x} ha sido completada." - login: "Alguien ha iniciado sesión" - createTokenDescription: "Si no tienes ni idea, elimina el token de acceso a través de \"{text}\"." _types: all: "Todo" - note: "Nuevas notas" follow: "Siguiendo" mention: "Menciones" reply: "Respuestas" @@ -2640,13 +1883,7 @@ _notification: pollEnded: "La encuesta terminó" receiveFollowRequest: "Recibió una solicitud de seguimiento" followRequestAccepted: "El seguimiento fue aceptado" - roleAssigned: "Rol asignado" - chatRoomInvitationReceived: "Invitado a la sala de chat." achievementEarned: "Logro desbloqueado" - exportCompleted: "La exportación se ha completado" - login: "Iniciar sesión" - createToken: "Crear tokens de acceso" - test: "Pruebas de nofiticaciones" app: "Notificaciones desde aplicaciones" _actions: followBack: "Te sigue de vuelta" @@ -2655,11 +1892,7 @@ _notification: _deck: alwaysShowMainColumn: "Siempre mostrar la columna principal" columnAlign: "Alinear columnas" - columnGap: "Margen entre columnas" - deckMenuPosition: "Posición del menú Deck" - navbarPosition: "Posición de la barra de navegación" addColumn: "Agregar columna" - newNoteNotificationSettings: "Configuración de las notificaciones para notas nuevas" configureColumn: "Ajustes de columna" swapLeft: "Mover a la izquierda" swapRight: "Mover a la derecha" @@ -2673,10 +1906,6 @@ _deck: introduction: "¡Crea la interfaz perfecta para tí organizando las columnas libremente!" introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras." widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget." - useSimpleUiForNonRootPages: "Mostrar páginas no pertenecientes a la raíz con la interfaz simple" - usedAsMinWidthWhenFlexible: "Se usará el ancho mínimo cuando la opción \"Autoajustar ancho\" esté habilitada" - flexible: "Autoajustar ancho" - enableSyncBetweenDevicesForProfiles: "Activar la sincronización de la información de perfiles entre dispositivos." _columns: main: "Principal" widgets: "Widgets" @@ -2687,8 +1916,6 @@ _deck: channel: "Canal" mentions: "Menciones" direct: "Notas directas" - roleTimeline: "Linea de tiempo del rol" - chat: "Chat" _dialog: charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}." charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}." @@ -2700,10 +1927,9 @@ _drivecleaner: orderByCreatedAtAsc: "Fecha ascendente" _webhookSettings: createWebhook: "Crear Webhook" - modifyWebhook: "Editar webhook" name: "Nombre" secret: "Secreto" - trigger: "Disparador" + events: "Eventos de webhook" active: "Activado" _events: follow: "Cuando se sigue a alguien" @@ -2713,223 +1939,3 @@ _webhookSettings: renote: "Cuando reciba un \"re-note\"" reaction: "Cuando se recibe una reacción" mention: "Cuando hay una mención" - _systemEvents: - abuseReport: "Cuando se recibe un nuevo informe de moderación" - abuseReportResolved: "Cuando se resuelve un informe de moderación" - userCreated: "Cuando se crea el usuario." - inactiveModeratorsWarning: "Cuando un moderador ha estado inactivo por un tiempo" - inactiveModeratorsInvitationOnlyChanged: "Cuando un moderador ha estado inactivo durante un tiempo, y el servidor se cambia a sólo por invitación" - deleteConfirm: "¿Estás seguro de querer eliminar el Webhook?" - testRemarks: "Haz clic en el botón de la derecha del switch para mandar una prueba Webhook con datos ficticios" -_abuseReport: - _notificationRecipient: - createRecipient: "Añadir destinatario a los informes" - _recipientType: - mail: "Correo" - webhook: "Webhook" - keywords: "Palabras Clave" -_moderationLogTypes: - createRole: "Rol creado" - deleteRole: "Rol eliminado" - updateRole: "Rol actualizado" - assignRole: "Rol asignado" - unassignRole: "Rol retirado" - suspend: "Suspender" - unsuspend: "Suspensión retirada" - addCustomEmoji: "Añadido emoji personalizado" - updateCustomEmoji: "Emoji personalizado actualizado" - deleteCustomEmoji: "Emoji personalizado eliminado" - updateServerSettings: "Ajustes de servidor actualizados" - updateUserNote: "Nota de moderación actualizada" - deleteDriveFile: "Archivo eliminado" - deleteNote: "Nota eliminada" - createGlobalAnnouncement: "Anuncio global creado" - createUserAnnouncement: "Anuncio de usuario creado" - updateGlobalAnnouncement: "Anuncio global actualizado" - updateUserAnnouncement: "Anuncio de usuario actualizado" - deleteGlobalAnnouncement: "Anuncio global eliminado" - deleteUserAnnouncement: "Anuncio de usuario eliminado" - resetPassword: "Resetear contraseña" - suspendRemoteInstance: "Instancia remota suspendida" - unsuspendRemoteInstance: "Suspensión de instancia remota retirada" - markSensitiveDriveFile: "Archivo marcado como sensible" - unmarkSensitiveDriveFile: "Archivo marcado como no sensible" - resolveAbuseReport: "Reporte resuelto" - createInvitation: "Generar invitación" - createAd: "Anuncio creado" - deleteAd: "Anuncio eliminado" - updateAd: "Anuncio actualizado" - createAvatarDecoration: "Decoración de avatar creada" - updateAvatarDecoration: "Decoración de avatar actualizada" - deleteAvatarDecoration: "Decoración de avatar eliminada" - unsetUserAvatar: "Quitar decoración de avatar de este usuario" - unsetUserBanner: "Quitar banner de este usuario" -_fileViewer: - title: "Detalles del archivo" - type: "Tipo de archivo" - size: "Tamaño del archivo" - url: "URL" - uploadedAt: "Subido el" - attachedNotes: "Notas adjuntas" - thisPageCanBeSeenFromTheAuthor: "Esta página solo puede ser vista por el autor." -_externalResourceInstaller: - title: "Instalar desde sitio externo" - checkVendorBeforeInstall: "Asegúrate de que el distribuidor de este recurso es de confianza antes de proceder a la instalación." - _plugin: - title: "¿Quieres instalar este plugin?" - _theme: - title: "¿Quieres instalar este tema?" - _meta: - base: "Esquema de color base" - _vendorInfo: - title: "Información del distribuidor" - endpoint: "Terminal referenciada" - hashVerify: "Verificación de hash" - _errors: - _invalidParams: - title: "Parámetros inválidos" - description: "No hay información suficiente para cargar datos de un sitio externo. Por favor, confirma la URL introducida." - _resourceTypeNotSupported: - title: "Este recurso externo no es compatible" - description: "El tipo de este recurso externo no es compatible. Por favor, contacta con el administrador del sitio." - _failedToFetch: - title: "No se pudo obtener los datos" - fetchErrorDescription: "Ha ocurrido un error al comunicarse con el sitio externo. Si no se soluciona tras intentarlo otra vez, por favor, contacta con el administrador del sitio." - parseErrorDescription: "Ha ocurrido un error al procesar los datos obtenidos del sitio externo. Por favor, contacta con el administrador del sitio." - _hashUnmatched: - title: "Verificación de datos fallida" - description: "Ha ocurrido un error al verificar la integridad de los datos obtenidos. Por seguridad, la instalación no se puede realizar. Por favor, contacta con el administrador del sitio." - _pluginParseFailed: - title: "Error de AiScript" - description: "Los datos se han obtenido correctamente, pero ha ocurrido un error de AiScript al procesarlos. Por favor, contacta con el autor del plugin. Se pueden ver más detalles del error en la consola de Javascript." - _pluginInstallFailed: - title: "Instalación del plugin fallida." - description: "Ha ocurrido un problema al instalar el plugin. Por favor, inténtalo de nuevo. Se pueden ver más detalles del error en la consola de Javascript." - _themeParseFailed: - title: "Análisis del tema fallido" - description: "Los datos se han obtenido correctamente, pero ha ocurrido un error al analizar el tema. Por favor, contacta con el autor. Se pueden ver más detalles del error en la consola de Javascript." - _themeInstallFailed: - title: "Instalación de tema fallida" - description: "Ha ocurrido un problema al instalar el tema. Por favor, inténtalo de nuevo. Se pueden ver más detalles del error en la consola de Javascript." -_dataSaver: - _media: - title: "Cargando Multimedia" - description: "Desactiva la carga automática de imágenes y vídeos. Tendrás que tocar en las imágenes y vídeos ocultos para cargarlos." - _avatar: - title: "Avatares animados" - description: "Desactiva la animación de los avatares. Las imágenes animadas pueden llegar a ser de mayor tamaño que las normales, por lo que al desactivarlas puedes reducir el consumo de datos." - _code: - title: "Resaltar código" - description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos." -_hemisphere: - N: "Hemisferio norte" - S: "Hemisferio sur" -_reversi: - reversi: "Reversi" - rules: "Reglas" - won: "{name} ha ganado" - total: "Total" - allGames: "Todos los juegos" - ended: "Finalizado" - playing: "Jugando actualmente" - isLlotheo: "El que tenga menos fichas gana (LLoTheO)" - loopedMap: "Mapa en bucle" - canPutEverywhere: "Las fichas se pueden poner a cualquier lugar\n" - timeLimitForEachTurn: "Tiempo límite por jugada." - freeMatch: "Partida libre." - lookingForPlayer: "Buscando oponente" - gameCanceled: "La partida ha sido cancelada." - shareToTlTheGameWhenStart: "Compartir la partida en la línea de tiempo cuando comience " - iStartedAGame: "¡La partida ha comenzado!" - opponentHasSettingsChanged: "El oponente ha cambiado su configuración" - allowIrregularRules: "Reglas irregulares (completamente libre)" - disallowIrregularRules: "Sin reglas irregulares " - showBoardLabels: "Mostrar el número de línea y de columna en el tablero de juego." - useAvatarAsStone: "Usar los avatares de los usuarios como fichas\n" -_offlineScreen: - title: "Fuera de línea. No se puede conectar con el servidor" - header: "Incapaz de conectar con el servidor" -_urlPreviewSetting: - title: "Configuración para la previsualización de la URL" - enable: "Activar la vista previa de URL" - allowRedirect: "Permitir la redirección de la visualización previa" - allowRedirectDescription: "Si una URL tiene una redirección establecida, puede activar esta función para seguir la redirección y mostrar una vista previa del contenido redirigido. Si se desactiva, se ahorrarán recursos del servidor, pero no se mostrará el contenido redirigido." - timeout: "Timeout de la carga de vista previa de las URLs (ms)" - timeoutDescription: "Si se tarda más de este valor en obtener la vista previa, ésta no se generará." - maximumContentLength: "Content-Length Máximo (bytes)" - maximumContentLengthDescription: "Si Content-Length es superior a este valor, no se generará la vista previa." - requireContentLength: "Genere la vista previa sólo si puede obtener Content-Length" - requireContentLengthDescription: "Si el otro servidor no devuelve Content-Length, no se generará la vista previa." - userAgent: "User-Agent" - userAgentDescription: "Establece el User-Agent que se utilizará al recuperar vistas previas. Si se deja en blanco, se utilizará el User-Agent por defecto." - summaryProxy: "Proxy endpoints para generar vistas previas" - summaryProxyDescription: "La vista previa se genera usando Summaly proxy, no la genera el mismo Misskey." - summaryProxyDescription2: "Los siguientes parámetros se vinculan al proxy como cadena de consulta (query string). Si el proxy no los admite, los valores se ignoran." -_mediaControls: - pip: "Picture in Picture" - playbackRate: "Velocidad de reproducción" - loop: "Reproducción en bucle" -_contextMenu: - title: "Menú contextual" - app: "Aplicación" - appWithShift: "Aplicación con la tecla shift" - native: "Interfaz nativa (del navegador web)" -_gridComponent: - _error: - requiredValue: "Este valor es obligatorio" - columnTypeNotSupport: "La validación con expresión regular sólo se admite para columnas de tipo:texto." - patternNotMatch: "Este valor no coincide con el patrón en {pattern}" - notUnique: "Este valor debe ser único" -_roleSelectDialog: - notSelected: "No seleccionado" -_customEmojisManager: - _gridCommon: - copySelectionRows: "Copiar filas seleccionadas" - copySelectionRanges: "Copiar selección" - deleteSelectionRows: "Borrar las líneas seleccionadas" - deleteSelectionRanges: "Borrar las filas de la selección" - searchSettings: "Ajustes de búsqueda" - searchSettingCaption: "Establecer criterios de búsqueda detallados." - searchLimit: "Límite de resultados" - sortOrder: "Ordenar" - registrationLogs: "Log de registros " - registrationLogsCaption: "Los registros se mostrarán al actualizar o borrar Emojis. Desaparecerán después de actualizarlos o eliminarlos, pasar a una nueva página o recargar." -_followRequest: - recieved: "Petición de seguimiento recibida" - sent: "Petición de seguimiento enviada" -_remoteLookupErrors: - _federationNotAllowed: - description: "Es posible que se haya desactivado la comunicación con este servidor o que haya sido bloqueado.\nPonte en contacto con el administrador del servidor.." - _uriInvalid: - title: "La URI es inválida" - description: "Ha habido un problema con la dirección introducida. Comprueba que no hayas escrito caracteres que no pueden ser usados en la URI" - _requestFailed: - title: "Solicitud fallida." - description: "Ha fallado la comunicación con este servidor. Es posible que el servidor no funcione. Asegúrese también de que no ha introducido un URI no válido o inexistente." - _responseInvalid: - title: "La respuesta no es válida" - description: "Has podido comunicarte con este servidor, pero los datos obtenidos eran incorrectos. Si estás consultando contenidos remotos a través de un servidor de terceros, vuelve a realizar la consulta utilizando un URI que pueda obtenerse del servidor de origen." - _noSuchObject: - title: "No se encuentra" - description: "No se ha encontrado el recurso solicitado, por favor, vuelve a comprobar el URI." -_captcha: - verify: "Por favor verifica el CAPTCHA" - testSiteKeyMessage: "Puedes comprobar la vista previa introduciendo los valores de prueba para el sitio y las claves secretas.\nPara más detalles, consulta la página siguiente.\n" - _error: - _requestFailed: - title: "Ha fallado la solicitud del CAPTCHA" - text: "Por favor, ejecútalo después de un rato o comprueba los ajustes de nuevo." - _verificationFailed: - title: "Ha fallado la validación del CAPTCHA" - text: "Comprueba que los ajustes son los correctos." - _unknown: - title: "Error en el CAPTCHA." - text: "Se ha producido un error inesperado." -_bootErrors: - title: "Fallo al cargar" -_search: - searchScopeAll: "Todo" - searchScopeLocal: "Local" - searchScopeUser: "Especificar usuario" -_uploader: - allowedTypes: "Tipos de archivos que se pueden cargar." diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 34afb28723..5d7a773f8c 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -2,15 +2,12 @@ _lang_: "Français" headlineMisskey: "Réseau relié par des notes" introMisskey: "Bienvenue ! Misskey est un service de microblogage décentralisé, libre et ouvert.\nÉcrivez des « notes » et partagez ce qui se passe à l’instant présent, autour de vous avec les autres 📡\nLa fonction « réactions », vous permet également d’ajouter une réaction rapide aux notes des autres utilisateur·rice·s 👍\nExplorons un nouveau monde 🚀" -poweredByMisskeyDescription: "{name} est l'un des services propulsés par la plateforme ouverte Misskey (appelée \"instance Misskey\")." +poweredByMisskeyDescription: "{nom} est l'un des services propulsés par la plateforme ouverte Misskey (appelée \"instance Misskey\")." monthAndDay: "{day}/{month}" search: "Rechercher" notifications: "Notifications" username: "Nom d’utilisateur·rice" password: "Mot de passe" -initialPasswordForSetup: "Mot de passe initial pour la configuration" -initialPasswordIsIncorrect: "Mot de passe initial pour la configuration est incorrecte" -initialPasswordForSetupDescription: "Utilisez le mot de passe que vous avez entré pour le fichier de configuration si vous avez installé Misskey vous-même.\nSi vous utilisez un service d'hébergement Misskey, utilisez le mot de passe fourni.\nSi vous n'avez pas défini de mot de passe, laissez le champ vide pour continuer." forgotPassword: "Mot de passe oublié" fetchingAsApObject: "Récupération depuis le fédiverse …" ok: "OK" @@ -48,25 +45,17 @@ pin: "Épingler sur le profil" unpin: "Désépingler" copyContent: "Copier le contenu" copyLink: "Copier le lien" -copyLinkRenote: "Copier le lien de la renote" delete: "Supprimer" deleteAndEdit: "Supprimer et réécrire" -deleteAndEditConfirm: "Êtes-vous sûr de vouloir effacer cette note et la modifier ? Vous perdrez toutes les réactions, renotes et réponses." +deleteAndEditConfirm: "Êtes-vous sûr·e de vouloir supprimer cette note et la reformuler ? Vous perdrez toutes les réactions, renotes et réponses y afférentes." addToList: "Ajouter à une liste" -addToAntenna: "Ajouter à l’antenne" sendMessage: "Envoyer un message" copyRSS: "Copier le RSS" copyUsername: "Copier le nom d’utilisateur·rice" -copyUserId: "Copier l'identifiant de l'utilisateur" -copyNoteId: "Copier l'identifiant de la note" -copyFileId: "Copier l'identifiant du fichier" -copyFolderId: "Copier l'identifiant du dossier" -copyProfileUrl: "Copier l'URL du profil" searchUser: "Chercher un·e utilisateur·rice" -searchThisUsersNotes: "Cherchez les notes de cet·te utilisateur·rice" reply: "Répondre" loadMore: "Afficher plus …" -showMore: "Voir plus" +showMore: "Afficher plus …" showLess: "Fermer" youGotNewFollower: "Vous suit" receiveFollowRequest: "Demande d’abonnement reçue" @@ -79,13 +68,13 @@ import: "Importer" export: "Exporter" files: "Fichiers" download: "Télécharger" -driveFileDeleteConfirm: "Êtes-vous sûr·e de vouloir supprimer le fichier « {name} » ? Les notes avec ce fichier joint seront aussi supprimées." +driveFileDeleteConfirm: "Êtes-vous sûr·e de vouloir supprimer le fichier \"{name}\" ? Les notes liées à ce fichier seront aussi supprimées." unfollowConfirm: "Désirez-vous vous désabonner de {name} ?" -exportRequested: "Vous avez demandé une exportation. L’opération pourrait prendre un peu de temps. Une fois terminée, le fichier sera ajouté au Drive." +exportRequested: "Vous avez demandé une exportation. L’opération pourrait prendre un peu de temps. Une terminée, le fichier résultant sera ajouté au Drive." importRequested: "Vous avez initié un import. Cela pourrait prendre un peu de temps." lists: "Listes" noLists: "Vous n’avez aucune liste" -note: "Note" +note: "Notes" notes: "Notes" following: "Abonnements" followers: "Abonné·e·s" @@ -112,7 +101,6 @@ enterEmoji: "Insérer un émoji" renote: "Renoter" unrenote: "Annuler la Renote" renoted: "Renoté !" -renotedToX: "Renoté en {name}" cantRenote: "Ce message ne peut pas être renoté." cantReRenote: "Impossible de renoter une Renote." quote: "Citer" @@ -126,23 +114,15 @@ sensitive: "Contenu sensible" add: "Ajouter" reaction: "Réactions" reactions: "Réactions" -emojiPicker: "Sélecteur d’émojis" -pinnedEmojisForReactionSettingDescription: "Vous pouvez définir les émojis épinglés lors de la réaction" -pinnedEmojisSettingDescription: "Vous pouvez définir les émojis épinglés lors de la saisie de l'émoji" -emojiPickerDisplay: "Affichage du sélecteur d'émojis" -overwriteFromPinnedEmojisForReaction: "Remplacer par les émojis épinglés pour la réaction" -overwriteFromPinnedEmojis: "Remplacer par les émojis épinglés globalement" +reactionSetting: "Réactions à afficher dans le sélecteur de réactions" reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser « + » pour ajouter." -rememberNoteVisibility: "Se souvenir de la visibilité des notes" -attachCancel: "Supprimer le fichier joint" -deleteFile: "Fichier supprimé" +rememberNoteVisibility: "Activer l'option \" se souvenir de la visibilité des notes \" vous permet de réutiliser automatiquement la visibilité utilisée lors de la publication de votre note précédente." +attachCancel: "Supprimer le fichier attaché" markAsSensitive: "Marquer comme sensible" unmarkAsSensitive: "Supprimer le marquage comme sensible" enterFileName: "Entrer le nom du fichier" mute: "Masquer" unmute: "Ne plus masquer" -renoteMute: "Masquer les renotes" -renoteUnmute: "Ne plus masquer les renotes" block: "Bloquer" unblock: "Débloquer" suspend: "Suspendre" @@ -152,11 +132,8 @@ unblockConfirm: "Êtes-vous sûr·e de vouloir débloquer ce compte ?" suspendConfirm: "Êtes-vous sûr·e de vouloir suspendre ce compte ?" unsuspendConfirm: "Êtes-vous sûr·e de vouloir annuler la suspension de ce compte ?" selectList: "Sélectionner une liste" -editList: "Modifier la liste" selectChannel: "Sélectionner un canal" selectAntenna: "Sélectionner une antenne" -editAntenna: "Modifier l'antenne" -createAntenna: "Créer une antenne" selectWidget: "Sélectionner un widget" editWidgets: "Modifier les widgets" editWidgetsExit: "Valider les modifications" @@ -169,21 +146,16 @@ addEmoji: "Ajouter un émoji" settingGuide: "Configuration proposée" cacheRemoteFiles: "Mise en cache des fichiers distants" cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants sont chargés directement depuis l’instance distante. La désactiver diminuera certes l’utilisation de l’espace de stockage local mais augmentera le trafic réseau puisque les miniatures ne seront plus générées." -youCanCleanRemoteFilesCache: "Vous pouvez supprimer tous les caches en cliquant le bouton 🗑️ dans la gestion des fichiers." -cacheRemoteSensitiveFiles: "Mettre en cache les fichiers distants sensibles" -cacheRemoteSensitiveFilesDescription: "Si vous désactivez ce paramètre, les fichiers sensibles distants ne seront pas mis en cache et un lien direct sera utilisé à la place" flagAsBot: "Ce compte est un robot" flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de Misskey pour traiter ce compte comme un robot." flagAsCat: "Ce compte est un chat" -flagAsCatDescription: "Miaou miaou miaou ?" +flagAsCatDescription: "Activer l'option \" Je suis un chat \" pour ce compte." flagShowTimelineReplies: "Afficher les réponses dans le fil" flagShowTimelineRepliesDescription: "Affiche les réponses des utilisateurs aux notes des autres utilisateurs dans la timeline si cette option est activée." autoAcceptFollowed: "Accepter automatiquement les demandes d’abonnement venant d’utilisateur·rice·s que vous suivez" addAccount: "Ajouter un compte" -reloadAccountsList: "Rafraichir la liste des comptes" loginFailed: "Échec de la connexion" showOnRemote: "Voir sur l’instance distante" -continueOnRemote: "Continuer sur l'instance distante" general: "Général" wallpaper: "Fond d’écran" setWallpaper: "Définir le fond d’écran" @@ -194,12 +166,11 @@ followConfirm: "Êtes-vous sûr·e de vouloir suivre {name} ?" proxyAccount: "Compte proxy" proxyAccountDescription: "Un compte proxy se comporte, dans certaines conditions, comme un·e abonné·e distant·e pour les utilisateurs d'autres instances. Par exemple, quand un·e utilisateur·rice ajoute un·e utilisateur·rice distant·e à une liste, ses notes ne seront pas visibles sur l'instance si personne ne suit cet·te utilisateur·rice. Le compte proxy va donc suivre cet·te utilisateur·rice pour que ses notes soient acheminées." host: "Serveur distant" -selectSelf: "Sélectionner manuellement" selectUser: "Sélectionner un·e utilisateur·rice" recipient: "Destinataire" annotation: "Commentaires" federation: "Fédération" -instances: "Instances" +instances: "Instance" registeredAt: "Premier contact le" latestRequestReceivedAt: "Dernière requête reçue" latestStatus: "Dernier statut" @@ -209,7 +180,6 @@ perHour: "par heure" perDay: "par jour" stopActivityDelivery: "Arrêter l’envoi de l’activité" blockThisInstance: "Bloquer cette instance" -silenceThisInstance: "Mettre cette instance en sourdine" operations: "Opérations" software: "Logiciel" version: "Version" @@ -229,8 +199,6 @@ clearCachedFiles: "Vider le cache" clearCachedFilesConfirm: "Êtes-vous sûr·e de vouloir vider tout le cache de fichiers distants ?" blockedInstances: "Instances bloquées" blockedInstancesDescription: "Listez les instances que vous désirez bloquer, une par ligne. Ces instances ne seront plus en capacité d'interagir avec votre instance." -silencedInstances: "Instances mises en sourdine" -silencedInstancesDescription: "Énumérer les noms d'hôte des instances à mettre en sourdine. Tous les comptes des instances énumérées seront traités comme mis en sourdine, ne peuvent faire que des demandes de suivi et ne peuvent pas mentionner les comptes locaux s'ils ne sont pas suivis. Cela n'affectera pas les instances bloquées." muteAndBlock: "Masqué·e·s / Bloqué·e·s" mutedUsers: "Utilisateur·rice·s en sourdine" blockedUsers: "Utilisateur·rice·s bloqué·e·s" @@ -238,6 +206,7 @@ noUsers: "Il n’y a pas d’utilisateur·rice·s" editProfile: "Modifier votre profil" noteDeleteConfirm: "Êtes-vous sûr·e de vouloir supprimer cette note ?" pinLimitExceeded: "Vous ne pouvez plus épingler d’autres notes." +intro: "L’installation de Misskey est terminée ! Veuillez créer un compte administrateur." done: "Terminé" processing: "Traitement en cours" preview: "Aperçu" @@ -271,15 +240,15 @@ announcements: "Annonces" imageUrl: "URL de l’image" remove: "Supprimer" removed: "Supprimé" -removeAreYouSure: "Êtes-vous sûr·e de vouloir supprimer « {x} » ?" -deleteAreYouSure: "Êtes-vous sûr·e de vouloir supprimer « {x} » ?" +removeAreYouSure: "Êtes-vous sûr·e de vouloir supprimer「{x}」?" +deleteAreYouSure: "Êtes-vous sûr·e de vouloir supprimer「{x}」?" resetAreYouSure: "Voulez-vous réinitialiser ?" -areYouSure: "Êtes-vous sûr·e ?" saved: "Enregistré" +messaging: "Discuter" upload: "Téléverser" keepOriginalUploading: "Garder l’image d’origine" keepOriginalUploadingDescription: "Conserve la version originale lors du téléchargement d'images. S'il est désactivé, le navigateur génère l'image pour la publication web lors du téléchargement." -fromDrive: "Depuis le Disque" +fromDrive: "Depuis le Drive" fromUrl: "Depuis une URL" uploadFromUrl: "Téléverser via une URL" uploadFromUrlDescription: "URL du fichier que vous souhaitez téléverser" @@ -288,12 +257,9 @@ uploadFromUrlMayTakeTime: "Le téléversement de votre fichier peut prendre un c explore: "Découvrir" messageRead: "Lu" noMoreHistory: "Il n’y a plus d’historique" +startMessaging: "Commencer à discuter" nUsersRead: "Lu par {n} personnes" agreeTo: "J’accepte {0}" -agree: "Accepter" -agreeBelow: "J’accepte ce qui suit" -basicNotesBeforeCreateAccount: "Notes importantes" -termsOfService: "Conditions d'utilisation" start: "Commencer" home: "Principal" remoteUserCaution: "Les informations de ce compte risqueraient d’être incomplètes du fait que l’utilisateur·rice provient d’une instance distante." @@ -312,7 +278,7 @@ dark: "Sombre" lightThemes: "Thèmes clairs" darkThemes: "Thèmes sombres" syncDeviceDarkMode: "Utiliser le mode sombre de votre appareil" -drive: "Disque" +drive: "Drive" fileName: "Nom du fichier" selectFile: "Choisir le fichier" selectFiles: "Choisir les fichiers" @@ -323,10 +289,8 @@ folderName: "Nom du dossier" createFolder: "Créer un dossier" renameFolder: "Renommer le dossier" deleteFolder: "Supprimer le dossier" -folder: "Dossier" addFile: "Ajouter un fichier" -showFile: "Voir les fichiers" -emptyDrive: "Le Disque est vide" +emptyDrive: "Le Drive est vide" emptyFolder: "Le dossier est vide" unableToDelete: "Suppression impossible" inputNewFileName: "Entrez un nouveau nom de fichier" @@ -338,7 +302,6 @@ copyUrl: "Copier l’URL" rename: "Renommer" avatar: "Avatar" banner: "Bannière" -displayOfSensitiveMedia: "Afficher les médias sensibles" whenServerDisconnected: "Lorsque la connexion au serveur est perdue" disconnectedFromServer: "Déconnecté·e du serveur" reload: "Rafraîchir" @@ -368,10 +331,12 @@ enableLocalTimeline: "Activer le fil local" enableGlobalTimeline: "Activer le fil global" disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur·rice·s et les modérateur·rice·s pourront toujours y accéder." registration: "S’inscrire" +enableRegistration: "Autoriser les nouvelles inscriptions" invite: "Inviter" -driveCapacityPerLocalAccount: "Capacité de stockage du Disque par utilisateur local" -driveCapacityPerRemoteAccount: "Capacité de stockage du Disque par utilisateur distant" +driveCapacityPerLocalAccount: "Volume du Drive par utilisateur local" +driveCapacityPerRemoteAccount: "Volume du Drive par utilisateur distant" inMb: "en mégaoctets" +iconUrl: "URL de l'icône" bannerUrl: "URL de l’image de la bannière" backgroundImageUrl: "URL de l'image d'arrière-plan" basicInfo: "Informations basiques" @@ -385,11 +350,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Activer hCaptcha" hcaptchaSiteKey: "Clé du site" hcaptchaSecretKey: "Clé secrète" -mcaptcha: "mCaptcha" -enableMcaptcha: "Activer mCaptcha" -mcaptchaSiteKey: "Clé du site" -mcaptchaSecretKey: "Clé secrète" -mcaptchaInstanceUrl: "URL de l'instance de mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Activer reCAPTCHA" recaptchaSiteKey: "Clé du site" @@ -405,10 +365,9 @@ name: "Nom" antennaSource: "Source de l’antenne" antennaKeywords: "Mots clés à recevoir" antennaExcludeKeywords: "Mots clés à exclure" -antennaExcludeBots: "Exclure les comptes robot" antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR." -notifyAntenna: "Me notifier pour les nouvelles notes" -withFileAntenna: "Notes ayant des fichiers joints uniquement" +notifyAntenna: "Je souhaite recevoir les notifications des nouvelles notes" +withFileAntenna: "Notes ayant des attachements uniquement" enableServiceworker: "Activer ServiceWorker" antennaUsersDescription: "Saisissez un seul nom d’utilisateur·rice par ligne" caseSensitive: "Sensible à la casse" @@ -432,24 +391,13 @@ about: "Informations" aboutMisskey: "À propos de Misskey" administrator: "Administrateur" token: "Jeton" -2fa: "Authentification à deux facteurs" -setupOf2fa: "Configuration de l’authentification à deux facteurs" -totp: "Application d'authentification" -totpDescription: "Entrer un mot de passe à usage unique à l'aide d'une application d'authentification" moderator: "Modérateur·rice·s" moderation: "Modérations" -moderationNote: "Note de modération" -moderationNoteDescription: "Vous pouvez remplir des notes qui seront partagés seulement entre modérateurs." -addModerationNote: "Ajouter une note de modération" -moderationLogs: "Journal de modération" nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s" -securityKeyAndPasskey: "Sécurité et clés de sécurité" securityKey: "Clé de sécurité" lastUsed: "Dernier utilisé" -lastUsedAt: "Dernière utilisation : {t}" unregister: "Se désinscrire" passwordLessLogin: "Se connecter sans mot de passe" -passwordLessLoginDescription: "Se connecter uniquement avec une clé de sécurité ou une clé d'accès sans utiliser de mot de passe" resetPassword: "Réinitialiser le mot de passe" newPasswordIs: "Votre nouveau mot de passe est \"{password}\"" reduceUiAnimation: "Réduire les animations dans l’interface" @@ -457,6 +405,7 @@ share: "Partager" notFound: "Non trouvé" notFoundDescription: "Aucune page ne correspond à l’URL spécifiée." uploadFolder: "Emplacement de téléversement par défaut" +cacheClear: "Vider le cache" markAsReadAllNotifications: "Marquer toutes les notifications comme lues" markAsReadAllUnreadNotes: "Marquer toutes les notes comme lues" markAsReadAllTalkMessages: "Marquer toutes les discussions comme lues" @@ -474,6 +423,8 @@ retype: "Confirmation" noteOf: "Notes de {user}" quoteAttached: "Avec citation" quoteQuestion: "Souhaitez-vous ajouter une citation ?" +noMessagesYet: "Pas encore de discussion" +newMessageExists: "Vous avez un nouveau message" onlyOneFileCanBeAttached: "Vous ne pouvez joindre qu’un seul fichier au message" signinRequired: "Veuillez vous connecter" invitations: "Invitations" @@ -497,16 +448,10 @@ uiLanguage: "Langue d’affichage de l’interface" aboutX: "À propos de {x}" emojiStyle: "Style des émojis" native: "Natif" -menuStyle: "Style du menu" -style: "Style" -drawer: "Sélecteur" -popup: "Pop-up" -showNoteActionsOnlyHover: "Afficher les actions de note uniquement au survol" -showReactionsCount: "Afficher le nombre de réactions des notes" +disableDrawer: "Les menus ne s'affichent pas dans le tiroir" noHistory: "Pas d'historique" signinHistory: "Historique de connexion" enableAdvancedMfm: "Activer la MFM avancée" -enableAnimatedMfm: "Activer le MFM animé" doing: "En cours..." category: "Catégorie" tags: "Étiquettes" @@ -515,8 +460,6 @@ createAccount: "Créer un compte" existingAccount: "Compte existant" regenerate: "Générer à nouveau" fontSize: "Taille de la police" -mediaListWithOneImageAppearance: "Hauteur des listes de médias n'ayant qu'une image " -limitTo: "Limiter à {x}" noFollowRequests: "Vous n’avez aucune demande d’abonnement en attente" openImageInNewTab: "Ouvrir les images dans un nouvel onglet" dashboard: "Tableau de bord" @@ -535,7 +478,7 @@ hideThisNote: "Masquer cette note" showFeaturedNotesInTimeline: "Afficher les notes des Tendances dans le fil d'actualité" objectStorage: "Stockage d'objets" useObjectStorage: "Utiliser le stockage d'objets" -objectStorageBaseUrl: "URL de base" +objectStorageBaseUrl: "Base URL" objectStorageBaseUrlDesc: "Préfixe d’URL utilisé pour construire l’URL vers le référencement d’objet (média). Spécifiez son URL si vous utilisez un CDN ou un proxy, sinon spécifiez l’adresse accessible au public selon le guide de service que vous allez utiliser. P.ex. 'https://.s3.amazonaws.com' pour AWS S3 et 'https://storage.googleapis.com/' pour GCS." objectStorageBucket: "Bucket" objectStorageBucketDesc: "Veuillez spécifier le nom du compartiment utilisé sur le service configuré." @@ -550,12 +493,9 @@ objectStorageUseSSLDesc: "Désactivez cette option si vous n'utilisez pas HTTPS objectStorageUseProxy: "Se connecter via proxy" objectStorageUseProxyDesc: "Désactivez cette option si vous n'utilisez pas de proxy pour la connexion API" objectStorageSetPublicRead: "Régler sur « public » lors de l'envoi" -s3ForcePathStyleDesc: "Si s3ForcePathStyle est activé, le nom du compartiment doit être spécifié comme une partie du chemin de l'URL plutôt que le nom d'hôte. Il faudra peut-être l'activer lors de l'utilisation d'une instance de Minio autohébergée, etc." serverLogs: "Journal du serveur" deleteAll: "Supprimer tout" showFixedPostForm: "Afficher le formulaire de publication en haut du fil d'actualité" -showFixedPostFormInChannel: "Afficher le formulaire de publication en haut du fil (canaux)" -withRepliesByDefaultForNewlyFollowed: "Afficher les réponses des nouvelles personnes que vous suivez dans le fil par défaut" newNoteRecived: "Voir les nouvelles notes" sounds: "Sons" sound: "Sons" @@ -565,8 +505,6 @@ showInPage: "Afficher dans la page" popout: "Fenêtre contextuelle" volume: "Volume" masterVolume: "Volume principal" -notUseSound: "Ne pas émettre de son" -useSoundOnlyWhenActive: "Émettre des sons uniquement quand Misskey est active" details: "Détails" chooseEmoji: "Choisissez un émoji" unableToProcess: "L’opération n’a pas pu être complétée." @@ -583,31 +521,21 @@ ascendingOrder: "Ascendant" descendingOrder: "Descendant" scratchpad: "ScratchPad" scratchpadDescription: "ScratchPad fournit un environnement expérimental pour AiScript. Vous pouvez vérifier la rédaction de votre code, sa bonne exécution et le résultat de son interaction avec Misskey." -uiInspector: "Inspecteur UI" output: "Sortie" script: "Script" disablePagesScript: "Désactiver AiScript sur les Pages" updateRemoteUser: "Mettre à jour les informations de l’utilisateur·rice distant·e" -unsetUserAvatar: "Supprimer l’avatar" -unsetUserAvatarConfirm: "Êtes-vous sûr·e de vouloir supprimer l'avatar ?" -unsetUserBanner: "Supprimer la bannière" -unsetUserBannerConfirm: "Êtes-vous sûr·e de vouloir supprimer la bannière ?" deleteAllFiles: "Supprimer tous les fichiers" deleteAllFilesConfirm: "Êtes-vous sûr·e de vouloir supprimer tous les fichiers ?" -removeAllFollowing: "Se désabonner de tous les utilisateurs auxquels vous êtes abonné·e" +removeAllFollowing: "Retenir tous les abonnements" removeAllFollowingDescription: "Se désabonner de tous les comptes de {host}. Veuillez lancer cette action dans les cas où l’instance n’existe plus, etc." userSuspended: "Cet·te utilisateur·rice a été suspendu·e." userSilenced: "Cette utilisateur·trice a été mis·e en sourdine." yourAccountSuspendedTitle: "Ce compte est suspendu" yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint les conditions d'utilisation de l'instance, ou pour un motif similaire. Si vous souhaitez connaître en détail les raisons de cette suspension, renseignez-vous auprès de l'administrateur·rice de votre instance. Merci de ne pas créer de nouveau compte." -tokenRevoked: "Ce jeton est invalide." -tokenRevokedDescription: "Votre jeton de connexion a expiré. Veuillez vous reconnecter." -accountDeleted: "Compte supprimé" -accountDeletedDescription: "Ce compte a été supprimé." menu: "Menu" divider: "Séparateur" addItem: "Ajouter un élément" -rearrange: "Trier par" relays: "Relais" addRelay: "Ajouter un relais" inboxUrl: "Inbox URL" @@ -627,7 +555,7 @@ description: "Description" describeFile: "Ajouter une description d'image" enterFileDescription: "Saisissez une description" author: "Auteur·rice" -leaveConfirm: "Vous avez des modifications non sauvegardées. Voulez-vous les ignorer ?" +leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer ?" manage: "Gestion" plugins: "Extensions" preferencesBackups: "Sauvegarder les paramètres" @@ -642,18 +570,17 @@ medium: "Moyen" small: "Petit" generateAccessToken: "Générer un jeton d'accès" permission: "Autorisations " -adminPermission: "Droits de l'administrateur" enableAll: "Tout activer" disableAll: "Tout désactiver" tokenRequested: "Autoriser l'accès au compte" -pluginTokenRequestedDescription: "Cette extension pourra utiliser les autorisations définies ici." +pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici." notificationType: "Type de notifications" edit: "Editer" -emailServer: "Serveur de messagerie" +emailServer: "Serveur mail" enableEmail: "Activer la distribution de courriel" -emailConfigInfo: "Utilisé pour confirmer votre adresse e-mail et réinitialiser votre mot de passe en cas d’oubli" +emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas d’oubli." email: "E-mail " -emailAddress: "Adresse e-mail" +emailAddress: "Adresses e-mail" smtpConfig: "Paramètres du serveur SMTP" smtpHost: "Serveur distant" smtpPort: "Port" @@ -664,9 +591,8 @@ smtpSecure: "Utiliser SSL/TLS implicitement dans les connexions SMTP" smtpSecureInfo: "Désactiver cette option lorsque STARTTLS est utilisé" testEmail: "Tester la distribution de courriel" wordMute: "Filtre de mots" -hardWordMute: "Filtre de mots dur" regexpError: "Erreur d’expression régulière" -regexpErrorDescription: "Une erreur s'est produite dans l'expression régulière sur la ligne {line} de votre mot muet {tab} :" +regexpErrorDescription: "Une erreur s'est produite dans l'expression régulière sur la ligne {ligne} de votre mot muet {tab} :" instanceMute: "Instance en sourdine" userSaysSomething: "{name} a dit quelque chose" makeActive: "Activer" @@ -686,21 +612,22 @@ useGlobalSettingDesc: "S'il est activé, les paramètres de notification de votr other: "Autre" regenerateLoginToken: "Régénérer le jeton de connexion" regenerateLoginTokenDescription: "Générer un nouveau jeton d'authentification. Cette opération ne devrait pas être nécessaire ; lors de la génération d'un nouveau jeton, tous les appareils seront déconnectés. " -theKeywordWhenSearchingForCustomEmoji: "Ce mot-clé est utilisé lors de la recherche des émojis personnalisés." setMultipleBySeparatingWithSpace: "Vous pouvez en définir plusieurs, en les séparant par des espaces." fileIdOrUrl: "ID du fichier ou URL" behavior: "Comportement" sample: "Exemple" abuseReports: "Signalements" reportAbuse: "Signaler" -reportAbuseRenote: "Signaler la renote" reportAbuseOf: "Signaler {name}" fillAbuseReportDescription: "Veuillez expliquer les raisons du signalement. S'il s'agit d'une note précise, veuillez en donner le lien." abuseReported: "Le rapport est envoyé. Merci." reporter: "Signalé par" reporteeOrigin: "Origine du signalement" reporterOrigin: "Signalé par" +forwardReport: "Transférer le signalement à l’instance distante" +forwardReportIsAnonymous: "L'instance distante ne sera pas en mesure de voir vos informations et apparaîtra comme un compte anonyme du système." send: "Envoyer" +abuseMarkAsResolved: "Marquer le signalement comme résolu" openInNewTab: "Ouvrir dans un nouvel onglet" openInSideView: "Ouvrir en vue latérale" defaultNavigationBehaviour: "Navigation par défaut" @@ -712,13 +639,10 @@ system: "Système" switchUi: "Modifier l'interface utilisateur" desktop: "Bureau" clip: "Clip" -createNew: "Créer" +createNew: "Créer nouveau" optional: "Facultatif" createNewClip: "Créer un nouveau clip" -unclip: "Supprimer le clip" -confirmToUnclipAlreadyClippedNote: "Cette note fait déjà partie du clip « {name} ». Souhaitez-vous la supprimer de ce clip ?" public: "Public" -private: "Privé" i18nInfo: "Misskey est traduit dans différentes langues par des bénévoles. Vous pouvez contribuer à {link}." manageAccessTokens: "Gérer les jetons d'accès" accountInfo: " Informations du compte " @@ -727,7 +651,7 @@ repliesCount: "Nombre de réponses envoyées" renotesCount: "Nombre de notes que vous avez renotées" repliedCount: "Nombre de réponses reçues" renotedCount: "Nombre de vos notes renotées" -followingCount: "Nombre d'abonnements" +followingCount: "Nombre de comptes suivis" followersCount: "Nombre d'abonnés" sentReactionsCount: "Nombre de réactions envoyées" receivedReactionsCount: "Nombre de réactions reçues" @@ -735,15 +659,14 @@ pollVotesCount: "Nombre de votes envoyés" pollVotedCount: "Nombre de votes reçus" yes: "Oui" no: "Non" -driveFilesCount: "Nombre de fichiers sur le Disque" -driveUsage: "Utilisation du Disque" +driveFilesCount: "Nombre de fichiers dans le Drive" +driveUsage: "Utilisation du Drive" noCrawle: "Refuser l'indexation par les robots" noCrawleDescription: "Demandez aux moteurs de recherche de ne pas indexer votre page de profil, vos notes, vos pages, etc." lockedAccountInfo: "À moins que vous ne définissiez la visibilité de votre note sur \"Abonné-e-s\", vos notes sont visibles par tous, même si vous exigez que les demandes d'abonnement soient approuvées manuellement." alwaysMarkSensitive: "Marquer les médias comme contenu sensible par défaut" loadRawImages: "Affichage complet des images jointes au lieu des vignettes" disableShowingAnimatedImages: "Désactiver l'animation des images" -highlightSensitiveMedia: "Mettre en évidence les médias sensibles" verificationEmailSent: "Un e-mail de vérification a été envoyé. Veuillez accéder au lien pour compléter la vérification." notSet: "Non défini" emailVerified: "Votre adresse e-mail a été vérifiée." @@ -754,11 +677,10 @@ contact: "Contact" useSystemFont: "Utiliser la police par défaut du système" clips: "Clips" experimentalFeatures: "Fonctionnalités expérimentales" -experimental: "Expérimental" -thisIsExperimentalFeature: "Ceci est une fonctionnalité expérimentale. Il y a une possibilité que les spécifications changent ou qu'elle ne fonctionne pas correctement." developer: "Développeur" makeExplorable: "Rendre le compte visible sur la page \"Découvrir\"." makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra pas sur la page \"Découvrir\"." +showGapBetweenNotesInTimeline: "Afficher un écart entre les notes sur la Timeline" duplicate: "Duliquer" left: "Gauche" center: "Centrer" @@ -798,7 +720,7 @@ inUse: "utilisé" editCode: "Modifier le code" apply: "Appliquer" receiveAnnouncementFromInstance: "Recevoir les messages d'information de l'instance" -emailNotification: "Notifications par courriel" +emailNotification: "Notifications par mail" publish: "Public" inChannelSearch: "Chercher dans le canal" useReactionPickerForContextMenu: "Clic-droit pour ouvrir le panneau de réactions" @@ -815,7 +737,7 @@ addDescription: "Ajouter une description" userPagePinTip: "Vous pouvez afficher des notes ici en sélectionnant l'option « Épingler au profil » dans le menu de chaque note." notSpecifiedMentionWarning: "Vous avez mentionné des utilisateur·rice·s qui ne font pas partie de la liste des destinataires" info: "Informations" -userInfo: "Informations sur l'utilisateur·rice" +userInfo: "Informations sur l'utilisateur" unknown: "Inconnu" onlineStatus: "Statut" hideOnlineStatus: "Se rendre invisible" @@ -836,18 +758,15 @@ administration: "Gestion" accounts: "Comptes" switch: "Remplacer" noMaintainerInformationWarning: "Informations administrateur non configurées." -noInquiryUrlWarning: "L'URL demandé n'est pas définie" noBotProtectionWarning: "La protection contre les bots n'est pas configurée." configure: "Configurer" postToGallery: "Publier dans la galerie" -postToHashtag: "Publier avec ce hashtag" gallery: "Galerie" recentPosts: "Les plus récentes" popularPosts: "Les plus consultées" shareWithNote: "Partager dans une note" ads: "Publicité" expiration: "Échéance" -startingperiod: "Commencer" memo: "Pense-bête" priority: "Priorité" high: "Haute" @@ -874,34 +793,29 @@ translatedFrom: "Traduit depuis {x}" accountDeletionInProgress: "La suppression de votre compte est en cours" usernameInfo: "C'est un nom qui identifie votre compte sur l'instance de manière unique. Vous pouvez utiliser des lettres de l'alphabet (minuscules et majuscules), des chiffres (de 0 à 9), ou bien le tiret « _ ». Vous ne pourrez pas modifier votre nom d'utilisateur·rice par la suite." aiChanMode: "Mode Ai" -devMode: "Mode développement" keepCw: "Garder le CW" pubSub: "Comptes Pub/Sub" lastCommunication: "Dernière communication" resolved: "Résolu" unresolved: "En attente" -breakFollow: "Supprimer l'abonné·e" -breakFollowConfirm: "Êtes-vous sûr de vouloir vous désabonner ?" +breakFollow: "Ne plus suivre" itsOn: "Activé" itsOff: "Désactivé" -on: "Activé" -off: "Désactivé" emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte" unread: "Non lu" filter: "Filtre" -controlPanel: "Panneau de configuration" +controlPanel: "Panneau de contrôle" manageAccounts: "Gérer les comptes" makeReactionsPublic: "Rendre les réactions publiques" makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données publique." classic: "Classique" muteThread: "Masquer cette discussion" unmuteThread: "Ne plus masquer le fil" -followingVisibility: "Visibilité des abonnements" -followersVisibility: "Visibilité des abonnés" +ffVisibility: "Visibilité des abonnés/abonnements" +ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu suis et les personnes qui te suivent." continueThread: "Afficher la suite du fil" deleteAccountConfirm: "Votre compte sera supprimé. Êtes vous certain ?" incorrectPassword: "Le mot de passe est incorrect." -incorrectTotp: "Le mot de passe à usage unique est incorrect ou a expiré." voteConfirm: "Confirmez-vous votre vote pour « {choice} » ?" hide: "Masquer" useDrawerReactionPickerForMobile: "Afficher le sélecteur de réactions en tant que panneau sur mobile" @@ -926,9 +840,6 @@ oneHour: "1 heure" oneDay: "1 jour" oneWeek: "1 semaine" oneMonth: "Un mois" -threeMonths: "3 mois" -oneYear: "1 an" -threeDays: "3 jours" reflectMayTakeTime: "Cela peut prendre un certain temps avant que cela ne se termine." failedToFetchAccountInformation: "Impossible de récupérer les informations du compte." rateLimitExceeded: "Limite de taux dépassée" @@ -936,14 +847,14 @@ cropImage: "Recadrer l'image" cropImageAsk: "Voulez-vous recadrer cette image ?" cropYes: "Rogner" cropNo: "Utiliser en l'état" -file: "Fichier" +file: "Fichiers" recentNHours: "Dernières {n} heures" recentNDays: "Derniers {n} jours" noEmailServerWarning: "Serveur de courrier non configuré." thereIsUnresolvedAbuseReportWarning: "Il n’y a aucun rapport non résolu." recommended: "Recommandé" check: "Vérifier" -driveCapOverrideLabel: "Modifier la capacité de stockage du Disque de cet·te utilisateur·rice" +driveCapOverrideLabel: "Modifier la capacité de stockage du drive de cet·te utilisateur·rice" driveCapOverrideCaption: "Si une valeur inférieure à 0 est spécifiée, elle est annulée." requireAdminForView: "Vous devez être connecté avec un compte administrateur pour les visualiser." isSystemAccount: "Ces comptes sont automatiquement créés et gérés par le système." @@ -970,7 +881,6 @@ remoteOnly: "Distant uniquement" failedToUpload: "Échec du transfert" cannotUploadBecauseInappropriate: "Impossible de télécharger le document car il a été déterminé qu'il pouvait contenir un contenu inapproprié." cannotUploadBecauseNoFreeSpace: "Impossible de télécharger en raison d'un manque d'espace libre sur le disque.\n" -cannotUploadBecauseExceedsFileSizeLimit: "Ce fichier ne peut pas être téléchargé parce qu'il dépasse la taille maximale." beta: "Bêta" enableAutoSensitive: "Détermination automatique de NSFW" enableAutoSensitiveDescription: "S'il est disponible, le drapeau NSFW est automatiquement défini sur le média en utilisant l'apprentissage automatique. Même si cette fonction est désactivée, elle peut être réglée automatiquement dans certains cas." @@ -985,652 +895,104 @@ unsubscribePushNotification: "Désactiver les notifications push" pushNotificationAlreadySubscribed: "Les notifications push sont déjà activées" pushNotificationNotSupported: "Votre navigateur ou votre instance ne prend pas en charge les notifications push" sendPushNotificationReadMessage: "Supprimer les notifications push une fois que les notifications ou messages pertinents ont été lus." -sendPushNotificationReadMessageCaption: "Cela peut augmenter la consommation de batterie de votre appareil." -windowMaximize: "Maximiser" -windowMinimize: "Minimaliser" windowRestore: "Restaurer" caption: "Libellé" loggedInAsBot: "Connecté actuellement en tant que bot" tools: "Outils" cannotLoad: "Chargement impossible" -numberOfProfileView: "Nombre de vues du profil" like: "J'aime" -unlike: "Ne plus aimer" numberOfLikes: "Favoris" show: "Affichage" neverShow: "Ne plus afficher" remindMeLater: "Peut-être plus tard" -didYouLikeMisskey: "Avez-vous aimé Misskey ?" -pleaseDonate: "Misskey est le logiciel libre utilisé par {host}. Merci de faire un don pour que nous puissions continuer à le développer !" -correspondingSourceIsAvailable: "Le code source correspondant est disponible à {anchor}" roles: "Rôles" role: "Rôles" noRole: "Aucun rôle" normalUser: "Simple utilisateur·rice" -undefined: "Non défini" assign: "Attribuer" -unassign: "Retirer" color: "Couleur" manageCustomEmojis: "Gestion des émojis personnalisés" -manageAvatarDecorations: "Gérer les décorations d'avatar" -youCannotCreateAnymore: "Vous avez atteint la limite de création." -cannotPerformTemporary: "Temporairement indisponible" -cannotPerformTemporaryDescription: "Temporairement indisponible puisque le nombre d'opérations dépasse la limite. Veuillez patienter un peu, puis réessayer." -invalidParamError: "Paramètres invalides" -invalidParamErrorDescription: "Les paramètres de la requête sont invalides. Il s'agit généralement d'un bogue, mais cela peut aussi être causé par un excès de caractères ou quelque chose de similaire." -permissionDeniedError: "Opération refusée" -permissionDeniedErrorDescription: "Ce compte n'a pas la permission d'effectuer cette opération." preset: "Préréglage" selectFromPresets: "Sélectionner à partir des préréglages" -achievements: "Accomplissements" -gotInvalidResponseError: "Réponse du serveur invalide" -gotInvalidResponseErrorDescription: "Il se peut que le serveur soit hors ligne ou en maintenance. Veuillez réessayer plus tard." thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes." -thisPostMayBeAnnoyingHome: "Publier vers le fil principal" thisPostMayBeAnnoyingCancel: "Annuler" -thisPostMayBeAnnoyingIgnore: "Publier quand-même" -collapseRenotes: "Réduire les renotes déjà vues" -internalServerError: "Erreur interne du serveur" -internalServerErrorDescription: "Une erreur inattendue s'est produite sur le serveur." -copyErrorInfo: "Copier les détails de l’erreur" -joinThisServer: "S'inscrire à cette instance" -exploreOtherServers: "Trouver une autre instance" -letsLookAtTimeline: "Jetez un coup d'œil au fil" -disableFederationConfirm: "Voulez-vous vraiment désactiver la fédération ?" -disableFederationConfirmWarn: "Même sans fédération, la note ne sera pas privée. Dans la plupart des cas, ce n'est pas nécessaire de désactiver la fédération." -disableFederationOk: "Désactiver" -invitationRequiredToRegister: "Actuellement, cette instance est uniquement sur invitation. Seuls ceux qui ont un code d'invitation peuvent s'inscrire." -emailNotSupported: "Cette instance ne prend pas en charge l'envoi de courriels" -postToTheChannel: "Publier au canal" -cannotBeChangedLater: "Cela ne peut pas être modifié plus tard." -reactionAcceptance: "Acceptation des réactions" -likeOnly: "Les favoris uniquement" -likeOnlyForRemote: "Toutes (mentions j'aime seulement pour les instances distantes)" -nonSensitiveOnly: "Non sensibles seulement" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non sensibles seulement (mentions j'aime seulement pour les instances distantes)" -rolesAssignedToMe: "Rôles attribués à moi" -resetPasswordConfirm: "Souhaitez-vous réinitialiser votre mot de passe ?" -sensitiveWords: "Mots sensibles" -sensitiveWordsDescription: "Définir la visibilité des notes contenant un mot défini ici au fil principal automatiquement. Vous pouvez définir plusieurs valeurs en les séparant par des sauts de ligne." -sensitiveWordsDescription2: "Séparer par une espace pour créer une expression AND ; entourer de barres obliques pour créer une expression régulière." -prohibitedWords: "Mots interdits" -prohibitedWordsDescription: "Publier une note contenant un mot défini ici produira une erreur. Vous pouvez définir plusieurs valeurs en les séparant par des sauts de ligne." -prohibitedWordsDescription2: "Séparer par une espace pour créer une expression AND ; entourer de barres obliques pour créer une expression régulière." -hiddenTags: "Hashtags cachés" -hiddenTagsDescription: "Les hashtags définis ne s'afficheront pas dans les tendances. Vous pouvez définir plusieurs hashtags en faisant un saut de ligne." -notesSearchNotAvailable: "La recherche de notes n'est pas disponible." license: "Licence" -unfavoriteConfirm: "Vraiment supprimer des favoris ?" -myClips: "Mes clips" -drivecleaner: "Nettoyeur du Disque" -retryAllQueuesNow: "Réessayer tous les fils d'attente immédiatement" -retryAllQueuesConfirmTitle: "Vraiment réessayer ?" -retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur." -enableChartsForRemoteUser: "Générer les graphiques pour les utilisateurs distants" -enableChartsForFederatedInstances: "Générer les graphiques pour les instances distantes" -enableStatsForFederatedInstances: "Recevoir les statistiques des instances distantes" -showClipButtonInNoteFooter: "Ajouter « Clip » au menu d'action de la note" -reactionsDisplaySize: "Taille de l'affichage des réactions" -limitWidthOfReaction: "Limiter la largeur maximale des réactions et les afficher en taille réduite" -noteIdOrUrl: "Identifiant de la note ou URL" video: "Vidéo" videos: "Vidéos" -audio: "Audio" -audioFiles: "Fichiers audio" dataSaver: "Économiseur de données" accountMigration: "Migration de compte" accountMoved: "Cet·te utilisateur·rice a migré son compte vers :" -accountMovedShort: "Ce compte a migré" -operationForbidden: "Opération non autorisée" -forceShowAds: "Toujours afficher les publicités" addMemo: "Ajouter un mémo" -editMemo: "Éditer le mémo" -reactionsList: "Réactions" -renotesList: "Liste de renotes" notificationDisplay: "Style des notifications" leftTop: "En haut à gauche" rightTop: "En haut à droite" leftBottom: "En bas à gauche" rightBottom: "En bas à droite" -stackAxis: "Direction d'empilement" vertical: "Vertical" horizontal: "Latéral" -position: "Position" serverRules: "Règles du serveur" -pleaseConfirmBelowBeforeSignup: "Pour vous inscrire sur cette instance, vous devez confirmer et accepter le contenu suivant." -pleaseAgreeAllToContinue: "Pour continuer, veuillez accepter tous les champs ci-dessus." -continue: "Continuer" -preservedUsernames: "Noms d'utilisateur·rice réservés" -preservedUsernamesDescription: "Énumérez les noms d'utilisateur à réserver, séparés par des nouvelles lignes. Les noms d'utilisateur spécifiés ici ne seront plus utilisables lors de la création d'un compte, sauf la création manuelle par un administrateur. De plus, les comptes existants ne seront pas affectés." -createNoteFromTheFile: "Rédiger une note de ce fichier" -archive: "Archive" -archived: "Archivé" -unarchive: "Annuler l'archivage" -channelArchiveConfirmTitle: "Voulez-vous vraiment archiver {name} ?" -channelArchiveConfirmDescription: "Une fois archivé, le canal n'apparaîtra plus dans la liste des canaux ni dans les résultats de recherche, et la publication des nouvelles notes sera impossible." -thisChannelArchived: "Ce canal a été archivé." -displayOfNote: "Affichage de la note" -initialAccountSetting: "Configuration initiale du profil" youFollowing: "Abonné·e" -preventAiLearning: "Refuser l'usage dans l'apprentissage automatique d'IA générative" -preventAiLearningDescription: "Demander aux robots d'indexation de ne pas utiliser le contenu publié, tel que les notes et les images, dans l'apprentissage automatique d'IA générative. Cela est réalisé en incluant le drapeau « noai » dans la réponse HTML. Une prévention complète n'est toutefois pas possible, car il est au robot d'indexation de respecter cette demande." -options: "Options" -specifyUser: "Spécifier l'utilisateur·rice" -openTagPageConfirm: "Ouvrir une page d'hashtags ?" -specifyHost: "Spécifier un serveur distant" -failedToPreviewUrl: "Aperçu d'URL échoué" -update: "Mettre à jour" -rolesThatCanBeUsedThisEmojiAsReaction: "Rôles qui peuvent utiliser cet émoji comme réaction" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Si aucun rôle n'est spécifié, tout le monde peut utiliser cet émoji comme réaction." -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Il faut un rôle public." -cancelReactionConfirm: "Supprimez la réaction ?" -changeReactionConfirm: "Changer la réaction ?" -later: "Plus tard" -goToMisskey: "Retour vers Misskey" -additionalEmojiDictionary: "Dictionnaires d'émojis additionnels" -installed: "Installé" -branding: "Image de marque" -enableServerMachineStats: "Publier les statistiques du matériel du serveur" -enableIdenticonGeneration: "Générer les identicons des utilisateurs" -turnOffToImprovePerformance: "Désactiver peut améliorer la performance." -createInviteCode: "Créer un code d'invitation" -createWithOptions: "Options" -createCount: "Quantité à créer" -inviteCodeCreated: "Code d'invitation créé" -inviteLimitExceeded: "Vous avez atteint la limite de codes d'invitation que vous pouvez générer." -createLimitRemaining: "Codes d'invitation pouvant être créés : {limit} restants" -inviteLimitResetCycle: "Vous pouvez créer jusqu'à {limit} codes d'invitation en {time}." -expirationDate: "Date d’expiration" -noExpirationDate: "Ne pas expirer" -inviteCodeUsedAt: "Code d'invitation utilisé à" -registeredUserUsingInviteCode: "Code d'invitation utilisé par" -waitingForMailAuth: "En attente de la vérification de l'adresse courriel" -inviteCodeCreator: "Créateur·rice de ce code d'invitation" -usedAt: "Utilisé le" -unused: "Non-utilisé" -used: "Utilisé" -expired: "Expiré" -doYouAgree: "Êtes-vous d’accord ?" -beSureToReadThisAsItIsImportant: "Assurez-vous de le lire ; c'est important." -iHaveReadXCarefullyAndAgree: "J'ai lu le contenu de « {x} » et donne mon accord." -dialog: "Dialogue" -icon: "Avatar" -forYou: "Pour vous" -currentAnnouncements: "Annonces actuelles" -pastAnnouncements: "Annonces passées" -youHaveUnreadAnnouncements: "Il y a des annonces non lues." -useSecurityKey: "Suivez les instructions de votre navigateur ou de votre appareil pour utiliser une clé de sécurité ou une clé d'accès." -replies: "Réponses" -renotes: "Renotes" -loadReplies: "Inclure les réponses" -loadConversation: "Afficher la conversation" -pinnedList: "Liste épinglée" -keepScreenOn: "Garder l'écran toujours allumé" -verifiedLink: "Votre propriété de ce lien a été vérifiée" -notifyNotes: "Notifier à propos des nouvelles notes" -unnotifyNotes: "Ne pas notifier pour la publication des notes" -authentication: "Authentification" -authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer" -dateAndTime: "Date et heure" -showRenotes: "Afficher les renotes" -edited: "Modifié" -notificationRecieveConfig: "Paramètres des notifications" -mutualFollow: "Abonnement mutuel" -followingOrFollower: "Abonnement ou abonné" -fileAttachedOnly: "Avec fichiers joints seulement" -showRepliesToOthersInTimeline: "Afficher les réponses aux autres dans le fil" -hideRepliesToOthersInTimeline: "Masquer les réponses aux autres dans le fil" -showRepliesToOthersInTimelineAll: "Afficher les réponses de toutes les personnes que vous suivez dans le fil" -hideRepliesToOthersInTimelineAll: "Masquer les réponses de toutes les personnes que vous suivez dans le fil" -confirmShowRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment afficher les réponses de toutes les personnes que vous suivez dans le fil ?" -confirmHideRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment masquer les réponses de toutes les personnes que vous suivez dans le fil ?" -externalServices: "Services externes" -sourceCode: "Code source" -sourceCodeIsNotYetProvided: "Le code source n'est pas encore disponible. Veuillez signaler ce problème aux administrateurs." -repositoryUrl: "URL du dépôt" -repositoryUrlDescription: "Entrez l'URL du dépôt où se trouve le code source ici. Si vous utilisez Misskey tel quel (sans changer le code source), entrez https://github.com/misskey-dev/misskey" -feedback: "Commentaires" -feedbackUrl: "URL pour les commentaires" -impressum: "Impressum" -impressumUrl: "URL de l'impressum" -impressumDescription: "Dans certains pays comme l'Allemagne, il est obligatoire d'afficher les informations sur l'opérateur d'un site (un impressum)." -privacyPolicy: "Politique de confidentialité" -privacyPolicyUrl: "URL de la politique de confidentialité" -tosAndPrivacyPolicy: "Conditions d'utilisation et politique de confidentialité" -avatarDecorations: "Décorations d'avatar" -attach: "Mettre" -detach: "Enlever" -detachAll: "Tout enlever" -angle: "Angle" -flip: "Inverser" -showAvatarDecorations: "Afficher les décorations d'avatar" -releaseToRefresh: "Relâcher pour rafraîchir" -refreshing: "Rafraîchissement..." -pullDownToRefresh: "Tirer vers le bas pour rafraîchir" -useGroupedNotifications: "Grouper les notifications" -signupPendingError: "Un problème est survenu lors de la vérification de votre adresse e-mail. Le lien a peut-être expiré." -cwNotationRequired: "Si « Masquer le contenu » est activé, une description doit être fournie." -doReaction: "Réagir" -code: "Code" -reloadRequiredToApplySettings: "Le rafraîchissement est nécessaire pour que les paramètres prennent effet." -remainingN: "Restants : {n}" -overwriteContentConfirm: "Voulez-vous remplacer le contenu actuel ?" -seasonalScreenEffect: "Effet d'écran saisonnier" -decorate: "Décorer" -addMfmFunction: "Insérer MFM" -enableQuickAddMfmFunction: "Afficher le sélecteur de MFM avancé" -bubbleGame: "Jeu de bulles" -sfx: "Effets sonores" -soundWillBePlayed: "Le son sera joué" -showReplay: "Voir le replay" -replay: "Rediffusion" -replaying: "En cours de rediffusion" -endReplay: "Arrêter la rediffusion" -copyReplayData: "Copier les données de la rediffusion" -ranking: "Classement" -lastNDays: "Derniers {n} jours" -backToTitle: "Retourner au titre" -hemisphere: "Votre région" -withSensitive: "Afficher les notes contenant des fichiers joints sensibles" -userSaysSomethingSensitive: "Note de {name} contenant des fichiers joints sensibles" -enableHorizontalSwipe: "Glisser pour changer d'onglet" -loading: "Chargement en cours" -surrender: "Annuler" -gameRetry: "Réessayer" -notUsePleaseLeaveBlank: "Laisser vide si non utilisé" -useTotp: "Entrer un mot de passe à usage unique" -useBackupCode: "Utiliser le codes de secours" -launchApp: "Lancer l'app" -useNativeUIForVideoAudioPlayer: "Lire les vidéos et audios en utilisant l'UI du navigateur" -keepOriginalFilename: "Garder le nom original du fichier" -keepOriginalFilenameDescription: "Si vous désactivez ce paramètre, les noms de fichiers seront automatiquement remplacés par des noms aléatoires lorsque vous téléchargerez des fichiers." -noDescription: "Il n'y a pas de description" -alwaysConfirmFollow: "Confirmer lors d'un abonnement" -inquiry: "Contact" -tryAgain: "Veuillez réessayer plus tard" -confirmWhenRevealingSensitiveMedia: "Confirmer pour révéler du contenu sensible" -sensitiveMediaRevealConfirm: "Ceci pourrait être du contenu sensible. Voulez-vous l'afficher ?" -createdLists: "Listes créées" -createdAntennas: "Antennes créées" -fromX: "De {x}" -genEmbedCode: "Générer le code d'intégration" -noteOfThisUser: "Notes de cet·te utilisateur·rice" -clipNoteLimitExceeded: "Aucune note supplémentaire ne peut être ajoutée à ce clip." -performance: "Performance" -modified: "Modifié" -discard: "Annuler" -thereAreNChanges: "Il y a {n} modification(s)" -signinWithPasskey: "Se connecter avec une clé d'accès" -unknownWebAuthnKey: "Clé d'accès inconnue." -passkeyVerificationFailed: "La vérification de la clé d'accès a échoué." -passkeyVerificationSucceededButPasswordlessLoginDisabled: "La vérification de la clé d'accès a réussi, mais la connexion sans mot de passe est désactivée." -messageToFollower: "Message aux abonné·es" -target: "Destinataire" -prohibitedWordsForNameOfUser: "Mots interdits pour les noms d'utilisateur·rices" -lockdown: "Verrouiller" -pleaseSelectAccount: "Sélectionner un compte" -availableRoles: "Rôles disponibles" -postForm: "Formulaire de publication" -information: "Informations" -_chat: - invitations: "Inviter" - noHistory: "Pas d'historique" - members: "Membres" - home: "Principal" - send: "Envoyer" -_abuseUserReport: - forward: "Transférer" - forwardDescription: "Transférer le signalement vers une instance distante en tant qu'anonyme." - resolve: "Résoudre" - accept: "Accepter" - reject: "Rejeter" - resolveTutorial: "Si le signalement est légitime dans son contenu, sélectionnez « Accepter » pour marquer le cas comme résolu par l'affirmative.\nSi le contenu du rapport n'est pas légitime, sélectionnez « Rejeter » pour marquer le cas comme résolu par la négative." -_delivery: - status: "Statut de la diffusion" - stop: "Suspendu·e" - resume: "Reprendre" - _type: - none: "Publié" - manuallySuspended: "Suspendre manuellement" - goneSuspended: "L'instance est suspendue en raison de la suppression de ce dernier" - autoSuspendedForNotResponding: "L'instance est suspendue car elle ne répond pas" -_bubbleGame: - howToPlay: "Comment jouer" - hold: "Réserver" - _score: - score: "Score" - scoreYen: "Montant gagné" - highScore: "Meilleur score" - maxChain: "Nombre maximum de chaînes" - yen: "{yen} yens" - estimatedQty: "{qty} pièces" - scoreSweets: "{onigiriQtyWithUnit} Onigiri(s)" -_announcement: - forExistingUsers: "Pour les utilisateurs existants seulement" - needConfirmationToRead: "Exiger la confirmation de la lecture" - needConfirmationToReadDescription: "Si activé, afficher un dialogue de confirmation quand l'annonce est marquée comme lue. Aussi, elle sera exclue de « marquer tout comme lu » ." - end: "Archiver l'annonce" - tooManyActiveAnnouncementDescription: "Un grand nombre d'annonces actives peut baisser l'expérience utilisateur. Considérez d'archiver les annonces obsolètes." - readConfirmTitle: "Marquer comme lu ?" - readConfirmText: "Cela marquera le contenu de « {title} » comme lu." - shouldNotBeUsedToPresentPermanentInfo: "Puisque cela pourrait nuire considérablement à l'expérience utilisateur pour les nouveaux utilisateurs, il est recommandé d'utiliser les annonces pour afficher des informations temporaires plutôt que des informations persistantes." - dialogAnnouncementUxWarn: "Avoir deux ou plus annonces de style dialogue en même temps pourrait nuire considérablement à l'expérience utilisateur. Veuillez les utiliser avec caution." - silence: "Ne pas me notifier" - silenceDescription: "Si activée, vous ne recevrez pas de notifications sur les annonces et n'aurez pas besoin de les marquer comme lues." -_initialAccountSetting: - accountCreated: "Votre compte a été créé avec succès !" - letsStartAccountSetup: "Procédons au réglage initial du compte." - letsFillYourProfile: "Commençons par configurer votre profil !" - profileSetting: "Paramètres du profil" - privacySetting: "Paramètres de confidentialité" - initialAccountSettingCompleted: "Configuration du profil terminée avec succès !" - haveFun: "Profitez de {name} !" - youCanContinueTutorial: "Vous pouvez procéder au tutoriel sur l'utilisation de {name}(Misskey) ou vous arrêter ici et commencer à l'utiliser immédiatement." - startTutorial: "Démarrer le tutoriel" - skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?" -_initialTutorial: - launchTutorial: "Visionner le tutoriel" - title: "Tutoriel" - wellDone: "Bien joué !" - skipAreYouSure: "Quitter le tutoriel ?" - _landing: - title: "Bienvenue dans le tutoriel" - description: "Ici, vous pouvez apprendre l'utilisation de base de Misskey et ses fonctionnalités." - _note: - title: "Qu'est-ce que les notes ?" - description: "Les messages sur Misskey sont appelés des « notes » . Les notes sont classées par ordre chronologique sur le fil et sont mises à jour en temps réel." - reply: "Vous pouvez répondre aux messages. Vous pouvez également répondre aux réponses et poursuivre la conversation comme un fil de discussion." - renote: "Vous pouvez partager cette note sur votre propre fil. Vous pouvez aussi ajouter du texte en citant." - reaction: "Vous pouvez ajouter des réactions. Les détails sont expliqués à la page suivante." - menu: "Vous pouvez afficher les détails de la note, copier le lien et effectuer d'autres actions." - _reaction: - title: "Qu'est-ce que les réactions ?" - description: "Vous pouvez ajouter des « réactions » aux notes. Les réactions vous permettent d'exprimer à l'aise des nuances qui ne peuvent pas être exprimées par des mentions j'aime." - letsTryReacting: "Des réactions peuvent être ajoutées en cliquant sur le bouton « + » de la note. Essayez d'ajouter une réaction à cet exemple de note !" - reactToContinue: "Ajoutez une réaction pour procéder." - reactNotification: "Vous recevez des notifications en temps réel lorsque quelqu'un réagit à votre note." - reactDone: "Vous pouvez annuler la réaction en cliquant sur le bouton « - » ." - _timeline: - title: "Fonctionnement des fils" - description1: "Misskey offre plusieurs fils selon l'usage (certains peuvent être désactivés par le serveur)." - home: "Vous pouvez voir les notes des utilisateurs auxquels vous êtes abonné·e." - local: "Vous pouvez voir les notes de tous les utilisateurs sur cette instance." - social: "Les notes des fils principal et local sont affichées." - global: "Vous pouvez voir les notes de toutes les instances connectées." - description2: "Vous pouvez passer d'un fil à l'autre en haut de l'écran à tout moment." - description3: "De plus, il y a les fils des listes et des canaux. Pour plus de détails, consultez {link}." - _postNote: - title: "Paramètres de la publication de note" - description1: "Lorsque vous publiez des notes sur Misskey, diverses options sont disponibles. Voici le formulaire de publication." - _visibility: - description: "Vous pouvez choisir qui peut voir vos notes." - public: "Visible à tous les utilisateurs." - home: "Uniquement visible sur le fil principal. Les utilisateurs pourront la voir en visitant ton profil, en s'abonnant à vous et par les renotes." - followers: "Uniquement visible à vos abonnés. Elle ne pourra être renotée que par vous-même." - direct: "Uniquement visible aux utilisateurs de votre choix. Les récipients seront notifiés. Cette option peut être utilisée comme alternative aux messages directs." - doNotSendConfidencialOnDirect1: "Faites attention quand vous envoyez vos informations sensibles !" - doNotSendConfidencialOnDirect2: "Les administrateurs de l'instance destinataire peuvent voir toutes les notes publiées. Soyez prudent·e avec vos informations sensibles quand vous envoyez des notes directes aux utilisateurs dont vous ne vous fiez pas aux instances." - localOnly: "Désactiver la fédération de la note aux autres instances. Les utilisateurs des autres instances ne pourront pas voir directement la note quelle que soit l'étendue de la publication mentionnée ci-dessus." - _cw: - title: "Masquer le contenu (CW)" - description: "Au lieu du corps du texte, le contenu du champ « commentaires » s'affichera. Appuyez sur « afficher le contenu » pour voir le corps du texte." - _exampleNote: - cw: "Attention : cela vous donnera faim !" - note: "J'ai mangé un beignet enrobé de chocolat 🍩😋" - useCases: "Utilisé pour désigner certaines notes selon les règles du serveur ou pour cacher des spoilers ou des textes sensibles." - _howToMakeAttachmentsSensitive: - title: "Comment marquer un fichier joint comme sensible ?" - description: "Attachez un drapeau « sensible » aux fichiers joints selon les règles du serveur ou si vous ne voulez pas que le fichier soit vu directement." - tryThisFile: "Essayez de marquer l'image jointe à ce formulaire de publication comme sensible !" - _exampleNote: - note: "Oups, j'ai échoué à ouvrir le couvercle du natto..." - method: "Pour marquer un fichier joint comme sensible, cliquez sur la vignette du fichier pour ouvrir le menu et cliquez sur « marquer comme sensible » ." - sensitiveSucceeded: "Quand vous joignez des fichiers, veuillez indiquer la sensibilité selon les règles du serveur." - doItToContinue: "Marquez le fichier joint comme sensible pour procéder." - _done: - title: "Le tutoriel est terminé ! 🎉" - description: "Les fonctionnalités introduites ici ne sont que quelques-unes. Pour savoir plus sur l'utilisation de Misskey, veuillez consulter {link}." -_timelineDescription: - home: "Sur le fil principal, vous pouvez voir les notes des utilisateurs auxquels vous êtes abonné·e." - local: "Sur le fil local, vous pouvez voir les notes de tous les utilisateurs sur cette instance." - social: "Sur le fil social, les notes des fils principal et local sont affichées." - global: "Sur le fil global, vous pouvez voir les notes de toutes les instances connectées." -_serverSettings: - iconUrl: "URL de l’icône" - appIconResolutionMustBe: "La résolution doit être au moins {resolution}." - shortName: "Nom court" - shortNameDescription: "Si le nom officiel de l'instance est long, cette abréviation peut être affichée à la place." - fanoutTimelineDescription: "Si activée, la performance de la récupération de la chronologie augmentera considérablement et la charge sur la base de données sera réduite. En revanche, l'utilisation de la mémoire de Redis augmentera. Considérez désactiver cette option si le serveur est bas en mémoire ou instable." - fanoutTimelineDbFallback: "Recours à la base de données" - fanoutTimelineDbFallbackDescription: "Si activée, une demande supplémentaire à la base de données est effectuée comme solution de rechange quand le fil n'est pas mis en cache. Si désactivée, la demande à la base de données n'est pas effectuée, ce qui réduit davantage la charge du serveur mais limite l'étendue du fil récupérable." -_accountMigration: - moveFrom: "Migrer un autre compte vers le présent compte" - moveFromSub: "Créer un alias vers un autre compte" - moveToLabel: "Compte vers lequel vous migrez :" - startMigration: "Migrer" - movedTo: "Compte vers lequel vous migrez :" _achievements: - earnedAt: "Date d'obtention" _types: _notes1: - title: "Je viens tout juste de configurer mon msky" description: "Publiez votre première note" flavor: "Passez un bon moment avec Misskey !" - _notes10: - title: "Quelques notes" - description: "Poster 10 notes" _notes100: title: "Beaucoup de notes" - description: "Poster 100 notes" - _notes500: - title: "Couvert de notes" - description: "Poster 500 notes" - _notes1000: - title: "Une montagne de notes" - description: "Poster 1000 notes" - _notes5000: - title: "Débordement de notes" - description: "Poster 5 000 notes" - _notes10000: - title: "Super note" - description: "Poster 10 000 notes" - _notes20000: - title: "Encore... plus... de... notes..." - description: "Poster 20 000 notes" - _notes30000: - title: "Notes notes notes !" - description: "Poster 30 000 notes" - _notes40000: - title: "Usine de notes" - description: "Poster 40 000 notes" - _notes50000: - title: "Planète des notes" - description: "Poster 50 000 notes" - _notes60000: - title: "Quasar de note" - description: "Poster 50 000 notes" - _notes70000: - title: "Trou noir de notes" - description: "Poster 70 000 notes" - _notes80000: - title: "Galaxie de notes" - description: "Poster 80 000 notes" - _notes90000: - title: "Univers de notes" - description: "Poster 90 000 notes" _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" - description: "Poster 100 000 notes" - flavor: "Avez-vous tant de choses à dire ?" _login3: - title: "Débutant I" + title: "Débutant Ⅰ" description: "Se connecter pour un total de 3 jours" - flavor: "Dès maintenant, appelez-moi Misskeynaute" _login7: - title: "Débutant II" + title: "Débutant Ⅱ" description: "Se connecter pour un total de 7 jours" - flavor: "On s'habitue ?" _login15: - title: "Débutant III" + title: "Débutant Ⅲ" description: "Se connecter pour un total de 15 jours" _login30: - title: "Misskeynaute I" description: "Se connecter pour un total de 30 jours" _login60: - title: "Misskeynaute II" description: "Se connecter pour un total de 60 jours" _login100: - title: "Misskeynaute III" description: "Se connecter pour un total de 100 jours" - flavor: "Misskeynaute acharné·e" _login200: - title: "Régulier I" description: "Se connecter pour un total de 200 jours" _login300: - title: "Régulier II" description: "Se connecter pour un total de 300 jours" _login400: - title: "Régulier III" description: "Se connecter pour un total de 400 jours" _login500: - title: "Expert I" description: "Se connecter pour un total de 500 jours" - flavor: "Non, mes amis, j'aime les notes" _login600: - title: "Expert II" description: "Se connecter pour un total de 600 jours" _login700: - title: "Expert III" description: "Se connecter pour un total de 700 jours" _login800: - title: "Maître des notes I" description: "Se connecter pour un total de 800 jours" _login900: - title: "Maître des notes II" description: "Se connecter pour un total de 900 jours" _login1000: - title: "Maître des notes III" - description: "Se connecter pour un total de 1 000 jours" flavor: "Merci d'utiliser Misskey !" - _noteClipped1: - title: "Je... dois... clip..." - description: "Ajouter sa première note aux clips" - _profileFilled: - title: "Bien préparé" - description: "Configuration de votre profil" _markedAsCat: title: "Je suis un chat" - description: "Rendre votre compte comme un chat" flavor: "Je n'ai pas encore de nom" - _following1: - title: "Vous suivez votre premier·ère utilisateur·rice" - _following10: - description: "S'abonner à plus de 10 utilisateur·rice·s" _following50: title: "Beaucoup d'amis" - description: "S'abonner à plus de 50 utilisateur·rice·s" - _following100: - description: "S'abonner à plus de 100 utilisateur·rice·s" - _following300: - description: "S'abonner à plus de 300 utilisateur·rice·s" _followers10: title: "Abonnez-moi !" - description: "Obtenir plus de 10 abonné·e·s" - _followers50: - description: "Obtenir plus de 50 abonné·e·s" - _followers100: - title: "Populaire" - description: "Obtenir plus de 100 abonné·e·s" - _followers300: - description: "Obtenir plus de 300 abonné·e·s" - _followers500: - title: "Tour radio" - description: "Obtenir plus de 500 abonné·e·s" - _followers1000: - title: "Influenceur·euse" - description: "Obtenir plus de 1000 abonné·e·s" _iLoveMisskey: title: "J’adore Misskey" - description: "Publication « J’❤ #Misskey »" - flavor: "L'équipe de développement de Misskey apprécie vraiment votre aide !" - _foundTreasure: - title: "Chasse au trésor" - description: "Vous avez trouvé le trésor caché" - _client30min: - title: "Pause bien méritée" - _postedAtLateNight: - flavor: "C’est l’heure d’aller au lit." - _postedAt0min0sec: - title: "Horloge parlante" - description: "Publication d’une note à 00:00" - flavor: "Tic tac, tic tac, tic tac, ding !" _viewInstanceChart: title: "Analyste" - _outputHelloWorldOnScratchpad: - title: "Hello, world!" - _open3windows: - title: "Multi-fenêtres" - _driveFolderCircularReference: - title: "Référence circulaire" - _setNameToSyuilo: - title: "Complexe de dieu" - description: "Vous avez spécifié « syuilo » comme nom" - _passedSinceAccountCreated1: - title: "Premier anniversaire" - description: "Un an est passé depuis la création du compte" - _passedSinceAccountCreated2: - title: "Second anniversaire" - description: "Deux ans sont passés depuis la création du compte" - _passedSinceAccountCreated3: - title: "3ème anniversaire" - description: "Trois ans sont passés depuis la création du compte" _loggedInOnBirthday: title: "Joyeux Anniversaire !" - description: "Vous vous êtes connecté à la date de votre anniversaire" _loggedInOnNewYearsDay: title: "Bonne année !" - description: "Vous vous êtes connecté le premier jour de l'année" - flavor: "Merci pour le soutient continue sur cette instance." _cookieClicked: - title: "Jeu de clic sur des cookies" - description: "Cliqué sur un cookie" flavor: "Attendez une minute, vous êtes sur le mauvais site web ?" - _brainDiver: - title: "Brain Diver" - description: "Poster le lien sur Brain Diver" - flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "Débordement de tests" - description: "Détruire le bouton de test de notifications dans un intervalle extrêmement court" - _tutorialCompleted: - title: "Diplôme de la course élémentaire de Misskey" - description: "Terminer le tutoriel" - _bubbleGameExplodingHead: - title: "🤯" - description: "Le plus gros objet du jeu de bulles" - _bubbleGameDoubleExplodingHead: - title: "Double🤯" _role: - new: "Nouveau rôle" - edit: "Modifier le rôle" - name: "Nom du rôle" - description: "Description du rôle" - permission: "Autorisations du rôle" assignTarget: "Attribuer" - manual: "Manuel" - manualRoles: "Rôles manuels" - conditional: "Conditionnel" - conditionalRoles: "Rôles conditionnels" - condition: "Condition" - isConditionalRole: "Ceci est un rôle conditionnel." - isPublic: "Rôle public" - options: "Options" - policies: "Stratégies" - baseRole: "Modèle de rôle" - useBaseValue: "Utiliser la valeur du modèle de rôle" - chooseRoleToAssign: "Sélectionner le rôle à assigner" - iconUrl: "URL de l’icône" - displayOrder: "Classement" priority: "Priorité" _priority: low: "Basse" @@ -1638,13 +1000,6 @@ _role: high: "Haute" _options: canManageCustomEmojis: "Gestion des émojis personnalisés" - canManageAvatarDecorations: "Gestion des décorations d'avatar" - driveCapacity: "Capacité de stockage du Disque" - antennaMax: "Nombre maximum d'antennes" - wordMuteMax: "Nombre maximal de caractères dans le filtre de mots" - canUseTranslator: "Usage de la fonctionnalité de traduction" - avatarDecorationLimit: "Nombre maximal de décorations d'avatar" - canImportAntennas: "Autoriser l'importation d'antennes" _sensitiveMediaDetection: description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement." sensitivity: "Sensibilité de la détection" @@ -1678,10 +1033,6 @@ _ad: back: "Retour" reduceFrequencyOfThisAd: "Voir cette publicité moins souvent" hide: "Cacher " - adsSettings: "Paramètres des publicités" - notesPerOneAd: "Intervalle de diffusion de publicités lors de la mise à jour en temps réel (nombre de notes par publicité)" - setZeroToDisable: "Mettre cette valeur à 0 pour désactiver la diffusion de publicités lors de la mise à jour en temps réel" - adsTooClose: "L'expérience utilisateur peut être gravement compromise par un intervalle de diffusion de publicités extrêmement court." _forgotPassword: enterEmail: "Entrez ici l'adresse e-mail que vous avez enregistrée pour votre compte. Un lien vous permettant de réinitialiser votre mot de passe sera envoyé à cette adresse." ifNoEmail: "Si vous n'avez pas enregistré d'adresse e-mail, merci de contacter l'administrateur·rice de votre instance." @@ -1697,10 +1048,9 @@ _email: _receiveFollowRequest: title: "Vous avez reçu une demande de suivi" _plugin: - install: "Installation d'extensions" + install: "Installation de plugin" installWarn: "N’installez que des extensions provenant de sources de confiance." - manage: "Gestion des extensions" - viewSource: "Afficher la source" + manage: "Gestion des plugins" _preferencesBackups: list: "Sauvegardes créées" saveNew: "Nouvelle sauvegarde" @@ -1712,7 +1062,7 @@ _preferencesBackups: nameAlreadyExists: "Le nom de sauvegarde \"{name}\" existe déjà. Veuillez spécifier un autre nom." applyConfirm: "Voulez-vous appliquer la sauvegarde '{name}' au dispositif actuel ? La configuration actuelle de l'appareil sera perdue." saveConfirm: "Voulez-vous écraser {name} ?" - deleteConfirm: "Êtes-vous sûr·e de vouloir supprimer {name} ?" + deleteConfirm: "Voulez-vous supprimer {name} ?" renameConfirm: "Voulez-vous remplacer \"{old}\" par \"{new}\" ?" noBackups: "Aucune sauvegarde n'est disponible. L'option \"Nouvelle sauvegarde\" vous permet de sauvegarder la configuration actuelle du client sur le serveur." createdAt: "Créé : {date} {time}" @@ -1734,9 +1084,6 @@ _aboutMisskey: donate: "Soutenir Misskey" morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰" patrons: "Contributeurs" - projectMembers: "Membres du projet" -_displayOfSensitiveMedia: - force: "Masquer tous les médias" _instanceTicker: none: "Cacher " remote: "Montrer pour les utilisateur·ice·s distant·e·s" @@ -1755,9 +1102,6 @@ _channel: following: "Abonné·e" usersCount: "{n} Participant·e·s" notesCount: "{n} Notes" - nameAndDescription: "Nom et description" - nameOnly: "Nom seulement" - allowRenoteToExternal: "Permettre la renote et la citation hors du canal" _menuDisplay: sideFull: "Latéral" sideIcon: "Latéral (icônes)" @@ -1767,6 +1111,11 @@ _wordMute: muteWords: "Mots à filtrer" muteWordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR." muteWordsDescription2: "Pour utiliser des expressions régulières (regex), mettez les mots-clés entre barres obliques." + softDescription: "Masquez les notes de votre fil selon les paramètres que vous définissez." + hardDescription: "Empêchez votre fil de charger les notes selon les paramètres que vous définissez. Cette action est irréversible : si vous modifiez ces paramètres plus tard, les notes précédemment filtrées ne seront pas récupérées." + soft: "Doux" + hard: "Strict" + mutedNotes: "Notes filtrées" _instanceMute: instanceMuteDescription: "Met en sourdine toutes les notes et renotes de l'instance configurée, y compris les réponses aux utilisateurs de l'instance muette." instanceMuteDescription2: "Séparer avec de nouvelles lignes" @@ -1813,6 +1162,7 @@ _theme: header: "Entête" navBg: "Fond de la barre latérale" navFg: "Texte de la barre latérale" + navHoverFg: "Texte de la barre latérale (survolé)" navActive: "Texte de la barre latérale (actif)" navIndicator: "Indicateur de barre latérale" link: "Lien" @@ -1829,26 +1179,30 @@ _theme: infoFg: "Texte d'information" infoWarnBg: "Arrière-plan des avertissements" infoWarnFg: "Texte d’avertissement" + cwBg: "Arrière-plan du CW" + cwFg: "Texte du bouton CW" + cwHoverBg: "Arrière-plan du bouton CW (survolé)" toastBg: "Arrière-plan de la bulle de notification" toastFg: "Texte de la bulle de notification" buttonBg: "Arrière-plan du bouton" buttonHoverBg: "Arrière-plan du bouton (survolé)" inputBorder: "Cadre de la zone de texte" + listItemHoverBg: "Arrière-plan d'item de liste (survolé)" + driveFolderBg: "Arrière-plan du dossier de disque" + wallpaperOverlay: "Superposition de fond d'écran" badge: "Badge" messageBg: "Arrière plan de la discussion" + accentDarken: "Plus sombre" + accentLighten: "Plus clair" fgHighlighted: "Texte mis en évidence" _sfx: note: "Nouvelle note" noteMy: "Ma note" notification: "Notifications" - reaction: "Lors de la sélection de la réaction" -_soundSettings: - driveFile: "Utiliser un effet sonore sur le Disque" - driveFileWarn: "Veuillez sélectionner le fichier sur le Disque" - driveFileTypeWarn: "Ce fichier n'est pas pris en charge" - driveFileTypeWarnDescription: "Veuillez sélectionner un fichier audio" - driveFileDurationWarn: "L'effet sonore est trop long" - driveFileDurationWarnDescription: "Utiliser un effet sonore long peut affecter l'utilisation de Misskey. Voulez-vous encore continuer ?" + chat: "Discuter" + chatBg: "Discussion (arrière-plan)" + antenna: "Réception de l’antenne" + channel: "Notifications de canal" _ago: future: "Futur" justNow: "à l’instant" @@ -1860,14 +1214,6 @@ _ago: monthsAgo: "Il y a {n} mois" yearsAgo: "Il y a {n} ans" invalid: "Il n'y a rien à voir ici" -_timeIn: - seconds: "Dans {n}s" - minutes: "Dans {n}min" - hours: "Dans {n}h" - days: "Dans {n}j" - weeks: "Dans {n} sem." - months: "Dans {n} mois" - years: "Dans {n}a" _time: second: "s" minute: "min" @@ -1877,25 +1223,19 @@ _2fa: alreadyRegistered: "Configuration déjà achevée." step1: "Tout d'abord, installez une application d'authentification, telle que {a} ou {b}, sur votre appareil." step2: "Ensuite, scannez le code QR affiché sur l’écran." - step3Title: "Veuillez saisir le code d’authentification" + step2Url: "Vous pouvez également saisir cette URL si vous utilisez un programme de bureau :" step3: "Entrez le jeton affiché sur votre application pour compléter la configuration." - setupCompleted: "Configuration terminée avec succès !" step4: "À partir de maintenant, ce même jeton vous sera demandé à chacune de vos connexions." - securityKeyNotSupported: "Votre navigateur ne prend pas en charge les clés de sécurité." securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser davantage le processus de connexion grâce à une clé de sécurité matérielle qui prend en charge FIDO2, ou bien en configurant l'authentification par empreinte digitale ou par code PIN sur votre appareil." - securityKeyName: "Nom de la clé" - removeKey: "Supprimer la clé de sécurité" - removeKeyConfirm: "Êtes-vous sûr·e de vouloir supprimer {name} ?" - renewTOTPOk: "Reconfigurer" + removeKeyConfirm: "Voulez-vous supprimer {name} ?" renewTOTPCancel: "Pas maintenant" - backupCodes: "Codes de Secours" _permissions: "read:account": "Afficher les informations du compte" "write:account": "Mettre à jour les informations de votre compte" "read:blocks": "Voir les comptes bloqués" "write:blocks": "Gérer les comptes bloqués" - "read:drive": "Parcourir le Disque" - "write:drive": "Modifier le Disque" + "read:drive": "Parcourir le Drive" + "write:drive": "Écrire sur le Drive" "read:favorites": "Afficher les favoris" "write:favorites": "Gérer les favoris" "read:following": "Voir les informations de vos abonnements" @@ -1915,37 +1255,13 @@ _permissions: "read:page-likes": "Voir les mentions « J'aime » des pages" "write:page-likes": "Gérer les mentions « J'aime » sur les pages" "read:user-groups": "Voir les groupes d'utilisateur·rice·s" - "write:user-groups": "Éditer les groupes d'utilisateur·rice·s" + "write:user-groups": "Éditer les groupes des utilisateur·rice·s" "read:channels": "Lire les canaux" "write:channels": "Gérer les canaux" "read:gallery": "Voir la galerie" "write:gallery": "Éditer la galerie" "read:gallery-likes": "Voir les mentions « J'aime » dans la galerie" "write:gallery-likes": "Gérer les mentions « J'aime » dans la galerie" - "read:flash": "Voir le Play" - "write:flash": "Modifier le Play" - "read:flash-likes": "Lire vos mentions j'aime des Play" - "write:flash-likes": "Modifier vos mentions j'aime des Play" - "read:admin:abuse-user-reports": "Voir les utilisateurs signalés" - "write:admin:delete-account": "Supprimer le compte d'utilisateur" - "write:admin:delete-all-files-of-a-user": "Supprimer tous les fichiers d'un utilisateur" - "read:admin:index-stats": "Voir les statistiques sur les index de base de données" - "read:admin:table-stats": "Voir les statistiques sur les index de base de données" - "read:admin:user-ips": "Voir l'adresse IP de l'utilisateur" - "read:admin:meta": "Voir les métadonnées de l'instance" - "write:admin:reset-password": "Réinitialiser le mot de passe de l'utilisateur" - "write:admin:resolve-abuse-user-report": "Résoudre le signalement d'un utilisateur" - "write:admin:send-email": "Envoyer un mail" - "read:admin:server-info": "Voir les informations de l'instance" - "read:admin:show-moderation-log": "Voir les logs de modération" - "read:admin:show-user": "Voir les informations privées de l'utilisateur" - "write:admin:suspend-user": "Suspendre l'utilisateur" - "write:admin:unset-user-avatar": "Retirer l'avatar de l'utilisateur" - "write:admin:unset-user-banner": "Retirer la bannière de l'utilisateur" - "write:admin:unsuspend-user": "Lever la suspension d'un utilisateur" - "write:admin:meta": "Gérer les métadonnées de l'instance" - "write:admin:roles": "Gérer les rôles" - "write:chat": "Gérer les discussions" _auth: shareAccess: "Autoriser \"{name}\" à accéder à votre compte ?" shareAccessAsk: "Voulez-vous vraiment autoriser cette application à accéder à votre compte?" @@ -1993,10 +1309,9 @@ _widgets: userList: "Liste utilisateur" _userList: chooseList: "Sélectionner une liste" - birthdayFollowings: "Utilisateurs qui fêtent l'anniversaire aujourd'hui" _cw: hide: "Masquer" - show: "Afficher le contenu" + show: "Afficher plus …" chars: "{count} caractères" files: "{count} fichiers" _poll: @@ -2030,11 +1345,10 @@ _visibility: followersDescription: "Publier à vos abonné·e·s uniquement" specified: "Direct" specifiedDescription: "Publier uniquement aux utilisateur·rice·s mentionné·e·s" - disableFederation: "Défédérer" _postForm: replyPlaceholder: "Répondre à cette note ..." quotePlaceholder: "Citez cette note ..." - channelPlaceholder: "Publier au canal…" + channelPlaceholder: "Publier vers le canal" _placeholders: a: "Quoi de neuf ?" b: "Il s'est passé quelque chose ?" @@ -2052,19 +1366,16 @@ _profile: metadataDescription: "Vous pouvez afficher jusqu'à quatre informations supplémentaires dans votre profil." metadataLabel: "Étiquette" metadataContent: "Contenu" - changeAvatar: "Changer l'avatar" + changeAvatar: "Changer l'image de profil" changeBanner: "Changer de bannière" - avatarDecorationMax: "Vous pouvez mettre au plus {max} décorations d'avatar." _exportOrImport: allNotes: "Toutes les notes" - clips: "Clip" followingList: "Abonnements" muteList: "Comptes masqués" blockingList: "Comptes bloqués" userLists: "Listes" excludeMutingUsers: "Exclure les utilisateur·rice·s mis en sourdine" excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs" - withReplies: "Inclure les réponses des utilisateur·rice·s importé·e·s dans le fil" _charts: federation: "Fédération" apRequest: "Requêtes" @@ -2097,16 +1408,7 @@ _timelines: social: "Social" global: "Global" _play: - new: "Créer un Play" - edit: "Modifier un Play" - created: "Play créé" - updated: "Play édité" - deleted: "Play supprimé" - pageSetting: "Configuration du Play" - editThisPage: "Modifier ce Play" viewSource: "Afficher la source" - my: "Mes Play" - liked: "Play aimés" featured: "Populaire" title: "Titre" script: "Script" @@ -2115,6 +1417,9 @@ _pages: newPage: "Créer une page" editPage: "Modifier une page" readPage: "Affichage de la source en cours" + created: "La page a été créée !" + updated: "La page a été mise à jour !" + deleted: "La page a été supprimée" pageSetting: "Paramètres de la Page" nameAlreadyExists: "L'URL de page spécifiée existe déjà" invalidNameTitle: "L'URL de page spécifiée n’est pas valide" @@ -2140,7 +1445,7 @@ _pages: fontSerif: "Serif" fontSansSerif: "Sans Serif" eyeCatchingImageSet: "Définir une image attractive" - eyeCatchingImageRemove: "Supprimer la miniature" + eyeCatchingImageRemove: "Supprimer l'image attractive" chooseBlock: "Ajouter un bloc" selectType: "Choisir un type" contentBlocks: "Contenu" @@ -2167,23 +1472,14 @@ _notification: youGotReply: "Réponse de {name}" youGotQuote: "Cité·e par {name}" youRenoted: "{name} vous a Renoté" - youWereFollowed: "s'est abonné·e à vous" + youWereFollowed: "Vous suit" youReceivedFollowRequest: "Vous avez reçu une demande d’abonnement" yourFollowRequestAccepted: "Votre demande d’abonnement a été accepté" pollEnded: "Les résultats du sondage sont disponibles" unreadAntennaNote: "Antenne {name}" - roleAssigned: "Rôle attribué" emptyPushNotificationMessage: "Les notifications push ont été mises à jour" - achievementEarned: "Accomplissement déverrouillé" - testNotification: "Tester la notification" - reactedBySomeUsers: "{n} utilisateur·rice·s ont réagi" - likedBySomeUsers: "{n} utilisateurs ont aimé votre note" - renotedBySomeUsers: "{n} utilisateur·rice·s ont renoté" - followedBySomeUsers: "{n} utilisateur·rice·s se sont abonné·e·s à vous" - login: "Quelqu'un s'est connecté" _types: all: "Toutes" - note: "Nouvelles notes" follow: "Nouvel·le abonné·e" mention: "Mentions" reply: "Réponses" @@ -2193,9 +1489,6 @@ _notification: pollEnded: "Sondages se cloturant" receiveFollowRequest: "Demande d'abonnement reçue" followRequestAccepted: "Demande d'abonnement acceptée" - roleAssigned: "Rôle reçu" - achievementEarned: "Déverrouillage d'accomplissement" - login: "Se connecter" app: "Notifications provenant des apps" _actions: followBack: "Suivre" @@ -2217,7 +1510,6 @@ _deck: deleteProfile: "Supprimer le profil" introduction: "Créez l’interface parfaite qui vous sied en arrangeant librement les colonnes !" introduction2: "Cliquez sur le + à droite de l'écran pour ajouter de nouvelles colonnes quand vous le souhaitez." - flexible: "Ajuster automatiquement la largeur" _columns: main: "Principale" widgets: "Widgets" @@ -2225,138 +1517,9 @@ _deck: tl: "Fil" antenna: "Antennes" list: "Listes" - channel: "Canal" + channel: "Canaux" mentions: "Mentions" direct: "Direct" -_drivecleaner: - orderBySizeDesc: "Taille descendante" - orderByCreatedAtAsc: "Date d'ajout ascendante" _webhookSettings: name: "Nom" - secret: "Secret" - trigger: "Activateur" active: "Activé" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "E-mail " - keywords: "Mots clés " -_moderationLogTypes: - createRole: "Rôle créé" - deleteRole: "Rôle supprimé" - updateRole: "Rôle mis à jour" - assignRole: "Rôle attribué" - unassignRole: "Rôle enlevé" - suspend: "Utilisateur suspendu" - unsuspend: "Suspension d'un utilisateur levée" - addCustomEmoji: "Émoji personnalisé ajouté" - updateCustomEmoji: "Émoji personnalisé mis à jour" - deleteCustomEmoji: "Émoji personnalisé supprimé" - updateServerSettings: "Paramètres du serveur mis à jour" - updateUserNote: "Note de modération mise à jour" - deleteDriveFile: "Fichier supprimé" - deleteNote: "Note supprimée" - createGlobalAnnouncement: "Annonce globale créée" - createUserAnnouncement: "Annonce individuelle créée" - updateGlobalAnnouncement: "Annonce globale mise à jour" - updateUserAnnouncement: "Annonce individuelle mise à jour" - deleteGlobalAnnouncement: "Annonce globale supprimée" - deleteUserAnnouncement: "Annonce individuelle supprimée" - resetPassword: "Mot de passe réinitialisé" - suspendRemoteInstance: "Instance distante suspendue" - unsuspendRemoteInstance: "Suspension d'une instance distante levée" - markSensitiveDriveFile: "Fichier marqué comme sensible" - unmarkSensitiveDriveFile: "Marquage du fichier comme sensible enlevé" - resolveAbuseReport: "Signalement résolu" - createInvitation: "Code d'invitation créé" - createAd: "Publicité créée" - deleteAd: "Publicité supprimée" - updateAd: "Publicité mise à jour" - createAvatarDecoration: "Décoration d'avatar créée" - updateAvatarDecoration: "Décoration d'avatar mise à jour" - deleteAvatarDecoration: "Décoration d'avatar supprimée" - unsetUserAvatar: "Supprimer l'avatar de l'utilisateur·rice" - unsetUserBanner: "Supprimer la bannière de l'utilisateur·rice" - deleteFlash: "Supprimer le Play" -_fileViewer: - title: "Détails du fichier" - type: "Type du fichier" - size: "Taille du fichier" - url: "URL" - uploadedAt: "Date de téléversement" - attachedNotes: "Notes avec ce fichier" - thisPageCanBeSeenFromTheAuthor: "Cette page ne peut être vue que par l'utilisateur qui a téléversé ce fichier." -_externalResourceInstaller: - title: "Installer depuis un site externe" - checkVendorBeforeInstall: "Veuillez confirmer que le distributeur est fiable avant l'installation." - _plugin: - title: "Voulez-vous installer cette extension ?" - _theme: - title: "Voulez-vous installer ce thème ?" - _meta: - base: "Palette de couleurs de base" - _vendorInfo: - title: "Informations sur le distributeur" - endpoint: "Point de terminaison référencé" - hashVerify: "Vérification de l'intégrité du fichier" - _errors: - _invalidParams: - title: "Paramètres invalides" - description: "Il y a un manque d'informations nécessaires pour obtenir des données à partir de sites externes. Veuillez vérifier l'URL." - _resourceTypeNotSupported: - title: "Cette ressource externe n'est pas prise en charge." - description: "Le type de ressource obtenue à partir de ce site externe n'est pas pris en charge. Veuillez contacter l'administrateur du site." - _failedToFetch: - title: "Échec de récupération des données" - fetchErrorDescription: "La communication avec le site externe a échoué. Si vous réessayez et que cela ne s'améliore pas, veuillez contacter l'administrateur du site." - parseErrorDescription: "Les données obtenues à partir du site externe n'ont pas pu être parsées. Veuillez contacter l'administrateur du site." - _hashUnmatched: - title: "Échec de vérification des données" - description: "La vérification de l'intégrité des données fournies a échoué. Pour des raisons de sécurité, l'installation ne peut pas continuer. Veuillez contacter l'administrateur du site." - _pluginParseFailed: - title: "Erreur d'AiScript" - description: "Bien que les données aient été obtenues, elles n'ont pas pu être lues, car il y a eu une erreur lors du parsage d'AiScript. Veuillez contacter l'auteur de l'extension. Pour plus de détails sur l'erreur, veuillez consulter la console JavaScript." - _pluginInstallFailed: - title: "Échec d'installation de l'extension" - description: "Il y a eu un problème lors de l'installation de l'extension. Veuillez réessayer. Pour plus de détails sur l'erreur, veuillez consulter la console JavaScript." - _themeParseFailed: - title: "Erreur de parsage du thème" - description: "Bien que les données aient été obtenues, elles n'ont pas pu être lues, car il y a eu une erreur lors du parsage du fichier du thème. Veuillez contacter l'auteur du thème. Pour plus de détails sur l'erreur, veuillez consulter la console JavaScript." - _themeInstallFailed: - title: "Échec d'installation du thème" - description: "Il y a eu un problème lors de l'installation du thème. Veuillez réessayer. Pour plus de détails sur l'erreur, veuillez consulter la console JavaScript." -_dataSaver: - _media: - title: "Chargement des médias" - description: "Empêche le chargement automatique des images et des vidéos. Appuyez sur les images et les vidéos cachées pour les charger." - _avatar: - title: "Animation d'avatars" - description: "Arrête l'animation d'avatars. Comme les images animées peuvent être plus volumineuses que les images normales, cela permet de réduire davantage le trafic de données." - _code: - title: "Mise en évidence du code" - description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données." -_reversi: - reversi: "Reversi" - blackIs: "{name} joue les noirs" - rules: "Règles" - waitingBoth: "Préparez-vous" - myTurn: "C’est votre tour" - turnOf: "C'est le tour de {name}" - pastTurnOf: "Tour de {name}" - surrender: "Se rendre" - surrendered: "Par abandon" - total: "Total" - playing: "En cours" - lookingForPlayer: "Recherche d'adversaire" -_mediaControls: - playbackRate: "Vitesse de lecture" -_embedCodeGen: - title: "Personnaliser le code d'intégration" - generateCode: "Générer le code d'intégration" -_remoteLookupErrors: - _noSuchObject: - title: "Non trouvé" -_search: - searchScopeAll: "Tous" - searchScopeLocal: "Local" - searchScopeUser: "Spécifier l'utilisateur·rice" diff --git a/locales/generateDTS.js b/locales/generateDTS.js index 49807144ec..bc98276325 100644 --- a/locales/generateDTS.js +++ b/locales/generateDTS.js @@ -1,230 +1,72 @@ import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; import * as yaml from 'js-yaml'; -import ts from 'typescript'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const parameterRegExp = /\{(\w+)\}/g; - -function createMemberType(item) { - if (typeof item !== 'string') { - return ts.factory.createTypeLiteralNode(createMembers(item)); - } - const parameters = Array.from( - item.matchAll(parameterRegExp), - ([, parameter]) => parameter, - ); - return parameters.length - ? ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('ParameterizedString'), - [ - ts.factory.createUnionTypeNode( - parameters.map((parameter) => - ts.factory.createStringLiteral(parameter), - ), - ), - ], - ) - : ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); -} +import * as ts from 'typescript'; function createMembers(record) { - return Object.entries(record).map(([k, v]) => { - const node = ts.factory.createPropertySignature( + return Object.entries(record) + .map(([k, v]) => ts.factory.createPropertySignature( undefined, ts.factory.createStringLiteral(k), undefined, - createMemberType(v), - ); - if (typeof v === 'string') { - ts.addSyntheticLeadingComment( - node, - ts.SyntaxKind.MultiLineCommentTrivia, - `* - * ${v.replace(/\n/g, '\n * ')} - `, - true, - ); - } - return node; - }); + typeof v === 'string' + ? ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) + : ts.factory.createTypeLiteralNode(createMembers(v)), + )); } export default function generateDTS() { const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); const members = createMembers(locale); const elements = [ - ts.factory.createVariableStatement( - [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier('kParameters'), - undefined, - ts.factory.createTypeOperatorNode( - ts.SyntaxKind.UniqueKeyword, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword), - ), - undefined, - ), - ], - ts.NodeFlags.Const, - ), - ), - ts.factory.createInterfaceDeclaration( - [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier('ParameterizedString'), - [ - ts.factory.createTypeParameterDeclaration( - undefined, - ts.factory.createIdentifier('T'), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ), - ], - undefined, - [ - ts.factory.createPropertySignature( - undefined, - ts.factory.createComputedPropertyName( - ts.factory.createIdentifier('kParameters'), - ), - undefined, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('T'), - undefined, - ), - ), - ], - ), - ts.factory.createInterfaceDeclaration( - [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier('ILocale'), - undefined, - undefined, - [ - ts.factory.createIndexSignature( - undefined, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier('_'), - undefined, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - undefined, - ), - ], - ts.factory.createUnionTypeNode([ - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('ParameterizedString'), - ), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('ILocale'), - undefined, - ), - ]), - ), - ], - ), ts.factory.createInterfaceDeclaration( [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier('Locale'), undefined, - [ - ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ - ts.factory.createExpressionWithTypeArguments( - ts.factory.createIdentifier('ILocale'), - undefined, - ), - ]), - ], + undefined, members, ), ts.factory.createVariableStatement( [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier('locales'), + [ts.factory.createVariableDeclaration( + ts.factory.createIdentifier('locales'), + undefined, + ts.factory.createTypeLiteralNode([ts.factory.createIndexSignature( undefined, - ts.factory.createTypeLiteralNode([ - ts.factory.createIndexSignature( - undefined, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier('lang'), - undefined, - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.StringKeyword, - ), - undefined, - ), - ], - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('Locale'), - undefined, - ), - ), - ]), - undefined, - ), - ], - ts.NodeFlags.Const, + [ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier('lang'), + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined, + )], + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier('Locale'), + undefined, + ), + )]), + undefined, + )], + ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags, ), ), - ts.factory.createFunctionDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - undefined, - ts.factory.createIdentifier('build'), - undefined, - [], - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('Locale'), - undefined, - ), + ts.factory.createExportAssignment( undefined, + true, + ts.factory.createIdentifier('locales'), ), - ts.factory.createExportDefault(ts.factory.createIdentifier('locales')), ]; - ts.addSyntheticLeadingComment( - elements[0], - ts.SyntaxKind.MultiLineCommentTrivia, - ' eslint-disable ', - true, + const printed = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + }).printList( + ts.ListFormat.MultiLine, + ts.factory.createNodeArray(elements), + ts.createSourceFile('index.d.ts', '', ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS), ); - ts.addSyntheticLeadingComment( - elements[0], - ts.SyntaxKind.SingleLineCommentTrivia, - ' This file is generated by locales/generateDTS.js', - true, - ); - ts.addSyntheticLeadingComment( - elements[0], - ts.SyntaxKind.SingleLineCommentTrivia, - ' Do not edit this file directly.', - true, - ); - const printed = ts - .createPrinter({ - newLine: ts.NewLineKind.LineFeed, - }) - .printList( - ts.ListFormat.MultiLine, - ts.factory.createNodeArray(elements), - ts.createSourceFile( - 'index.d.ts', - '', - ts.ScriptTarget.ESNext, - true, - ts.ScriptKind.TS, - ), - ); - fs.writeFileSync(`${__dirname}/index.d.ts`, printed, 'utf-8'); + fs.writeFileSync(`${__dirname}/index.d.ts`, `/* eslint-disable */ +// This file is generated by locales/generateDTS.js +// Do not edit this file directly. +${printed}`, 'utf-8'); } diff --git a/locales/hr-HR.yml b/locales/hr-HR.yml index 9cfebdd01a..ed97d539c0 100644 --- a/locales/hr-HR.yml +++ b/locales/hr-HR.yml @@ -1,5 +1 @@ --- -_lang_: "japanski" -ok: "OK" -gotIt: "Razumijem" -cancel: "otkazati" diff --git a/locales/ht-HT.yml b/locales/ht-HT.yml index e3595c79b6..ed97d539c0 100644 --- a/locales/ht-HT.yml +++ b/locales/ht-HT.yml @@ -1,18 +1 @@ --- -_lang_: "Japonè" -password: "modpas" -ok: "OK" -gotIt: "Konprann" -cancel: "anile" -noThankYou: "Sispann" -instance: "sèvè" -profile: "pwofil" -save: "kenbe" -delete: "efase" -instances: "sèvè" -remove: "efase" -smtpPass: "modpas" -_2fa: - renewTOTPCancel: "Sispann" -_widgets: - profile: "pwofil" diff --git a/locales/hu-HU.yml b/locales/hu-HU.yml deleted file mode 100644 index d0fdc027e9..0000000000 --- a/locales/hu-HU.yml +++ /dev/null @@ -1,105 +0,0 @@ ---- -_lang_: "Magyar" -monthAndDay: "{month}.{day}." -search: "Keresés" -notifications: "Értesítések" -username: "Felhasználónév" -password: "Jelszó" -forgotPassword: "Elfelejtett jelszó" -ok: "OK" -gotIt: "Rendben" -cancel: "Mégse" -noThankYou: "Nem, köszönöm" -enterUsername: "Felhasználónév megadása" -renotedBy: "{user} Renotolta" -noNotes: "Nincs Note" -noNotifications: "Nincs értesítés" -instance: "Szerver" -settings: "Beállítások" -notificationSettings: "Értesítés beállításai" -basicSettings: "Alapbeállítás" -otherSettings: "Egyéb beállítások" -openInWindow: "Megnyitás ablakban" -profile: "Saját profil" -timeline: "Idővonal" -noAccountDescription: "Nincs leírás" -login: "Bejelentkezés" -loggingIn: "Belépés" -logout: "Kijelentkezés" -signup: "Regisztráció" -uploading: "Feltöltés" -save: "Mentés" -users: "Felhasználók" -addUser: "Felhasználó hozzáadása" -favorite: "Kedvencek" -favorites: "Kedvencek" -unfavorite: "Törlés a kedvencek közül." -favorited: "Kedvencek közé rakva." -alreadyFavorited: "Már a kedvencek között van." -cantFavorite: "Nem sikerült a kedvencek közé rakni." -pin: "Rögzítés" -unpin: "Rögzítés feloldása" -copyContent: "Tartalom másolása" -copyLink: "Hivatkozás Másolása" -delete: "Törlés" -deleteAndEdit: "Törlés és szerkesztés" -deleteAndEditConfirm: "Biztosan törlöd ezt a jegyzetet és újrafogalmazza? Így eveszíted az összes reakciót, renote-ot és választ." -addToList: "Hozzáadás a listákhoz" -privacy: "Adatvédelem" -makeFollowManuallyApprove: "Csak jóváhagyással követhetnek" -defaultNoteVisibility: "Alapértelmezett láthatóság" -follow: "Követés" -followRequest: "Követés kérése" -followRequests: "Követési kérések" -unfollow: "Követés visszavonása" -followRequestPending: "Függőben levő követési kérés" -enterEmoji: "Írj egy emoji-t" -renote: "Renote" -unrenote: "Renote visszavonása" -renoted: "Renotolva" -cantRenote: "Nem lehet Renotolni" -cantReRenote: "A Renote nem renotálható" -quote: "Idézet" -inChannelRenote: "Csak csatornán bellüli Renote" -inChannelQuote: "Csak csatornán bellüli idézet" -pinnedNote: "Csatolt jegyzet" -pinned: "Rögzítés" -you: "Te" -clickToShow: "Kattints ide" -sensitive: "Érzékeny" -add: "Hozzáad" -reaction: "Reakciók" -reactions: "Reakciók" -instances: "Szerver" -remove: "Törlés" -pinnedNotes: "Csatolt jegyzet" -smtpUser: "Felhasználónév" -smtpPass: "Jelszó" -user: "Felhasználók" -searchByGoogle: "Keresés" -renotes: "Renote" -_theme: - keys: - renote: "Renote" -_sfx: - notification: "Értesítések" -_2fa: - renewTOTPCancel: "Nem, köszönöm" -_widgets: - profile: "Saját profil" - notifications: "Értesítések" - timeline: "Idővonal" -_profile: - username: "Felhasználónév" -_notification: - _types: - renote: "Renote" - quote: "Idézet" - reaction: "Reakciók" - login: "Bejelentkezés" - _actions: - renote: "Renote" -_deck: - _columns: - notifications: "Értesítések" - tl: "Idővonal" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 144990e6a6..34c995b05e 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -45,22 +45,16 @@ pin: "Sematkan ke profil" unpin: "Lepas sematan dari profil" copyContent: "Salin konten" copyLink: "Salin tautan" -copyLinkRenote: "Salin tautan renote" delete: "Hapus" deleteAndEdit: "Hapus dan sunting" deleteAndEditConfirm: "Apakah kamu yakin ingin menghapus note ini dan menyuntingnya? Kamu akan kehilangan semua reaksi, renote dan balasan di note ini." addToList: "Tambahkan ke daftar" -addToAntenna: "Tambahkan ke Antena" sendMessage: "Kirim pesan" copyRSS: "Salin RSS" copyUsername: "Salin nama pengguna" copyUserId: "Salin ID pengguna" copyNoteId: "Salin ID catatan" -copyFileId: "Salin Berkas" -copyFolderId: "Salin Folder" -copyProfileUrl: "Salin Alamat Web Profil" searchUser: "Cari pengguna" -searchThisUsersNotes: "Mencari catatan pengguna" reply: "Balas" loadMore: "Selebihnya" showMore: "Selebihnya" @@ -82,7 +76,7 @@ exportRequested: "Kamu telah meminta ekspor. Ini akan memakan waktu sesaat. Sete importRequested: "Kamu telah meminta impor. Ini akan memakan waktu sesaat." lists: "Daftar" noLists: "Kamu tidak memiliki daftar apapun" -note: "Catatan" +note: "Catat" notes: "Catatan" following: "Ikuti" followers: "Pengikut" @@ -109,14 +103,11 @@ enterEmoji: "Masukkan emoji" renote: "Renote" unrenote: "Hapus renote" renoted: "Telah direnote" -renotedToX: "{name} telah merenote" cantRenote: "Postingan ini tidak dapat direnote" cantReRenote: "Renote tidak dapat direnote" quote: "Kutip" inChannelRenote: "Hanya renote dalam kanal" inChannelQuote: "Hanya kutip dalam kanal" -renoteToChannel: "Renote ke kanal" -renoteToOtherChannel: "Renote ke kanal lainnya" pinnedNote: "Catatan yang disematkan" pinned: "Sematkan ke profil" you: "Kamu" @@ -125,16 +116,10 @@ sensitive: "Konten sensitif" add: "Tambahkan" reaction: "Reaksi" reactions: "Reaksi" -emojiPicker: "Emoji Picker" -pinnedEmojisForReactionSettingDescription: "Atur sematan emoji pada reaksi" -pinnedEmojisSettingDescription: "Atur sematan emoji pada masukan emoji" -emojiPickerDisplay: "Tampilan Emoji Picker" -overwriteFromPinnedEmojisForReaction: "Timpa dari pengaturan reaksi" -overwriteFromPinnedEmojis: "Timpa dari pengaturan umum" +reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi" reactionSettingDescription2: "Geser untuk memindah urutan emoji, klik untuk menghapus, tekan \"+\" untuk menambahkan" rememberNoteVisibility: "Ingat pengaturan visibilitas catatan" attachCancel: "Hapus lampiran" -deleteFile: "Berkas dihapus" markAsSensitive: "Tandai sebagai konten sensitif" unmarkAsSensitive: "Hapus tanda konten sensitif" enterFileName: "Masukkan nama berkas" @@ -144,18 +129,17 @@ renoteMute: "Matikan renote" renoteUnmute: "Batal mematikan renote" block: "Blokir" unblock: "Buka blokir" -suspend: "Tangguhkan" -unsuspend: "Batalkan penangguhan" +suspend: "Bekukan" +unsuspend: "Buka pembekuan" blockConfirm: "Apakah kamu yakin ingin memblokir akun ini?" unblockConfirm: "Apakah kamu yakin ingin membuka blokir akun ini?" -suspendConfirm: "Apakah kamu yakin ingin menangguhkan akun ini?" -unsuspendConfirm: "Apakah kamu yakin ingin membatalkan penangguhan akun ini?" +suspendConfirm: "Apakah kamu yakin ingin membekukan akun ini?" +unsuspendConfirm: "Apakah kamu yakin ingin membuka pembekuan akun ini?" selectList: "Pilih daftar" editList: "Sunting daftar" selectChannel: "Pilih kanal" selectAntenna: "Pilih Antena" editAntenna: "Sunting antena" -createAntenna: "Membuat antena." selectWidget: "Pilih gawit" editWidgets: "Sunting gawit" editWidgetsExit: "Selesai" @@ -168,9 +152,6 @@ addEmoji: "Tambahkan emoji" settingGuide: "Pengaturan rekomendasi" cacheRemoteFiles: "Tembolokkan berkas dari instansi luar" cacheRemoteFilesDescription: "Ketika pengaturan ini dinonaktifkan, berkas dari instansi luar akan dimuat langsung. Menonaktifkan ini akan mengurangi penggunaan penyimpanan peladen, namun dapat menyebabkan peningkatan lalu lintas bandwidth, karena keluku tidak dihasilkan." -youCanCleanRemoteFilesCache: "Kamu dapat mengosongkan tembolok dengan mengeklik tombol 🗑️ pada layar manajemen berkas." -cacheRemoteSensitiveFiles: "Tembolokkan berkas dari instansi luar" -cacheRemoteSensitiveFilesDescription: "Menonaktifkan pengaturan ini menyebabkan berkas sensitif dari instansi luar ditautkan secara langsung, bukan ditembolok." flagAsBot: "Atur akun ini sebagai Bot" flagAsBotDescription: "Jika akun ini dikendalikan oleh program, tetapkanlah opsi ini. Jika diaktifkan, ini akan berfungsi sebagai tanda bagi pengembang lain untuk mencegah interaksi berantai dengan bot lain dan menyesuaikan sistem internal Misskey untuk memperlakukan akun ini sebagai bot." flagAsCat: "Atur akun ini sebagai kucing" @@ -182,10 +163,6 @@ addAccount: "Tambahkan akun" reloadAccountsList: "Muat ulang daftar akun" loginFailed: "Gagal untuk masuk" showOnRemote: "Lihat profil asli" -continueOnRemote: "Lihat di peladen asal" -chooseServerOnMisskeyHub: "Pilih peladen dari Misskey Hub" -specifyServerHost: "Tentukan domain peladen" -inputHostName: "Masukkan nama domain" general: "Umum" wallpaper: "Wallpaper" setWallpaper: "Atur wallpaper" @@ -196,7 +173,6 @@ followConfirm: "Apakah kamu yakin ingin mengikuti {name}?" proxyAccount: "Akun proksi" proxyAccountDescription: "Akun proksi merupakan sebuah akun yang bertindak sebagai pengikut instansi luar untuk pengguna dalam kondisi tertentu. Sebagai contoh, ketika pengguna menambahkan seorang pengguna instansi luar ke dalam daftar, aktivitas dari pengguna instansi luar tidak akan disampaikan ke instansi apabila tidak ada pengguna lokal yang mengikuti pengguna tersebut, dengan begitu akun proksilah yang akan mengikutinya." host: "Host" -selectSelf: "Pilih diri sendiri" selectUser: "Pilih pengguna" recipient: "Penerima" annotation: "Keterangan konten" @@ -211,7 +187,6 @@ perHour: "per Jam" perDay: "per Hari" stopActivityDelivery: "Berhenti mengirim aktivitas" blockThisInstance: "Blokir instansi ini" -silenceThisInstance: "Senyapkan instansi ini" operations: "Tindakan" software: "Perangkat lunak" version: "Versi" @@ -231,9 +206,6 @@ clearCachedFiles: "Hapus tembolok" clearCachedFilesConfirm: "Apakah kamu yakin ingin menghapus seluruh tembolok berkas instansi luar?" blockedInstances: "Instansi terblokir" blockedInstancesDescription: "Daftar nama host dari instansi yang diperlukan untuk diblokir. Instansi yang didaftarkan tidak akan dapat berkomunikasi dengan instansi ini." -silencedInstances: "Instansi yang disenyapkan" -silencedInstancesDescription: "Daftar nama host dari instansi yang ingin kamu senyapkan. Semua akun dari instansi yang terdaftar akan diperlakukan sebagai disenyapkan. Hal ini membuat akun hanya dapat membuat permintaan mengikuti, dan tidak dapat menyebutkan akun lokal apabila tidak mengikuti. Hal ini tidak akan mempengaruhi instansi yang diblokir." -federationAllowedHosts: "Server yang membolehkan federasi" muteAndBlock: "Bisukan / Blokir" mutedUsers: "Pengguna yang dibisukan" blockedUsers: "Pengguna yang diblokir" @@ -241,6 +213,7 @@ noUsers: "Tidak ada pengguna" editProfile: "Sunting profil" noteDeleteConfirm: "Apakah kamu yakin ingin menghapus catatan ini?" pinLimitExceeded: "Kamu tidak dapat menyematkan catatan lagi" +intro: "Instalasi Misskey telah selesai! Mohon untuk membuat pengguna admin." done: "Selesai" processing: "Memproses" preview: "Pratinjau" @@ -250,7 +223,7 @@ noCustomEmojis: "Tidak ada emoji kustom" noJobs: "Tidak ada kerja" federating: "memfederasi" blocked: "Diblokir" -suspended: "Ditangguhkan" +suspended: "Diberhentikan" all: "Semua" subscribing: "Berlangganan" publishing: "Sedang menyiarkan langsung" @@ -277,8 +250,8 @@ removed: "Telah dihapus" removeAreYouSure: "Apakah kamu yakin ingin menghapus \"{x}\"?" deleteAreYouSure: "Apakah kamu yakin ingin menghapus \"{x}\"?" resetAreYouSure: "Yakin mau atur ulang?" -areYouSure: "Apakah kamu yakin?" saved: "Telah disimpan" +messaging: "Pesan" upload: "Unggah" keepOriginalUploading: "Simpan gambar asli" keepOriginalUploadingDescription: "Simpan gambar yang diunggah sebagaimana gambar aslinya. Bila dimatikan, versi tampilan web akan dihasilkan pada saat diunggah." @@ -291,6 +264,7 @@ uploadFromUrlMayTakeTime: "Membutuhkan beberapa waktu hingga pengunggahan selesa explore: "Jelajahi" messageRead: "Telah dibaca" noMoreHistory: "Tidak ada sejarah lagi" +startMessaging: "Mulai mengirim pesan" nUsersRead: "Dibaca oleh {n}" agreeTo: "Saya setuju kepada {0}" agree: "Setuju" @@ -321,15 +295,12 @@ selectFile: "Pilih berkas" selectFiles: "Pilih berkas" selectFolder: "Pilih folder" selectFolders: "Pilih folder" -fileNotSelected: "Tidak ada file yang dipilih" renameFile: "Ubah nama berkas" folderName: "Nama folder" createFolder: "Buat folder" renameFolder: "Ubah nama folder" deleteFolder: "Hapus folder" -folder: "Folder" addFile: "Tambahkan berkas" -showFile: "Tampilkan berkas" emptyDrive: "Drive kosong" emptyFolder: "Folder kosong" unableToDelete: "Tidak dapat menghapus" @@ -342,7 +313,6 @@ copyUrl: "Salin tautan" rename: "Ubah nama" avatar: "Avatar" banner: "Banner" -displayOfSensitiveMedia: "Tampilkan media NSFW" whenServerDisconnected: "Ketika kehilangan koneksi dengan peladen" disconnectedFromServer: "Terputus koneksi dari peladen" reload: "Muat ulang" @@ -372,10 +342,12 @@ enableLocalTimeline: "Nyalakan lini masa lokal" enableGlobalTimeline: "Nyalakan lini masa global" disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua lini masa meskipun lini masa tersebut tidak diaktifkan." registration: "Pendaftaran" +enableRegistration: "Nyalakan pendaftaran pengguna baru" invite: "Undang" driveCapacityPerLocalAccount: "Kapasitas drive per pengguna lokal" driveCapacityPerRemoteAccount: "Kapasitas drive per pengguna remote" inMb: "dalam Megabytes" +iconUrl: "URL Gambar ikon" bannerUrl: "URL Banner" backgroundImageUrl: "URL Gambar latar" basicInfo: "Informasi Umum" @@ -389,11 +361,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Nyalakan hCaptcha" hcaptchaSiteKey: "Site Key" hcaptchaSecretKey: "Secret Key" -mcaptcha: "mCaptcha" -enableMcaptcha: "Nyalakan mCaptcha" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret Key" -mcaptchaInstanceUrl: "URL instansi mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Nyalakan reCAPTCHA" recaptchaSiteKey: "Site key" @@ -409,7 +376,6 @@ name: "Nama" antennaSource: "Sumber Antenna" antennaKeywords: "Kata kunci yang diterima" antennaExcludeKeywords: "Kata kunci yang dikecualikan" -antennaExcludeBots: "Kecualikan akun bot" antennaKeywordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR." notifyAntenna: "Beritahu untuk catatan baru" withFileAntenna: "Hanya tampilkan catatan dengan berkas yang dilampirkan" @@ -420,10 +386,10 @@ withReplies: "Termasuk balasan" connectedTo: "Akun berikut terhubung" notesAndReplies: "Catatan dan balasan" withFiles: "Media" -silence: "Senyapkan" -silenceConfirm: "Apakah kamu yakin ingin menyenyapkan pengguna ini?" -unsilence: "Batalkan senyap" -unsilenceConfirm: "Apakah kamu ingin untuk batal menyenyapkan pengguna ini?" +silence: "Bungkam" +silenceConfirm: "Apakah kamu yakin ingin membungkam pengguna ini?" +unsilence: "Hapus bungkam" +unsilenceConfirm: "Apakah kamu ingin untuk batal membungkam pengguna ini?" popularUsers: "Pengguna populer" recentlyUpdatedUsers: "Pengguna dengan aktivitas terkini" recentlyRegisteredUsers: "Pengguna baru saja bergabung" @@ -437,14 +403,10 @@ aboutMisskey: "Tentang Misskey" administrator: "Admin" token: "Token" 2fa: "Autentikasi 2-faktor" -setupOf2fa: "Atur autentikasi 2-faktor" totp: "Aplikasi autentikator" totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai" moderator: "Moderator" moderation: "Moderasi" -moderationNote: "Catatan moderasi" -addModerationNote: "Tambahkan catatan moderasi" -moderationLogs: "Log moderasi" nUsersMentioned: "{n} pengguna disebut" securityKeyAndPasskey: "Security key dan passkey" securityKey: "Kunci keamanan" @@ -460,13 +422,14 @@ share: "Bagikan" notFound: "Tidak dapat ditemukan" notFoundDescription: "Tidak ada halaman sesuai dengan URL yang ditentukan." uploadFolder: "Lokasi unggah folder bawaan" +cacheClear: "Bersihkan tembolok" markAsReadAllNotifications: "Tandai semua notifikasi telah dibaca" markAsReadAllUnreadNotes: "Tandai semua catatan telah dibaca" markAsReadAllTalkMessages: "Tandai semua pesan telah dibaca" help: "Bantuan" inputMessageHere: "Ketik pesan disini" close: "Tutup" -invites: "Undangan" +invites: "Undang" members: "Anggota" transfer: "Transfer" title: "Judul" @@ -477,10 +440,11 @@ retype: "Masukkan ulang" noteOf: "Catatan milik {user}" quoteAttached: "Dikutip" quoteQuestion: "Apakah kamu ingin menambahkan kutipan?" -attachAsFileQuestion: "Teks dalam papan klip terlalu panjang. Apakah kamu ingin melampirkannya sebagai berkas teks?" +noMessagesYet: "Tidak ada pesan" +newMessageExists: "Kamu mendapatkan pesan baru" onlyOneFileCanBeAttached: "Kamu hanya dapat melampirkan satu berkas ke dalam pesan" signinRequired: "Silahkan login" -invitations: "Undangan" +invitations: "Undang" invitationCode: "Kode undangan" checking: "Memeriksa" available: "Tersedia" @@ -501,10 +465,8 @@ uiLanguage: "Bahasa antarmuka pengguna" aboutX: "Tentang {x}" emojiStyle: "Gaya emoji" native: "Native" -menuStyle: "Gaya menu" -style: "Gaya" +disableDrawer: "Jangan gunakan menu bergaya laci" showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk" -showReactionsCount: "Lihat jumlah reaksi dalam catatan" noHistory: "Tidak ada riwayat" signinHistory: "Riwayat masuk" enableAdvancedMfm: "Nyalakan MFM tingkat lanjut" @@ -538,7 +500,7 @@ showFeaturedNotesInTimeline: "Tampilkan catatan yang diunggulkan di lini masa" objectStorage: "Object Storage" useObjectStorage: "Gunakan object storage" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "Prefix URL digunakan untuk mengonstruksi URL ke object (media) referencing. Tentukan URL jika kamu menggunakan CDN atau Proxy. Jika tidak, tentukan alamat yang dapat diakses secara publik sesuai dengan panduan dari layanan yang akan kamu gunakan. Contohnya: 'https://.s3.amazonaws.com' untuk AWS S3, dan 'https://storage.googleapis.com/' untuk GCS." +objectStorageBaseUrlDesc: "Prefix URL digunakan untuk mengkonstruksi URL ke object (media) referencing. Tentukan URL jika kamu menggunakan CDN atau Proxy, jika tidak tentukan alamat yang dapat diakses secara publik sesuai dengan panduan dari layanan yang akan kamu gunakan, contohnya. 'https://.s3.amazonaws.com' untuk AWS S3, dan 'https://storage.googleapis.com/' untuk GCS." objectStorageBucket: "Bucket" objectStorageBucketDesc: "Mohon tentukan nama bucket yang digunakan pada layanan yang telah dikonfigurasi." objectStoragePrefix: "Prefix" @@ -555,9 +517,8 @@ objectStorageSetPublicRead: "Setel \"public-read\" disaat mengunggah" s3ForcePathStyleDesc: "Jika s3ForcePathStyle dinyalakan, nama bucket harus dimasukkan dalam path URL dan bukan URL nama host tersebut. Kamu perlu menyalakan pengaturan ini jika menggunakan layanan seperti instansi Minio yang self-hosted." serverLogs: "Log Peladen" deleteAll: "Hapus semua" -showFixedPostForm: "Tampilkan form posting di atas lini masa" +showFixedPostForm: "Tampilkan form posting di atas lini masa." showFixedPostFormInChannel: "Tampilkan form posting di atas lini masa (Kanal)" -withRepliesByDefaultForNewlyFollowed: "Termasuk balasan dari pengguna baru yang diikuti pada lini masa secara bawaan" newNoteRecived: "Kamu mendapat catatan baru" sounds: "Bunyi" sound: "Bunyi" @@ -567,8 +528,6 @@ showInPage: "Tampilkan di halaman" popout: "Pop-out" volume: "Volume" masterVolume: "Master volume" -notUseSound: "Tidak ada keluaran suara" -useSoundOnlyWhenActive: "Hanya keluarkan suara jika Misskey sedang aktif" details: "Selengkapnya" chooseEmoji: "Pilih emoji" unableToProcess: "Operasi tersebut tidak dapat diselesaikan." @@ -589,18 +548,14 @@ output: "Keluaran" script: "Script" disablePagesScript: "Nonaktifkan script pada halaman" updateRemoteUser: "Perbaharui informasi pengguna instansi luar" -unsetUserAvatar: "Hapus avatar" -unsetUserAvatarConfirm: "Apakah kamu yakin ingin menghapus avatar?" -unsetUserBanner: "Hapus banner" -unsetUserBannerConfirm: "Apakah kamu yakin ingin menghapus banner?" deleteAllFiles: "Hapus semua berkas" deleteAllFilesConfirm: "Apakah kamu yakin ingin menghapus semua berkas?" removeAllFollowing: "Batalkan mengikuti semua pengguna" removeAllFollowingDescription: "Batal mengikuti semua akun dari {host}. Mohon jalankan ini ketika instansi sudah tidak ada lagi." -userSuspended: "Pengguna ini telah ditangguhkan" -userSilenced: "Pengguna ini telah disenyapkan." -yourAccountSuspendedTitle: "Akun ini ditangguhkan" -yourAccountSuspendedDescription: "Akun ini ditangguhkan karena melanggar ketentuan penggunaan layanan peladen atau semacamnya. Hubungi admin apabila ingin mengetahui alasan lebih lanjut. Mohon untuk tidak membuat akun baru." +userSuspended: "Pengguna ini telah dibekukan." +userSilenced: "Pengguna ini telah dibungkam." +yourAccountSuspendedTitle: "Akun ini dibekukan" +yourAccountSuspendedDescription: "Akun ini dibekukan karena melanggar ketentuan penggunaan layanan peladen atau semacamnya. Hubungi admin apabila ingin tahu alasan lebih lanjut. Mohon untuk tidak membuat akun baru." tokenRevoked: "Token tidak valid" tokenRevokedDescription: "Token ini telah kedaluwarsa. Mohon masuk lagi." accountDeleted: "Akun telah dihapus" @@ -643,7 +598,6 @@ medium: "Sedang" small: "Kecil" generateAccessToken: "Buat token akses" permission: "Izin" -adminPermission: "Wewenang Izin Admin" enableAll: "Aktifkan semua" disableAll: "Nonaktifkan semua" tokenRequested: "Berikan ijin akses ke akun" @@ -665,10 +619,9 @@ smtpSecure: "Gunakan SSL/TLS implisit untuk koneksi SMTP" smtpSecureInfo: "Matikan ini ketika menggunakan STARTTLS" testEmail: "Tes pengiriman surel" wordMute: "Bisukan kata" -hardWordMute: "Pembisuan kata keras" regexpError: "Kesalahan ekspresi reguler" regexpErrorDescription: "Galat terjadi pada baris {line} ekspresi reguler dari {tab} kata yang dibisukan:" -instanceMute: "Bisukan instansi" +instanceMute: "Bisuka instansi" userSaysSomething: "{name} mengatakan sesuatu" makeActive: "Aktifkan" display: "Tampilkan" @@ -687,21 +640,22 @@ useGlobalSettingDesc: "Jika dinyalakan, setelan notifikasi akun kamu akan diguna other: "Lainnya" regenerateLoginToken: "Perbarui token login" regenerateLoginTokenDescription: "Perbarui token yang digunakan secara internal saat login. Normalnya aksi ini tidak diperlukan. Jika diperbarui, semua perangkat akan dilogout." -theKeywordWhenSearchingForCustomEmoji: "Kata kunci ini digunakan untuk mencari emoji kustom yang dicari." setMultipleBySeparatingWithSpace: "Kamu dapat menyetel banyak dengan memisahkannya menggunakan spasi." fileIdOrUrl: "File-ID atau URL" behavior: "Perilaku" sample: "Contoh" abuseReports: "Laporkan" reportAbuse: "Laporkan" -reportAbuseRenote: "Laporkan renote" reportAbuseOf: "Laporkan {name}" fillAbuseReportDescription: "Mohon isi rincian laporan. Jika laporan ini mengenai catatan yang spesifik, mohon lampirkan serta URL catatan tersebut." abuseReported: "Laporan kamu telah dikirimkan. Terima kasih." reporter: "Pelapor" reporteeOrigin: "Yang dilaporkan" reporterOrigin: "Pelapor" +forwardReport: "Teruskan laporan ke instansi luar" +forwardReportIsAnonymous: "Untuk melindungi privasi akun kamu, akun anonim dari sistem akan digunakan sebagai pelapor pada instansi luar." send: "Kirim" +abuseMarkAsResolved: "Tandai laporan sebagai selesai" openInNewTab: "Buka di tab baru" openInSideView: "Buka di tampilan samping" defaultNavigationBehaviour: "Navigasi bawaan" @@ -719,7 +673,6 @@ createNewClip: "Buat klip baru" unclip: "Batalkan klip" confirmToUnclipAlreadyClippedNote: "Catatan ini sudah disertakan di klip \"{name}\". Yakin ingin membatalkan catatan dari klip ini?" public: "Publik" -private: "Tersembunyi" i18nInfo: "Misskey diterjemahkan ke dalam banyak bahasa oleh sukarelawan. Kamu juga dapat ikut membantu menerjemahkannya di {link}." manageAccessTokens: "Kelola token akses" accountInfo: "Informasi akun" @@ -744,7 +697,6 @@ lockedAccountInfo: "Kecuali kamu menyetel visibilitas catatan milikmu ke \"Hanya alwaysMarkSensitive: "Tandai media dalam catatan sebagai media sensitif" loadRawImages: "Tampilkan lampiran gambar secara penuh daripada thumbnail" disableShowingAnimatedImages: "Jangan mainkan gambar bergerak" -highlightSensitiveMedia: "Sorot media sensitif" verificationEmailSent: "Surel verifikasi telah dikirimkan. Mohon akses tautan yang telah disertakan untuk menyelesaikan verifikasi." notSet: "Tidak disetel" emailVerified: "Surel telah diverifikasi" @@ -760,6 +712,7 @@ thisIsExperimentalFeature: "Fitur ini eksperimental. Fungsionalitas dari fitur i developer: "Pengembang" makeExplorable: "Buat akun tampil di \"Jelajahi\"" makeExplorableDescription: "Jika kamu mematikan ini, akun kamu tidak akan muncul di menu \"Jelajahi\"" +showGapBetweenNotesInTimeline: "Tampilkan jarak diantara catatan pada lini masa" duplicate: "Duplikat" left: "Kiri" center: "Tengah" @@ -896,8 +849,8 @@ makeReactionsPublicDescription: "Pengaturan ini akan membuat daftar dari semua r classic: "Klasik" muteThread: "Bisukan thread" unmuteThread: "Suarakan thread" -followingVisibility: "Visibilitas mengikuti" -followersVisibility: "Visibilitas pengikut" +ffVisibility: "Visibilitas Mengikuti/Pengikut" +ffVisibilityDescription: "Mengatur siapa yang dapat melihat pengikutmu dan yang kamu ikuti." continueThread: "Lihat lanjutan thread" deleteAccountConfirm: "Akun akan dihapus. Apakah kamu yakin?" incorrectPassword: "Kata sandi salah." @@ -925,9 +878,6 @@ oneHour: "1 Jam" oneDay: "1 Hari" oneWeek: "1 Bulan" oneMonth: "satu bulan" -threeMonths: "3 bulan" -oneYear: "1 tahun" -threeDays: "3 hari" reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan." failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun" rateLimitExceeded: "Batas sudah terlampaui" @@ -1001,7 +951,6 @@ neverShow: "Jangan tampilkan lagi" remindMeLater: "Mungkin nanti" didYouLikeMisskey: "Apakah kamu mulai menyukai Misskey?" pleaseDonate: "{host} menggunakan perangkat lunak bebas yaitu Misskey. Kami sangat mengapresiasi sekali donasi dari kamu agar pengembangan Misskey tetap dapat berlanjut!" -correspondingSourceIsAvailable: "Sumber kode terkait tersedia di {anchor}" roles: "Peran" role: "Peran" noRole: "Peran tidak temukan" @@ -1011,7 +960,6 @@ assign: "Tetapkan\n" unassign: "Batalkan penetapan" color: "Warna" manageCustomEmojis: "Kelola Emoji Kustom" -manageAvatarDecorations: "Kelola dekorasi avatar" youCannotCreateAnymore: "Kamu melewati batas pembuatan." cannotPerformTemporary: "Sementara Tidak Tersedia" cannotPerformTemporaryDescription: "Aksi ini tidak dapat dilakukan sementara karena melewati batas eksekusi. Mohon tunggu sejenak dan coba lagi." @@ -1034,7 +982,7 @@ internalServerErrorDescription: "Peladen sedang mengalami galat tak terduga" copyErrorInfo: "Salin detil galat" joinThisServer: "Gabung peladen ini" exploreOtherServers: "Cari peladen lain" -letsLookAtTimeline: "LIhat lini masa" +letsLookAtTimeline: "LIhat timeline" disableFederationConfirm: "Matikan federasi?" disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi privat. Umumnya, mematikan federasi tidak diperlukan." disableFederationOk: "Matikan federasi" @@ -1052,11 +1000,6 @@ resetPasswordConfirm: "Yakin untuk mereset kata sandimu?" sensitiveWords: "Kata sensitif" sensitiveWordsDescription: "Visibilitas dari semua catatan mengandung kata yang telah diatur akan dijadikan \"Beranda\" secara otomatis. Kamu dapat mendaftarkan kata tersebut lebih dari satu dengan menuliskannya di baris baru." sensitiveWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler." -prohibitedWords: "Kata yang dilarang" -prohibitedWordsDescription: "Menyalakan kesalahan ketika mencoba untuk memposting catatan dengan set kata-kata yang termasuk. Beberapa kata dapat diatur dan dipisahkan dengan baris baru." -prohibitedWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler." -hiddenTags: "Tagar tersembunyi" -hiddenTagsDescription: "Pilih tanda yang mana akan tidak diperlihatkan dalam daftar tren.\nTanda lebih dari satu dapat didaftarkan dengan tiap baris." notesSearchNotAvailable: "Pencarian catatan tidak tersedia." license: "Lisensi" unfavoriteConfirm: "Yakin ingin menghapusnya dari favorit?" @@ -1068,13 +1011,10 @@ retryAllQueuesConfirmText: "Hal ini akan meningkatkan beban sementara ke peladen enableChartsForRemoteUser: "Buat bagan data pengguna instansi luar" enableChartsForFederatedInstances: "Buat bagan data peladen instansi luar" showClipButtonInNoteFooter: "Tambahkan \"Klip\" ke menu aksi catatan" -reactionsDisplaySize: "Ukuran tampilan reaksi" -limitWidthOfReaction: "Batasi lebar maksimum reaksi dan tampilkan dalam ukuran terbatasi." +largeNoteReactions: "Besarkan reaksi yang ditampilkan" noteIdOrUrl: "ID catatan atau URL" video: "Video" videos: "Video" -audio: "Suara" -audioFiles: "Berkas Suara" dataSaver: "Penghemat data" accountMigration: "Pemindahan akun" accountMoved: "Pengguna ini telah berpindah ke akun baru:" @@ -1102,7 +1042,6 @@ preservedUsernames: "Nama pengguna tercadangkan" preservedUsernamesDescription: "Daftar nama pengguna yang dicadangkan dipisah dengan baris baru. Nama pengguna berikut akan tidak dapat dipakai pada pembuatan akun normal, namun dapat digunakan oleh admin untuk membuat akun baru. Akun yang sudah ada dengan menggunakan nama pengguna ini tidak akan terpengaruh." createNoteFromTheFile: "Buat catatan dari berkas ini" archive: "Arsipkan" -archived: "Diarsipkan" channelArchiveConfirmTitle: "Yakin untuk mengarsipkan {name}?" channelArchiveConfirmDescription: "Kanal yang diarsipkan tidak akan muncul pada daftar kanal atau hasil pencarian. Postingan baru juga tidak dapat ditambahkan lagi." thisChannelArchived: "Kanal ini telah diarsipkan." @@ -1113,7 +1052,6 @@ preventAiLearning: "Tolak penggunaan Pembelajaran Mesin (AI Generatif)" preventAiLearningDescription: "Minta perayap web untuk tidak menggunakan materi teks atau gambar yang telah diposting ke dalam set data Pembelajaran Mesin (Prediktif / Generatif). Hal ini dicapai dengan menambahkan flag HTML-Response \"noai\" ke masing-masing konten. Pencegahan penuh mungkin tidak dapat dicapai dengan flag ini, karena juga dapat diabaikan begitu saja." options: "Opsi peran" specifyUser: "Pengguna spesifik" -openTagPageConfirm: "Apakah ingin membuka laman tagar?" failedToPreviewUrl: "Tidak dapat dipratinjau" update: "Perbarui" rolesThatCanBeUsedThisEmojiAsReaction: "Peran yang dapat menggunakan emoji ini sebagai reaksi" @@ -1128,182 +1066,6 @@ installed: "Terpasang" branding: "Merek" enableServerMachineStats: "Tampilkan informasi mesin peladen menjadi publik" enableIdenticonGeneration: "Nyalakan pembuatan Identicon per pengguna" -turnOffToImprovePerformance: "Matikan untuk tingkatkan performa." -createInviteCode: "Buat kode undangan" -createWithOptions: "Buat dengan opsi" -createCount: "Jumlah undangan" -inviteCodeCreated: "Kode undangan dibuat" -inviteLimitExceeded: "Kamu telah mencapai jumlah maksimum kode undangan yang dapat dibuat." -createLimitRemaining: "Kode undangan yang dapat dibuat: tersisa {limit}" -inviteLimitResetCycle: "Kamu dapat membuat hingga {limit} kode undangan dalam {time}." -expirationDate: "Tanggal kedaluwarsa" -noExpirationDate: "tidak ada tanggal kedaluwarsa" -inviteCodeUsedAt: "Kode undangan digunakan pada" -registeredUserUsingInviteCode: "Undangan digunakan oleh" -waitingForMailAuth: "Menunggu verifikasi surel" -inviteCodeCreator: "Undangan dibuat oleh" -usedAt: "Digunakan pada" -unused: "Tidak digunakan" -used: "Digunakan" -expired: "Kedaluwarsa" -doYouAgree: "Apa kamu setuju?" -beSureToReadThisAsItIsImportant: "Mohon baca informasi penting berikut." -iHaveReadXCarefullyAndAgree: "Saya telah membaca \"{x}\" dan menyetujui." -dialog: "Dialog" -icon: "Avatar" -forYou: "Untuk Anda" -currentAnnouncements: "Pengumuman Saat Ini" -pastAnnouncements: "Pengumuman Terdahulu" -youHaveUnreadAnnouncements: "Terdapat pengumuman yang belum dibaca" -useSecurityKey: "Mohon ikuti instruksi peramban atau perangkat kamu untuk menggunakan kunci pengaman atau passkey." -replies: "Balas" -renotes: "Renote" -loadReplies: "Tampilkan balasan" -loadConversation: "Tampilkan percakapan" -pinnedList: "Daftar yang dipin" -keepScreenOn: "Biarkan layar tetap menyala" -verifiedLink: "Tautan kepemilikan telah diverifikasi" -notifyNotes: "Beritahu mengenai catatan baru" -unnotifyNotes: "Berhenti memberitahu mengenai catatan baru" -authentication: "Autentikasi" -authenticationRequiredToContinue: "Mohon autentikasikan terlebih dahulu sebelum melanjutkan" -dateAndTime: "Tanggal dan Waktu" -showRenotes: "Tampilkan renote" -edited: "Telah disunting" -notificationRecieveConfig: "Pengaturan notifikasi" -mutualFollow: "Saling mengikuti" -followingOrFollower: "Mengikuti atau pengikut" -fileAttachedOnly: "Hanya catatan dengan berkas" -showRepliesToOthersInTimeline: "Tampilkan balasan ke pengguna lain dalam lini masa" -hideRepliesToOthersInTimeline: "Sembunyikan balasan ke orang lain dari lini masa" -showRepliesToOthersInTimelineAll: "Tampilkan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa" -hideRepliesToOthersInTimelineAll: "Sembuyikan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa" -confirmShowRepliesAll: "Operasi ini tidak dapat diubah. Apakah kamu yakin untuk menampilkan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa?" -confirmHideRepliesAll: "Operasi ini tidak dapat diubah. Apakah kamu yakin untuk menyembunyikan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa?" -externalServices: "Layanan eksternal" -sourceCode: "Sumber kode" -sourceCodeIsNotYetProvided: "Sumber kode belum tersedia. Hubungi admin untuk memperbaiki masalah ini." -repositoryUrl: "URL Repositori" -repositoryUrlDescription: "Jika kamu menggunakan Misskey begitu saja (tanpa ada perubahan dalam kode sumber), masukkan https://github.com/misskey-dev/misskey" -repositoryUrlOrTarballRequired: "Apabila kamu masih mempublikasikan repositori, kamu setidaknya harus menyediakan berkas tarball. Lihat .config/example.yml untuk informasi lebih lanjut." -feedback: "Umpan balik" -feedbackUrl: "URL Umpan balik" -impressum: "Impressum" -impressumUrl: "Tautan Impressum" -impressumDescription: "Pada beberapa negara seperti Jerman, inklusi dari informasi kontak operator (sebuah Impressum) diperlukan secara legal untuk situs web komersil." -privacyPolicy: "Kebijakan Privasi" -privacyPolicyUrl: "Tautan Kebijakan Privasi" -tosAndPrivacyPolicy: "Syarat dan Ketentuan serta Kebijakan Privasi" -avatarDecorations: "Dekorasi avatar" -attach: "Lampirkan" -detach: "Hapus" -detachAll: "Lepas Semua" -angle: "Sudut" -flip: "Balik" -showAvatarDecorations: "Tampilkan dekorasi avatar" -releaseToRefresh: "Lepaskan untuk memuat ulang" -refreshing: "Sedang memuat ulang..." -pullDownToRefresh: "Tarik ke bawah untuk memuat ulang" -useGroupedNotifications: "Tampilkan notifikasi secara dikelompokkan" -signupPendingError: "Terdapat masalah ketika memverifikasi alamat surel. Tautan kemungkinan telah kedaluwarsa." -cwNotationRequired: "Jika \"Sembunyikan konten\" diaktifkan, deskripsi harus disediakan." -doReaction: "Tambahkan reaksi" -code: "Kode" -reloadRequiredToApplySettings: "Muat ulang diperlukan untuk menerapkan pengaturan." -remainingN: "Sisa : {n}" -overwriteContentConfirm: "Apakah kamu yakin untuk menimpa konten saat ini?" -seasonalScreenEffect: "Efek layar musiman" -decorate: "Dekor" -addMfmFunction: "Tambahkan dekorasi" -enableQuickAddMfmFunction: "Tampilkan pemilih MFM tingkat lanjut" -bubbleGame: "Bubble Game" -sfx: "Efek Suara" -soundWillBePlayed: "Suara yang akan dimainkan" -showReplay: "Lihat tayangan ulang" -replay: "Tayangan ulang" -replaying: "Menayangkan Ulang" -endReplay: "Keluat dari tayangan ulang" -copyReplayData: "Salin data tayangan ulang" -ranking: "Peringkat" -lastNDays: "{n} hari terakhir" -backToTitle: "Ke Judul" -hemisphere: "Letak kamu tinggal" -withSensitive: "Lampirkan catatan dengan berkas sensitif" -userSaysSomethingSensitive: "Postingan oleh {name} mengandung konten sensitif" -enableHorizontalSwipe: "Geser untuk mengganti tab" -loading: "Memuat..." -surrender: "Batalkan" -gameRetry: "Coba lagi" -notUsePleaseLeaveBlank: "Kosongi bila tidak digunakan" -useTotp: "Gunakan TOTP" -useBackupCode: "Gunakan kode cadangan" -launchApp: "Luncurkan Aplikasi" -useNativeUIForVideoAudioPlayer: "Gunakan antarmuka peramban ketika memainkan video dan audio" -keepOriginalFilename: "Simpan nama berkas asli" -keepOriginalFilenameDescription: "Apabila pengaturan ini dimatikan, nama berkas akan diganti dengan string acak secara otomatis ketika kamu mengunggah berkas." -noDescription: "Tidak ada deskripsi" -alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti" -inquiry: "Hubungi kami" -tryAgain: "Silahkan coba lagi." -createdLists: "Senarai yang dibuat" -createdAntennas: "Antena yang dibuat" -fromX: "Dari {x}" -noteOfThisUser: "Catatan oleh pengguna ini" -clipNoteLimitExceeded: "Klip ini tak bisa ditambahi lagi catatan." -performance: "Kinerja" -modified: "Diubah" -thereAreNChanges: "Ada {n} perubahan" -prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna" -postForm: "Buat catatan" -information: "Informasi" -_chat: - invitations: "Undang" - noHistory: "Tidak ada riwayat" - members: "Anggota" - home: "Beranda" - send: "Kirim" -_settings: - webhook: "Webhook" -_abuseUserReport: - accept: "Setuju" - reject: "Tolak" -_delivery: - status: "Status pengiriman" - stop: "Ditangguhkan" - resume: "Lanjutkan pengiriman" - _type: - none: "Sedang menyiarkan langsung" - manuallySuspended: "Ditangguhkan manual" - goneSuspended: "Sedang ditangguhkan untuk penghapusan peladen" - autoSuspendedForNotResponding: "Sedang ditangguhkan karena peladen tidak menjawab" -_bubbleGame: - howToPlay: "Cara bermain" - hold: "Tahan" - _score: - score: "Skor" - scoreYen: "Jumlah uang didapat" - highScore: "Skor tertinggi" - maxChain: "Jumlah skor berantai" - yen: "{yen} Yen" - estimatedQty: "{qty} buah" - scoreSweets: "{onigiriQtyWithUnit} onigiri" - _howToPlay: - section1: "Atur posisi dan jatuhkan obyek ke dalam kotak." - section2: "Ketika dua obyek menyentuh tipe yang sama satu sama lain, obyek tersebut akan berganti dan kamu mendapatkan poin skor." - section3: "Permainan berakhir jika obyek memenuhi kotak. Capai skor tertinggi dengan menggabungkan obyek bersama sambil menghindari obyek tersebut memenuhi kotak permainan!" -_announcement: - forExistingUsers: "Hanya pengguna yang telah ada" - forExistingUsersDescription: "Pengumuman ini akan dimunculkan ke pengguna yang sudah ada dari titik waktu publikasi jika dinyalakan. Apabila dimatikan, mereka yang baru mendaftar setelah publikasi ini akan juga melihatnya." - needConfirmationToRead: "Membutuhkan konfirmasi terpisah bahwa telah dibaca" - needConfirmationToReadDescription: "Permintaan terpisah untuk mengonfirmasi menandai pengumuman ini telah dibaca akan ditampilkan apabila fitur ini dinyalakan. Pengumuman ini juga akan dikecualikan dari fungsi \"Tandai semua telah dibaca\"." - end: "Arsipkan pengumuman" - tooManyActiveAnnouncementDescription: "Terlalu banyak pengumuman dapat memperburuk pengalaman pengguna. Mohon pertimbangkan untuk mengarsipkan pengumuman yang sudah usang/tidak relevan." - readConfirmTitle: "Tandai telah dibaca?" - readConfirmText: "Aksi ini akan menandai konten dari \"{title}\" telah dibaca." - shouldNotBeUsedToPresentPermanentInfo: "Karena dapat berdampak pada pengalaman pengguna untuk pengguna baru, sangat direkomendasikan untuk menggunakan notifikasi secara mengalir daripada tetap." - dialogAnnouncementUxWarn: "Memiliki dua atau lebih gaya dialog notifikasi secara bersamaan dapat berdampak signifikan pada pengalaman pengguna, mohon untuk menggunakannya dengan hati-hati." - silence: "Tiada notifikasi" - silenceDescription: "Apabila diaktifkan, notifikasi dari pengumuman ini akan dilewatkan dan pengguna tidak perlu membacanya." _initialAccountSetting: accountCreated: "Akun kamu telah sukses dibuat!" letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu." @@ -1316,91 +1078,11 @@ _initialAccountSetting: pushNotificationDescription: "Menyalakan notifikasi dorong akan membuatmu menerima notifikasi dari {name} secara langsung ke perangkatmu." initialAccountSettingCompleted: "Pengaturan profil selesai!" haveFun: "Selamat menikmati, {name}!" - youCanContinueTutorial: "Kamu dapat menjutkan ke tutorial dalam bagaimana menggunakan {name} (Misskey) atau kamu dapat keluar dari pemasangan ini dan langsung menggunakannya segera." - startTutorial: "Mulai Tutorial" + ifYouNeedLearnMore: "Kalau kamu ingin mempelajari lebih lanjut bagaimana cara menggunakan {name} (Misskey), silahkan kunjungi {link}." skipAreYouSure: "Yakin melewati atur profil?" laterAreYouSure: "Yakin banget untuk atur profil nanti?" -_initialTutorial: - launchTutorial: "Lihat Tutorial" - title: "Tutorial" - wellDone: "Kerja bagus!" - skipAreYouSure: "Berhenti dari Tutorial?" - _landing: - title: "Selamat datang di Tutorial" - description: "Di sini kamu dapat mempelajari dasar-dasar dari penggunaan Misskey dan fitur-fiturnya." - _note: - title: "Apa itu Catatan?" - description: "Postingan di Misskey disebut sebagai 'Catatan'. Catatan ditampilkan secara kronologis pada lini masa dan dimutakhirkan secara real-time." - reply: "Klik pada tombol ini untuk membalas ke sebuah pesan. Bisa juga untuk membalas ke sebuah balasan dan melanjutkannya seperti percakapan selayaknya utas." - renote: "Kamu dapat membagikan catatan ke lini masa milikmu. Kamu juga dapat mengutipnya dengan komentarmu." - reaction: "Kamu dapat menambahkan reaksi ke Catatan. Detil lebih lanjut akan dijelaskan di halaman berikutnya." - menu: "Kamu dapat melihat detil catatan, menyalin tautan, dan melakukan aksi lainnya." - _reaction: - title: "Apa itu Reaksi?" - description: "Catatan dapat direaksi dengan berbagai emoji. Reaksi memperbolehkan kamu untuk mengekspresikan nuansa yang tidak dapat disampaikan hanya dengan sebuah \"suka\"." - letsTryReacting: "Reaksi dapat ditambahkan dengan mengklik tombol '+' pada catatan. Coba lakukan mereaksi contoh catatan ini!" - reactToContinue: "Tambahkan reaksi untuk melanjutkan." - reactNotification: "Kamu akan menerima notifikasi real0time ketika seseorang mereaksi catatan kamu." - reactDone: "Kamu dapat mengurungkan reaksi dengan menekan tombol '-'." - _timeline: - title: "Konsep Lini Masa" - description1: "Misskey menyediakan berbagai lini masa sesuai dengan penggunaan (beberapa mungkin tidak tersedia karena bergantung dengan kebijakan peladen)." - home: "Kamu dapat melihat catatan dari akun yang kamu ikuti." - local: "Kamu dapat melihat catatan dari semua pengguna yang ada pada peladen ini." - social: "Catatan dari linimasa Beranda dan Lokal akan ditampilkan." - global: "Kamu dapat melihat catatan dari semua peladen yang terhubung." - description2: "Kamu dapat mengganti linimasa di bagian atas layar kamu kapan saja." - description3: "Sebagai tambahan, terdapat juga linimasa daftar dan linimasa kanal. Untuk detil lebih lanjut, silahkan melihat ke tautan berikut: {link}." - _postNote: - title: "Pengaturan posting Catatan" - description1: "Ketika memposting catatan ke Misskey, terdapat beberapa opsi yang tersedia. Form posting terlihat seperti ini." - _visibility: - description: "Kamu dapat membatasi siapa yang dapat melihat catatan kamu." - public: "Perlihatkan catatan ke semua pengguna." - home: "Hanya publik ke lini masa Beranda. Pengguna yang mengunjungi profilmu melalui pengikut dan renote dapat melihatnya." - followers: "Perlihatkan ke pengikut saja. Hanya pengikut yang dapat melihat postinganmu dan tidak dapat direnote oleh siapapun." - direct: "Hanya perlihatkan ke pengguna spesifik dan penerima akan diberi tahu. Dapat juga digunakan sebagai alternatif dari pesan langsung." - doNotSendConfidencialOnDirect1: "Hati-hati ketika mengirim informasi yang sensitif!" - doNotSendConfidencialOnDirect2: "Admin dari peladen dapat melihat apa yang kamu tulis. Hati-hati dengan informasi sensitif ketika mengirimkan catatan langsung kepada pengguna pada peladen yang tidak dipercaya." - localOnly: "Memposting dengan opsi ini tidak akan memfederasi catatan ke peladen lain. Pengguna pada peladen lain tidak akan dapat melihat catatan ini secara langsung, meskipun dengan pengaturan visibilitas yang sudah diatur di atas." - _cw: - title: "Peringatan Konten (CW)" - description: "Alih-alih isinya, konten yang ditulis dalam kolom 'komentar' akan ditampilkan. Menekan 'Selebihnya' akan menampilkan isi konten." - _exampleNote: - cw: "Peringatan: Bikin Lapar!" - note: "Baru aja makan donat berlapis coklat 🍩😋" - useCases: "Fungsi ini digunakan ketika mengikutik panduan peladen untuk catatan yang dibutuhkan atau untuk membatasi diri dari teks sensitif atau spoiler." - _howToMakeAttachmentsSensitive: - title: "Bagaimana menandai lampiran sebagai sensitif?" - description: "Fungsi ini digunakan untuk lampiran yang dibutuhkan oleh panduan peladen atau sesuatu yang seharusnya tidak boleh dibiarkan begitu saja dengan cara menambahkan penanda \"sensitif\"." - tryThisFile: "Coba tandai gambar yang dilampirkan pada form ini sebagai sensitif!" - _exampleNote: - note: "Ups, kesalahan banget buka penutup wadah natto..." - method: "Untuk menandai lampiran sebagai sensitif, klik gambar pada berkas, buka menu, lalu klik \"Tandai sebagai sensitif\"." - sensitiveSucceeded: "Ketika melampirkan berkas, mohon atur sensitifitas sesuai dengan panduan peladen." - doItToContinue: "Tandai berkas terlampir sebagai sensitif untuk melanjutkan." - _done: - title: "Kamu telah menyelesaikan tutorial! 🎉" - description: "Fungsi yang diperkenalkan di sini merupakan sebagian kecil dari fitur yang ada. Untuk pemahaman lebih detil dalam menggunakan Misskey, kamu dapat merujuk ke {link}." -_timelineDescription: - home: "Pada linimasa Beranda, kamu dapat melihat catatan dari akun yang kamu ikuti." - local: "Pada linimasa Lokal, kamu dapat melihat catatan dari semua pengguna yang ada pada peladen ini." - social: "Linimasa sosial menampilkan catatan dari kedua linimasa Beranda dan Lokal." - global: "Pada linimasa Global, kamu dapat melihat catatan dari semua peladen yang terhubung." _serverRules: description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan." -_serverSettings: - iconUrl: "URL ikon" - appIconDescription: "Tentukan ikon yang digunakan ketika {host} ditampilkan sebagai aplikasi." - appIconUsageExample: "Contoh: Sebagai PWA, atau ketika ditampilkan sebagai markah layar beranda pada ponsel" - appIconStyleRecommendation: "Karena ikon berkemungkinan dipotong menjadi persegi atau lingkaran, ikon dengan margin terwanai di sekeliling konten sangat direkomendasikan." - appIconResolutionMustBe: "Minimum resolusi adalah {resolution}." - manifestJsonOverride: "Ambil alih manifest.json" - shortName: "Nama pendek" - shortNameDescription: "Inisial untuk nama instansi yang dapat ditampilkan apabila nama lengkap resmi terlalu panjang." - fanoutTimelineDescription: "Dapat meningkatkan performa dalam pengambilan data linimasa dan mengurangi beban pada database ketika dinyalakan. Sebagai gantinya, penggunaan memory pada Redis akan meningkan. Pertimbangkan untuk menonaktifkan fitur ini jika mengalami kekurangan memori pada server atau menyebabkan server tidak stabil." - fanoutTimelineDbFallback: "Fallback ke database" - fanoutTimelineDbFallbackDescription: "Ketika diaktifkan, lini masa akan fallback ke database untuk melakukan kueri tambahan apabila linimasa tidak disimpan dalam cache. Menonaktifkan ini dapat mengurangi beban server dengan mengeliminasi proses fallback, namun dapat berakibat membatasi jarak data dari lini masa yang dapat diambil." _accountMigration: moveFrom: "Pindahkan akun lain ke akun ini" moveFromSub: "Buat alias ke akun lain" @@ -1655,19 +1337,6 @@ _achievements: title: "Brain Diver" description: "Posting tautan mengenai Brain Diver" flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "Tes overflow" - description: "Picu tes notifikasi secara berulang dalam waktu yang sangat pendek" - _tutorialCompleted: - title: "Ijazah Sekolah Dasar Misskey" - description: "Tutorial selesai" - _bubbleGameExplodingHead: - title: "🤯" - description: "Obyek paling terbesar di permainan gelembung" - _bubbleGameDoubleExplodingHead: - title: "Ganda 🤯" - description: "Dua dari obyek paling terbesar pada permainan gelembung di waktu yang sama" - flavor: "Kamu dapat mengisi kotak makan siang seperti ini 🤯 🤯." _role: new: "Buat peran" edit: "Sunting peran" @@ -1678,9 +1347,7 @@ _role: assignTarget: "Tipe tugas" descriptionOfAssignTarget: "Manual untuk mengganti secara manual siapa yang mendapatkan peran ini dan siapa yang tidak.\nKondisional untuk pengguna secara otomatis dimasukkan atau dihapus dari peran berdasarkan kondisi yang ditentukan." manual: "Manual" - manualRoles: "Peran manual" conditional: "Kondisional" - conditionalRoles: "Peran kondisional" condition: "Kondisi" isConditionalRole: "Ini adalah peran kondisional" isPublic: "Publikkan Peran" @@ -1708,13 +1375,8 @@ _role: gtlAvailable: "Dapat melihat lini masa global" ltlAvailable: "Dapat melihat lini masa lokal" canPublicNote: "Dapat mengirim catatan publik" - mentionMax: "Jumlah maksimum sebutan dalam sebuah catatan" canInvite: "Dapat membuat kode undangan instansi" - inviteLimit: "Batas jumlah undangan" - inviteLimitCycle: "Interval Penerbitan Kode Undangan" - inviteExpirationTime: "Interval kedaluwarsa undangan" canManageCustomEmojis: "Dapat mengelola Emoji kustom" - canManageAvatarDecorations: "Kelola dekorasi avatar" driveCapacity: "Kapasitas Drive" alwaysMarkNsfw: "Selalu tandai berkas sebagai NSFW" pinMax: "Jumlah maksimal catatan yang disematkan" @@ -1729,19 +1391,9 @@ _role: descriptionOfRateLimitFactor: "Batas kecepatan yang rendah tidak begitu membatasi, batas kecepatan tinggi lebih membatasi. " canHideAds: "Dapat menyembunyikan iklan" canSearchNotes: "Penggunaan pencarian catatan" - canUseTranslator: "Penggunaan penerjemah" - avatarDecorationLimit: "Jumlah maksimum dekorasi avatar yang dapat diterapkan" - canImportAntennas: "Izinkan mengimpor antena" - canImportUserLists: "Izinkan mengimpor senarai" _condition: - roleAssignedTo: "Ditugaskan ke peran manual" isLocal: "Pengguna lokal" isRemote: "Pengguna remote" - isCat: "Pengguna Kucing" - isBot: "Pengguna Bot" - isSuspended: "Pengguna yang ditangguhkan" - isLocked: "Akun privat" - isExplorable: "Pengguna efektif yang akunnya dapat dicari" createdLessThan: "Telah berlalu kurang dari X sejak pembuatan akun" createdMoreThan: "Telah berlalu lebih dari X sejak pembuatan akun" followersLessThanOrEq: "Memiliki pengikut X atau kurang dari tersebut" @@ -1767,9 +1419,8 @@ _emailUnavailable: disposable: "Alamat surel temporer tidak dapat digunakan" mx: "Peladen alamat surel ini tidak valid" smtp: "Peladen alamat surel ini tidak merespon" - banned: "Kamu tidak dapat mendaftar dengan alamat surel ini" _ffVisibility: - public: "Publik" + public: "Terbitkan" followers: "Tampil untuk pengikut saja" private: "Tersembunyi" _signup: @@ -1787,11 +1438,6 @@ _ad: back: "Kembali" reduceFrequencyOfThisAd: "Tampilkan iklan ini lebih sedikit" hide: "Jangan tampilkan" - timezoneinfo: "Hari dalam satu minggu ditentukan dari zona waktu peladen." - adsSettings: "Pengaturan iklan" - notesPerOneAd: "Interval penempatan pemutakhiran iklan secara real-time (catatan per iklan)" - setZeroToDisable: "Atur nilai ini ke 0 untuk menonaktifkan pemutakhiran iklan secara real-time" - adsTooClose: "Interval iklan saat ini kemungkinan memperburuk pengalaman pengguna secara signifikan karena diatur pada nilai yang terlalu rendah." _forgotPassword: enterEmail: "Masukkan alamat surel yang kamu gunakan pada saat mendaftar. Sebuah tautan untuk mengatur ulang kata sandi kamu akan dikirimkan ke alamat surel tersebut." ifNoEmail: "Apabila kamu tidak menggunakan surel pada saat pendaftaran, mohon hubungi admin segera." @@ -1810,8 +1456,6 @@ _plugin: install: "Memasang plugin" installWarn: "Mohon jangan memasang plugin yang tidak dapat dipercayai." manage: "Manajemen plugin" - viewSource: "Lihat sumber" - viewLog: "Tampilkan log" _preferencesBackups: list: "Cadangan yang dibuat" saveNew: "Simpan cadangan baru" @@ -1841,17 +1485,10 @@ _aboutMisskey: contributors: "Kontributor utama" allContributors: "Seluruh kontributor" source: "Sumber kode" - original: "Asli" - thisIsModifiedVersion: "{name} menggunakan versi modifikasi dari Misskey yang asli." translation: "Terjemahkan Misskey" donate: "Donasi ke Misskey" morePatrons: "Kami sangat mengapresiasi dukungan dari banyak penolong lain yang tidak tercantum disini. Terima kasih! 🥰" patrons: "Pendukung" - projectMembers: "Anggota proyek" -_displayOfSensitiveMedia: - respect: "Sembunyikan media yang ditandai sensitif" - ignore: "Tampilkan media yang ditandai sensitif" - force: "Sembunyikan semua media" _instanceTicker: none: "Jangan tampilkan" remote: "Tampilkan untuk pengguna instansi luar" @@ -1872,7 +1509,6 @@ _channel: notesCount: "terdapat {n} catatan" nameAndDescription: "Nama dan deskripsi" nameOnly: "Hanya nama" - allowRenoteToExternal: "Perbolehkan catat ulang dan kutipan di luar dari kanal" _menuDisplay: sideFull: "Horisontal" sideIcon: "Horisontal (Ikon)" @@ -1882,6 +1518,11 @@ _wordMute: muteWords: "Kata yang dibisukan" muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR." muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan ekspresi reguler." + softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari lini masa." + hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke lini masa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke lini masa meskipun jika kondisi tersebut diubah." + soft: "Lembut" + hard: "Keras" + mutedNotes: "Catatan yang dibisukan" _instanceMute: instanceMuteDescription: "Pengaturan ini akan membisukan note/renote apa saja dari instansi yang terdaftar, termasuk pengguna yang membalas pengguna lain dalam instansi yang dibisukan." instanceMuteDescription2: "Pisah dengan baris baru" @@ -1928,6 +1569,7 @@ _theme: header: "Header" navBg: "Latar belakang bilah samping" navFg: "Teks bilah samping" + navHoverFg: "Teks bilah samping (Mengambang)" navActive: "Teks bilah samping (Aktif)" navIndicator: "Indikator bilah samping" link: "Tautan" @@ -1944,27 +1586,30 @@ _theme: infoFg: "Teks informasi" infoWarnBg: "Latar belakang peringatan" infoWarnFg: "Teks peringatan" + cwBg: "Latar belakang tombol Sembunyikan Konten" + cwFg: "Teks tombol Sembunyikan Konten" + cwHoverBg: "Latar belakang tombol Sembunyikan Konten (Mengambang)" toastBg: "Latar belakang notifikasi" toastFg: "Teks notifikasi" buttonBg: "Latar belakang tombol" buttonHoverBg: "Latar belakang tombol (Mengambang)" inputBorder: "Batas bidang masukan" + listItemHoverBg: "Latar belakang daftar item (Mengambang)" + driveFolderBg: "Latar belakang folder drive" + wallpaperOverlay: "Lapisan wallpaper" badge: "Lencana" messageBg: "Latar belakang obrolan" + accentDarken: "Aksen (Gelap)" + accentLighten: "Aksen (Terang)" fgHighlighted: "Teks yang disorot" _sfx: note: "Catatan" noteMy: "Catatan (Saya)" notification: "Notifikasi" - reaction: "Ketika memilih reaksi" -_soundSettings: - driveFile: "Menggunakan berkas audio dalam Drive" - driveFileWarn: "Pilih berkas audio dari Drive" - driveFileTypeWarn: "Berkas ini tidak didukung" - driveFileTypeWarnDescription: "Pilih berkas audio" - driveFileDurationWarn: "Audio ini terlalu panjang" - driveFileDurationWarnDescription: "Audio panjang dapat mengganggu penggunaan Misskey. Masih ingin melanjutkan?" - driveFileError: "Tak bisa memuat audio. Mohon ubah pengaturan" + chat: "Pesan" + chatBg: "Obrolan (Latar Belakang)" + antenna: "Penerimaan Antenna" + channel: "Notifikasi Kanal" _ago: future: "Masa depan" justNow: "Baru saja" @@ -1976,32 +1621,36 @@ _ago: monthsAgo: "{n} bulan lalu" yearsAgo: "{n} tahun lalu" invalid: "Tidak ada sama sekali disini" -_timeIn: - seconds: "dalam {n} detik" - minutes: "dalam {n} menit" - hours: "dalam {n} jam" - days: "dalam {n} hari" - weeks: "dalam {n} minggu" - months: "dalam {n} bulan" - years: "dalam {n} tahun" _time: second: "detik" minute: "menit" hour: "jam" day: "hari" +_timelineTutorial: + title: "Bagaimana cara menggunakan Misskey" + step1_1: "Ini adalah \"lini masa\". Semua \"catatan\" yang dikirimkan oleh {name} akan dimunculkan secara kronologis di sini." + step1_2: "Ada beberapa lini masa yang berbeda. Seperti contoh, \"Lini masa Beranda\" berisi catatan dari pengguna yang kamu ikuti, dan \"Lini masa lokal\" berisi catatan dari semua pengguna dari {name}." + step2_1: "Selanjutnya, mari kita coba memposting sebuah catatan. Kamu dapat melakukanya dengan menekan tombol dengan ikon pensil." + step2_2: "Bagaimana dengan menuliskan sedikit perkenalan diri, atau hanya \"Hello {name}\" kalau kamu lagi ngga feeling?" + step3_1: "Udah selesai memposting catatan pertamamu?" + step3_2: "Catatan pertamamu seharusnya sekarang sudah tampil di lini masa kamu." + step4_1: "Kamu dapat menyisipkan \"Reaksi\" ke dalam catatan." + step4_2: "Untuk menyisipkan reaksi, tekan tanda \"+\" dalam catatan dan pilih emoji yang kamu suka untuk mereaksi catatan tersebut." _2fa: - alreadyRegistered: "Kamu telah mendaftarkan perangkat autentikasi 2-faktor." + alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor." registerTOTP: "Daftarkan aplikasi autentikator" - step1: "Pertama, pasang aplikasi autentikasi (seperti {a} atau {b}) di perangkat kamu." + passwordToTOTP: "Masukkan kata sandimu" + step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu." step2: "Lalu, pindai kode QR yang ada di layar." - step2Uri: "Masukkan URI berikut jika kamu menggunakan program desktop" + step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel." + step2Url: "Di aplikasi desktop, masukkan URL berikut:" step3Title: "Masukkan kode autentikasi" step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan." - setupCompleted: "Penyetelan autentikasi 2-faktor selesai" - step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi autentikasi kamu." + step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi otentikasi kamu." securityKeyNotSupported: "Peramban kamu tidak mendukung security key." registerTOTPBeforeKey: "Mohon atur aplikasi autentikator untuk mendaftarkan security key atau passkey." - securityKeyInfo: "Kamu dapat memasang autentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau autentikasi PIN pada perangkatmu." + securityKeyInfo: "Kamu dapat memasang otentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau otentikasi PIN pada perangkatmu." + chromePasskeyNotSupported: "Passkey Chrome saat ini tidak didukung." registerSecurityKey: "Daftarkan security key atau passkey." securityKeyName: "Masukkan nama key." tapSecurityKey: "Mohon ikuti peramban kamu untuk mendaftarkan security key atau passkey" @@ -2012,12 +1661,6 @@ _2fa: renewTOTPConfirm: "Hal ini akan menyebabkan kode verifikasi dari aplikasi autentikator sebelumnya berhenti bekerja" renewTOTPOk: "Atur ulang" renewTOTPCancel: "Tidak sekarang." - checkBackupCodesBeforeCloseThisWizard: "Sebelum kamu menutup jendela ini, pastikan untuk memperhatikan dan mencadangkan kode cadangan berikut." - backupCodes: "Kode Pencadangan" - backupCodesDescription: "Kamu dapat menggunakan kode ini untuk mendapatkan akses ke akun kamu apabila berada dalam situasi tidak dapat menggunakan aplikasi autentikasi 2-faktor yang kamu miliki. Setiap kode hanya dapat digunakan satu kali. Mohon simpan kode ini di tempat yang aman." - backupCodeUsedWarning: "Kode cadangan telah digunakan. Mohon mengatur ulang autentikasi 2-faktor secepatnya apabila kamu sudah tidak dapat menggunakannya lagi." - backupCodesExhaustedWarning: "Semua kode cadangan telah digunakan. Apabila kamu kehilangan akses pada aplikasi autentikasi 2-faktor milikmu, kamu tidak dapat mengakses akun ini lagi. Mohon atur ulang autentikasi 2-faktor kamu." - moreDetailedGuideHere: "Berikut panduan detilnya" _permissions: "read:account": "Lihat informasi akun" "write:account": "Sunting informasi akun" @@ -2051,59 +1694,6 @@ _permissions: "write:gallery": "Sunting galeri" "read:gallery-likes": "Lihat daftar postingan galeri yang disukai" "write:gallery-likes": "Sunting daftar postingan galeri yang disukai" - "read:flash": "Lihat Play" - "write:flash": "Sunting Play" - "read:flash-likes": "Lihat daftar Play yang disukai" - "write:flash-likes": "Sunting daftar Play yang disukai" - "read:admin:abuse-user-reports": "Lihat laporan pengguna" - "write:admin:delete-account": "Hapus akun pengguna" - "write:admin:delete-all-files-of-a-user": "Hapus semua berkas dari seorang pengguna" - "read:admin:index-stats": "Lihat statistik indeks basis data" - "read:admin:table-stats": "Lihat statistik tabel basis data" - "read:admin:user-ips": "Lihat alamat IP pengguna" - "read:admin:meta": "Lihat metadata instansi" - "write:admin:reset-password": "Atur ulang kata sandi pengguna" - "write:admin:resolve-abuse-user-report": "Selesaikan laporan pengguna" - "write:admin:send-email": "Mengirim surel" - "read:admin:server-info": "Lihat informasi peladen" - "read:admin:show-moderation-log": "Lihat log moderasi" - "read:admin:show-user": "Lihat informasi pengguna privat" - "write:admin:suspend-user": "Tangguhkan pengguna" - "write:admin:unset-user-avatar": "Hapus avatar pengguna" - "write:admin:unset-user-banner": "Hapus banner pengguna" - "write:admin:unsuspend-user": "Batalkan penangguhan pengguna" - "write:admin:meta": "Kelola metadata instansi" - "write:admin:user-note": "Kelola moderasi catatan" - "write:admin:roles": "Kelola peran" - "read:admin:roles": "Lihat peran" - "write:admin:relays": "Kelola relay" - "read:admin:relays": "Lihat relay" - "write:admin:invite-codes": "Kelola kode undangan" - "read:admin:invite-codes": "Lihat kode undangan" - "write:admin:announcements": "Kelola pengumuman" - "read:admin:announcements": "Lihat Pengumuman" - "write:admin:avatar-decorations": "Kelola dekorasi avatar" - "read:admin:avatar-decorations": "Lihat dekorasi avatar" - "write:admin:federation": "Kelola data federasi" - "write:admin:account": "Kelola akun pengguna" - "read:admin:account": "Lihat akun pengguna" - "write:admin:emoji": "Kelola emoji" - "read:admin:emoji": "Lihat emoji" - "write:admin:queue": "Kelola antrian kerja" - "read:admin:queue": "Lihat informasi antrian kerja" - "write:admin:promo": "Kelola catatan promosi" - "write:admin:drive": "Kelola drive pengguna" - "read:admin:drive": "Kelola informasi drive pengguna" - "read:admin:stream": "Gunakan API WebSocket untuk Admin" - "write:admin:ad": "Kelola iklan" - "read:admin:ad": "Lihat iklan" - "write:invite-codes": "Membuat kode undangan" - "read:invite-codes": "Mendapatkan kode undangan" - "write:clip-favorite": "Kelola klip yang difavoritkan" - "read:clip-favorite": "Lihat klip yang difavoritkan" - "read:federation": "Mendapatkan data federasi" - "write:report-abuse": "Melaporkan pelanggaran" - "write:chat": "Buat atau hapus obrolan" _auth: shareAccessTitle: "Mendapatkan ijin akses aplikasi" shareAccess: "Apakah kamu ingin mengijinkan \"{name}\" untuk mengakses akun ini?" @@ -2119,7 +1709,6 @@ _antennaSources: homeTimeline: "Catatan dari pengguna yang diikuti" users: "Catatan dari pengguna tertentu" userList: "Catatan dari daftar tertentu" - userBlacklist: "Semua catatan kecuali untuk satu pengguna atau lebih yang telah ditentukan" _weekday: sunday: "Minggu" monday: "Senin" @@ -2158,7 +1747,6 @@ _widgets: _userList: chooseList: "Pilih daftar" clicker: "Pengeklik" - birthdayFollowings: "Pengguna yang merayakan hari ulang tahunnya hari ini" _cw: hide: "Sembunyikan" show: "Lihat konten" @@ -2220,19 +1808,15 @@ _profile: metadataContent: "Isi" changeAvatar: "Ubah avatar" changeBanner: "Ubah header" - verifiedLinkDescription: "Dengan memasukkan URL yang mengandung tautan ke profil kamu di sini, ikon verifikasi kepemilikan dapat ditampilkan di sebelah kolom ini." - avatarDecorationMax: "Dapat ditambahkan hingga {max} dekorasi." _exportOrImport: allNotes: "Semua catatan" favoritedNotes: "Catatan favorit" - clips: "Klip" followingList: "Ikuti" muteList: "Bisukan" blockingList: "Blokir" userLists: "Daftar" excludeMutingUsers: "Kecualikan pengguna yang dibisukan" excludeInactiveUsers: "Kecualikan pengguna tidak aktif" - withReplies: "Termasuk balasan dari pengguna yang diimpor ke dalam lini masa" _charts: federation: "Federasi" apRequest: "Permintaan" @@ -2279,11 +1863,13 @@ _play: title: "Judul" script: "Script" summary: "Deskripsi" - visibilityDescription: "Membuat catatan ini privat berarti tidak akan terlihat pada profil kamu, namun siapapun yang memiliki URL dari catatan ini akan dapat mengaksesnya." _pages: newPage: "Buat halaman baru" editPage: "Sunting halaman" readPage: "Lihat sumber kode aktif" + created: "Halaman berhasil dibuat" + updated: "Halaman berhasil diperbaharui!" + deleted: "Halaman telah dihapus" pageSetting: "Pengaturan Halaman" nameAlreadyExists: "URL Halaman yang ditentukan sudah ada" invalidNameTitle: "URL Halaman yang ditentukan tidak valid" @@ -2321,8 +1907,6 @@ _pages: section: "Bagian" image: "Gambar" button: "Tombol" - dynamic: "Blok Dinamis" - dynamicDescription: "Blok ini telah dihapus. Mohon gunakan {play} dari sekarang." note: "Catatan yang ditanam" _note: id: "ID Catatan" @@ -2342,23 +1926,11 @@ _notification: youReceivedFollowRequest: "Kamu menerima permintaan mengikuti" yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima" pollEnded: "Hasil Kuesioner telah keluar" - newNote: "Catatan baru" unreadAntennaNote: "Antena {name}" - roleAssigned: "Peran Diberikan" emptyPushNotificationMessage: "Pembaruan notifikasi dorong" achievementEarned: "Pencapaian didapatkan" - testNotification: "Tes notifikasi" - checkNotificationBehavior: "Cek tampilan notifikasi" - sendTestNotification: "Kirim tes notifikasi" - notificationWillBeDisplayedLikeThis: "Notifikasi akan terlihat seperti ini" - reactedBySomeUsers: "{n} orang memberikan reaksi" - likedBySomeUsers: "{n} pengguna menyukai catatan kamu" - renotedBySomeUsers: "{n} orang telah merenote" - followedBySomeUsers: "{n} orang telah mengikuti" - flushNotification: "Bersihkan notifikasi" _types: all: "Semua" - note: "Catatan baru" follow: "Ikuti" mention: "Sebut" reply: "Balasan" @@ -2368,9 +1940,7 @@ _notification: pollEnded: "Jajak pendapat berakhir" receiveFollowRequest: "Permintaan mengikuti diterima" followRequestAccepted: "Permintaan mengikuti disetujui" - roleAssigned: "Peran Diberikan" achievementEarned: "Pencapaian didapatkan" - login: "Masuk" app: "Notifikasi dari aplikasi tertaut" _actions: followBack: "Ikuti Kembali" @@ -2393,9 +1963,6 @@ _deck: introduction: "Buat antarmuka sempurna untukmu dengan menata kolom secara bebas!" introduction2: "Klik \"+\" pada kanan layar untuk menambahkan kolom baru kapanpun yang kamu mau." widgetsIntroduction: "Mohon pilih \"Sunting gawit\" pada menu kolom dan tambahkan gawit." - useSimpleUiForNonRootPages: "Gunakan antarmuka sederhana ke halaman yang dituju" - usedAsMinWidthWhenFlexible: "Lebar minimum akan digunakan untuk ini ketika opsi \"Atur-otomatis lebar\" dinyalakan" - flexible: "Atur-otomatis lebar" _columns: main: "Utama" widgets: "Widget" @@ -2418,9 +1985,9 @@ _drivecleaner: orderByCreatedAtAsc: "Tanggal (Naik)" _webhookSettings: createWebhook: "Buat Webhook" - modifyWebhook: "Sunting Webhook" name: "Nama" secret: "Secret" + events: "Webhook Events" active: "Aktif" _events: follow: "Ketika mengikuti pengguna" @@ -2430,181 +1997,3 @@ _webhookSettings: renote: "Ketika direnote" reaction: "Ketika menerima reaksi" mention: "Ketika sedang disebut" - deleteConfirm: "Apakah kamu yakin ingin menghapus Webhook?" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Surel" - webhook: "Webhook" - keywords: "Kata kunci" -_moderationLogTypes: - createRole: "Peran telah dibuat" - deleteRole: "Peran telah dihapus" - updateRole: "Peran telah diperbaharui" - assignRole: "Yang ditugaskan dalam peran" - unassignRole: "Dihapus dari peran" - suspend: "Tangguhkan" - unsuspend: "Batal ditangguhkan" - addCustomEmoji: "Emoji kustom ditambahkan" - updateCustomEmoji: "Emoji kustom diperbaharui" - deleteCustomEmoji: "Emoji kustom dihapus" - updateServerSettings: "Pengaturan peladen diperbaharui" - updateUserNote: "Catatan moderasi diperbaharui" - deleteDriveFile: "Berkas dihapus" - deleteNote: "Catatan dihapus" - createGlobalAnnouncement: "Pengumuman global dibuat" - createUserAnnouncement: "Pengumuman pengguna dibuat" - updateGlobalAnnouncement: "Pengumuman global diperbaharui" - updateUserAnnouncement: "Pengumuman pengguna diperbaharui" - deleteGlobalAnnouncement: "Pengumuman global telah dihapus" - deleteUserAnnouncement: "Pengumuman pengguna telah dihapus." - resetPassword: "Atur ulang kata sandi" - suspendRemoteInstance: "Instansi luar telah ditangguhkan" - unsuspendRemoteInstance: "Instansi luar batal ditangguhkan" - updateRemoteInstanceNote: "Catatan moderasi telah diperbaharui untuk peladen luar." - markSensitiveDriveFile: "Berkas ditandai sensitif" - unmarkSensitiveDriveFile: "Berkas batal ditandai sensitif" - resolveAbuseReport: "Laporan terselesaikan" - createInvitation: "Buat kode undangan" - createAd: "Iklan telah dibuat" - deleteAd: "Iklan telah dihapus" - updateAd: "Iklan telah diperbaharui" - createAvatarDecoration: "Buat dekorasi avatar" - updateAvatarDecoration: "Perbarui dekorasi avatar" - deleteAvatarDecoration: "Hapus dekorasi avatar" - unsetUserAvatar: "Hapus avatar pengguna" - unsetUserBanner: "Hapus banner pengguna" - deleteAccount: "Akun dihapus" -_fileViewer: - title: "Rincian berkas" - type: "Jenis berkas" - size: "Ukuran berkas" - url: "URL" - uploadedAt: "Diunggah pada" - attachedNotes: "Catatan yang dilampirkan" - thisPageCanBeSeenFromTheAuthor: "Halaman ini hanya dapat dilihat oleh pengguna yang mengunggah bekas ini." -_externalResourceInstaller: - title: "Pasang dari situs eksternal" - checkVendorBeforeInstall: "Pastikan sumber dari sumber daya ini terpercaya sebelum melakukan pemasangan." - _plugin: - title: "Apakah kamu ingin memasang plugin ini?" - _theme: - title: "Apakah kamu ingin memasang tema ini?" - _meta: - base: "Skema warna dasar" - _vendorInfo: - title: "Informasi sumber" - endpoint: "Referensi Endpoint" - hashVerify: "Verifikasi hash" - _errors: - _invalidParams: - title: "Parameter tidak valid" - description: "Tidak cukup informasi untuk memuat data dari situs eksternal. Mohon konfirmasi kembali URL yang dimasukkan." - _resourceTypeNotSupported: - title: "Sumber daya eksternal ini tidak didukung" - description: "Tipe sumber daya eksternal ini tidak didukung. Mohon kontak administrator dari situs tersebut." - _failedToFetch: - title: "Gagal memuat data" - fetchErrorDescription: "Kesalahan terjadi ketika menghubungkan dengan situs eksternal. Jika percobaan kembali tidak dapat memperbaiki masalah ini, mohon hubungi administrator dari situs tersebut." - parseErrorDescription: "Kesalahan terjadi dalam memproses data yang dimuat dari situs eksternal. Mohon hubungi administrator dari situs tersebut." - _hashUnmatched: - title: "Verifikasi data gagal" - description: "Kesalahan terjadi dalam memverifikasi integritas data yang diambil. Sebagai pencegahan keamanan, pemasangan tidak dapat dilanjutkan. Mohon hubungi administrator dari situs tersebut." - _pluginParseFailed: - title: "Kesalahan AiScript" - description: "Data yang diminta telah diambil dengan sukses, namun kesalahan terjadi ketika AiScript melakukan parsing. Mohon hubungi pembuat plugin. Detil kesalahan dapat dilihat pada konsol Javascript." - _pluginInstallFailed: - title: "Pemasangan plugin gagal" - description: "Kesalahan terjadi ketika pemasangan plugin. Mohon coba lagi. Detil kesalahan dapat dilihat pada konsol Javascript." - _themeParseFailed: - title: "Parsing tema gagal" - description: "Data yang diminta telah diambil dengan sukses, namun kesalahan terjadi ketika tema melakukan parsing. Mohon hubungi pembuat tema. Detil kesalahan dapat dilihat pada konsol Javascript." - _themeInstallFailed: - title: "Pemasangan tema gagal" - description: "Kesalahan terjadi ketika pemasangan tema. Mohon coba lagi. Detil kesalahan dapat dilihat pada konsol Javascript." -_dataSaver: - _media: - title: "Memuat media" - description: "Mencegah gambar/video dimuat secara otomatis. Menyembunyikan gambar/video dan akan dimuat ketika diketuk." - _avatar: - title: "Gambar avatar" - description: "Hentikan animasi gambar avatar. Gambar animasi dapat berukuran lebih besar dari gambar biasa, berpotensi pada pengurangan lalu lintas data lebih jauh." - _code: - title: "Penyorotan kode" - description: "Jika notasi penyorotan kode digunakan di MFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data." -_hemisphere: - N: "Bumi belahan utara" - S: "Bumi belahan selatan" - caption: "Digunakan dalam beberapa pengaturan klien untuk menentukan musim." -_reversi: - reversi: "Reversi" - gameSettings: "Pengaturan permainan" - chooseBoard: "Pilih papan" - blackOrWhite: "Hitam/Putih" - blackIs: "{name} bermain sebagai Hitam" - rules: "Aturan" - thisGameIsStartedSoon: "Permainan akan segera dimulai" - waitingForOther: "Menunggu langkah giliran dari lawan" - waitingForMe: "Menungguh langkah giliran dari kamu" - waitingBoth: "Bersiap" - ready: "Siap" - cancelReady: "Belum siap" - opponentTurn: "Giliran lawan" - myTurn: "Giliran kamu" - turnOf: "Giliran {name}" - pastTurnOf: "Giliran {name}" - surrender: "Menyerah" - surrendered: "Telah menyerah" - timeout: "Waktu habis" - drawn: "Seri" - won: "{name} menang" - black: "Hitam" - white: "Putih" - total: "Jumlah" - turnCount: "Langkah ke {count}" - myGames: "Rondeku" - allGames: "Semua ronde" - ended: "Selesai" - playing: "Sedang bermain" - isLlotheo: "Pemain dengan batu yang sedikit menang (Llotheo)" - loopedMap: "Peta melingkar" - canPutEverywhere: "Keping dapat ditaruh dimana saja" - timeLimitForEachTurn: "Batas waktu untuk gantian" - freeMatch: "Pertandingan bebas" - lookingForPlayer: "Mencari lawan..." - gameCanceled: "Permainan ini telah dibatalkan." - shareToTlTheGameWhenStart: "Bagikan permainan ke lini masa ketika dimulai" - iStartedAGame: "Permainan telah dimulai! #MisskeyReversi" - opponentHasSettingsChanged: "Lawan telah mengganti pengaturan mereka." - allowIrregularRules: "Aturan non-reguler (bebas sepenuhnya)" - disallowIrregularRules: "Tanpa aturan non-reguler" - showBoardLabels: "Tampilkan penomoran baris dan kolom pada papan" - useAvatarAsStone: "Ubah batu menjadi avatar pengguna" -_offlineScreen: - title: "Luring - tidak dapat terhubung ke peladen" - header: "Tidak dapat tersambung ke server" -_urlPreviewSetting: - title: "Pengaturan pratinjau URL" - enable: "Aktifkan pratinjau URL" - timeout: "Waktu timeout pratinjau URL (ms)" - timeoutDescription: "Apabila ini memakan waktu lama dari nilai yang ditentukan untuk mendapatkan pratinjau, pratinjau tidak akan dibuat." - maximumContentLength: "Content-Length Maksimum (bytes)" - maximumContentLengthDescription: "Apabila Content-Length lebih besar dari nilai ini, pratinjau tidak akan dibuat." - requireContentLength: "Buat pratinjau hanya ketika Content-Length dapat didapatkan" - requireContentLengthDescription: "Apabila peladen lain tidak memberika Content-Length, pratinjau tidak akan dibuat." - userAgent: "User-Agent" - userAgentDescription: "Atur User-Agent yang digunakan untuk mengambil pratinjau. Apabila dibiarkan kosong, User-Agent bawaan akan digunakan." - summaryProxy: "Titik akhir proksi yang membuat pratinjau" - summaryProxyDescription: "Bukan untuk Misskey, namun untuk menghasilkan pratinjau menggunakan Summaly Proxy." - summaryProxyDescription2: "Parameter berikut tertautkan dengan proksi sebagai string kueri. Apabila proksi tidak mendukung tersebut, nilai di dalamnya diabaikan." -_mediaControls: - pip: "Gambar dalam Gambar" - playbackRate: "Kecepatan Pemutaran" - loop: "Ulangi Pemutaran" -_remoteLookupErrors: - _noSuchObject: - title: "Tidak dapat ditemukan" -_search: - searchScopeAll: "Semua" - searchScopeLocal: "Lokal" - searchScopeUser: "Pengguna spesifik" diff --git a/locales/index.d.ts b/locales/index.d.ts index 73bcb2f1c8..7028417bf3 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1,12028 +1,2163 @@ /* eslint-disable */ // This file is generated by locales/generateDTS.js // Do not edit this file directly. -declare const kParameters: unique symbol; -export interface ParameterizedString { - [kParameters]: T; -} -export interface ILocale { - [_: string]: string | ParameterizedString | ILocale; -} -export interface Locale extends ILocale { - /** - * 日本語 - */ +export interface Locale { "_lang_": string; - /** - * ノートでつながるネットワーク - */ "headlineMisskey": string; - /** - * ようこそ!Misskeyは、オープンソースの分散型マイクロブログサービスです。 - * 「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡 - * 「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍 - * 新しい世界を探検しよう🚀 - */ "introMisskey": string; - /** - * {name}は、オープンソースのプラットフォームMisskeyのサーバーのひとつです。 - */ - "poweredByMisskeyDescription": ParameterizedString<"name">; - /** - * {month}月 {day}日 - */ - "monthAndDay": ParameterizedString<"month" | "day">; - /** - * 検索 - */ + "poweredByMisskeyDescription": string; + "monthAndDay": string; "search": string; - /** - * リセット - */ - "reset": string; - /** - * 通知 - */ "notifications": string; - /** - * ユーザー名 - */ "username": string; - /** - * パスワード - */ "password": string; - /** - * 初期設定開始用パスワード - */ - "initialPasswordForSetup": string; - /** - * 初期設定開始用のパスワードが違います。 - */ - "initialPasswordIsIncorrect": string; - /** - * Misskeyを自分でインストールした場合は、設定ファイルに入力したパスワードを使用してください。 - * Misskeyのホスティングサービスなどを使用している場合は、提供されたパスワードを使用してください。 - * パスワードを設定していない場合は、空欄にしたまま続行してください。 - */ - "initialPasswordForSetupDescription": string; - /** - * パスワードを忘れた - */ "forgotPassword": string; - /** - * 連合に照会中 - */ "fetchingAsApObject": string; - /** - * OK - */ "ok": string; - /** - * わかった - */ "gotIt": string; - /** - * キャンセル - */ "cancel": string; - /** - * やめておく - */ "noThankYou": string; - /** - * ユーザー名を入力 - */ "enterUsername": string; - /** - * {user}がリノート - */ - "renotedBy": ParameterizedString<"user">; - /** - * ノートはありません - */ + "renotedBy": string; "noNotes": string; - /** - * 通知はありません - */ "noNotifications": string; - /** - * サーバー - */ "instance": string; - /** - * 設定 - */ "settings": string; - /** - * 通知の設定 - */ "notificationSettings": string; - /** - * 基本設定 - */ "basicSettings": string; - /** - * その他の設定 - */ "otherSettings": string; - /** - * ウィンドウで開く - */ "openInWindow": string; - /** - * プロフィール - */ "profile": string; - /** - * タイムライン - */ "timeline": string; - /** - * 自己紹介はありません - */ "noAccountDescription": string; - /** - * ログイン - */ "login": string; - /** - * ログイン中 - */ "loggingIn": string; - /** - * ログアウト - */ "logout": string; - /** - * 新規登録 - */ "signup": string; - /** - * アップロード中 - */ "uploading": string; - /** - * 保存 - */ "save": string; - /** - * ユーザー - */ "users": string; - /** - * ユーザーを追加 - */ "addUser": string; - /** - * お気に入り - */ "favorite": string; - /** - * お気に入り - */ "favorites": string; - /** - * お気に入り解除 - */ "unfavorite": string; - /** - * お気に入りに登録しました。 - */ "favorited": string; - /** - * 既にお気に入りに登録されています。 - */ "alreadyFavorited": string; - /** - * お気に入りに登録できませんでした。 - */ "cantFavorite": string; - /** - * ピン留め - */ "pin": string; - /** - * ピン留め解除 - */ "unpin": string; - /** - * 内容をコピー - */ "copyContent": string; - /** - * リンクをコピー - */ "copyLink": string; - /** - * リモートのリンクをコピー - */ - "copyRemoteLink": string; - /** - * リノートのリンクをコピー - */ - "copyLinkRenote": string; - /** - * 削除 - */ "delete": string; - /** - * 削除して編集 - */ "deleteAndEdit": string; - /** - * このノートを削除してもう一度編集しますか?このノートへのリアクション、リノート、返信も全て削除されます。 - */ "deleteAndEditConfirm": string; - /** - * リストに追加 - */ "addToList": string; - /** - * アンテナに追加 - */ "addToAntenna": string; - /** - * メッセージを送信 - */ "sendMessage": string; - /** - * RSSをコピー - */ "copyRSS": string; - /** - * ユーザー名をコピー - */ "copyUsername": string; - /** - * ユーザーIDをコピー - */ "copyUserId": string; - /** - * ノートIDをコピー - */ "copyNoteId": string; - /** - * ファイルIDをコピー - */ "copyFileId": string; - /** - * フォルダーIDをコピー - */ "copyFolderId": string; - /** - * プロフィールURLをコピー - */ "copyProfileUrl": string; - /** - * ユーザーを検索 - */ "searchUser": string; - /** - * ユーザーのノートを検索 - */ - "searchThisUsersNotes": string; - /** - * 返信 - */ "reply": string; - /** - * もっと見る - */ "loadMore": string; - /** - * もっと見る - */ "showMore": string; - /** - * 閉じる - */ "showLess": string; - /** - * フォローされました - */ "youGotNewFollower": string; - /** - * フォローリクエストされました - */ "receiveFollowRequest": string; - /** - * フォローが承認されました - */ "followRequestAccepted": string; - /** - * メンション - */ "mention": string; - /** - * あなた宛て - */ "mentions": string; - /** - * ダイレクト投稿 - */ "directNotes": string; - /** - * インポートとエクスポート - */ "importAndExport": string; - /** - * インポート - */ "import": string; - /** - * エクスポート - */ "export": string; - /** - * ファイル - */ "files": string; - /** - * ダウンロード - */ "download": string; - /** - * ファイル「{name}」を削除しますか?このファイルを使用した一部のコンテンツも削除されます。 - */ - "driveFileDeleteConfirm": ParameterizedString<"name">; - /** - * {name}のフォローを解除しますか? - */ - "unfollowConfirm": ParameterizedString<"name">; - /** - * エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。 - */ + "driveFileDeleteConfirm": string; + "unfollowConfirm": string; "exportRequested": string; - /** - * インポートをリクエストしました。これには時間がかかる場合があります。 - */ "importRequested": string; - /** - * リスト - */ "lists": string; - /** - * リストはありません - */ "noLists": string; - /** - * ノート - */ "note": string; - /** - * ノート - */ "notes": string; - /** - * フォロー - */ "following": string; - /** - * フォロワー - */ "followers": string; - /** - * フォローされています - */ "followsYou": string; - /** - * リスト作成 - */ "createList": string; - /** - * リストの管理 - */ "manageLists": string; - /** - * エラー - */ "error": string; - /** - * 問題が発生しました - */ "somethingHappened": string; - /** - * 再試行 - */ "retry": string; - /** - * ページの読み込みに失敗しました。 - */ "pageLoadError": string; - /** - * これは通常、ネットワークまたはブラウザキャッシュが原因です。キャッシュをクリアするか、しばらく待ってから再度試してください。 - */ "pageLoadErrorDescription": string; - /** - * サーバーの応答がありません。しばらく待ってから再度試してください。 - */ "serverIsDead": string; - /** - * このページを表示するためには、リロードして新しいバージョンのクライアントをご利用ください。 - */ "youShouldUpgradeClient": string; - /** - * リスト名を入力 - */ "enterListName": string; - /** - * プライバシー - */ "privacy": string; - /** - * フォローを承認制にする - */ "makeFollowManuallyApprove": string; - /** - * デフォルトの公開範囲 - */ "defaultNoteVisibility": string; - /** - * フォロー - */ "follow": string; - /** - * フォロー申請 - */ "followRequest": string; - /** - * フォロー申請 - */ "followRequests": string; - /** - * フォロー解除 - */ "unfollow": string; - /** - * フォロー許可待ち - */ "followRequestPending": string; - /** - * 絵文字を入力 - */ "enterEmoji": string; - /** - * リノート - */ "renote": string; - /** - * リノート解除 - */ "unrenote": string; - /** - * リノートしました。 - */ "renoted": string; - /** - * {name} にリノートしました。 - */ - "renotedToX": ParameterizedString<"name">; - /** - * この投稿はリノートできません。 - */ "cantRenote": string; - /** - * リノートをリノートすることはできません。 - */ "cantReRenote": string; - /** - * 引用 - */ "quote": string; - /** - * チャンネル内リノート - */ "inChannelRenote": string; - /** - * チャンネル内引用 - */ "inChannelQuote": string; - /** - * チャンネルにリノート - */ - "renoteToChannel": string; - /** - * 他のチャンネルにリノート - */ - "renoteToOtherChannel": string; - /** - * ピン留めされたノート - */ "pinnedNote": string; - /** - * ピン留め - */ "pinned": string; - /** - * あなた - */ "you": string; - /** - * クリックして表示 - */ "clickToShow": string; - /** - * センシティブ - */ "sensitive": string; - /** - * 追加 - */ "add": string; - /** - * リアクション - */ "reaction": string; - /** - * リアクション - */ "reactions": string; - /** - * 絵文字ピッカー - */ - "emojiPicker": string; - /** - * リアクション時にピン留め表示する絵文字を設定できます - */ - "pinnedEmojisForReactionSettingDescription": string; - /** - * 絵文字入力時にピン留め表示する絵文字を設定できます - */ - "pinnedEmojisSettingDescription": string; - /** - * ピッカーの表示 - */ - "emojiPickerDisplay": string; - /** - * リアクション設定から上書きする - */ - "overwriteFromPinnedEmojisForReaction": string; - /** - * 全般設定から上書きする - */ - "overwriteFromPinnedEmojis": string; - /** - * ドラッグして並び替え、クリックして削除、+を押して追加します。 - */ + "reactionSetting": string; "reactionSettingDescription2": string; - /** - * 公開範囲を記憶する - */ "rememberNoteVisibility": string; - /** - * 添付取り消し - */ "attachCancel": string; - /** - * ファイルを削除 - */ - "deleteFile": string; - /** - * センシティブとして設定 - */ "markAsSensitive": string; - /** - * センシティブを解除する - */ "unmarkAsSensitive": string; - /** - * ファイル名を入力 - */ "enterFileName": string; - /** - * ミュート - */ "mute": string; - /** - * ミュート解除 - */ "unmute": string; - /** - * リノートをミュート - */ "renoteMute": string; - /** - * リノートのミュートを解除 - */ "renoteUnmute": string; - /** - * ブロック - */ "block": string; - /** - * ブロック解除 - */ "unblock": string; - /** - * 凍結 - */ "suspend": string; - /** - * 解凍 - */ "unsuspend": string; - /** - * ブロックしますか? - */ "blockConfirm": string; - /** - * ブロック解除しますか? - */ "unblockConfirm": string; - /** - * 凍結しますか? - */ "suspendConfirm": string; - /** - * 解凍しますか? - */ "unsuspendConfirm": string; - /** - * リストを選択 - */ "selectList": string; - /** - * リストを編集 - */ "editList": string; - /** - * チャンネルを選択 - */ "selectChannel": string; - /** - * アンテナを選択 - */ "selectAntenna": string; - /** - * アンテナを編集 - */ "editAntenna": string; - /** - * アンテナを作成 - */ - "createAntenna": string; - /** - * ウィジェットを選択 - */ "selectWidget": string; - /** - * ウィジェットを編集 - */ "editWidgets": string; - /** - * 編集を終了 - */ "editWidgetsExit": string; - /** - * カスタム絵文字 - */ "customEmojis": string; - /** - * 絵文字 - */ "emoji": string; - /** - * 絵文字 - */ "emojis": string; - /** - * 絵文字名 - */ "emojiName": string; - /** - * 絵文字画像URL - */ "emojiUrl": string; - /** - * 絵文字を追加 - */ "addEmoji": string; - /** - * おすすめ設定 - */ "settingGuide": string; - /** - * リモートのファイルをキャッシュする - */ "cacheRemoteFiles": string; - /** - * この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持します。 - */ "cacheRemoteFilesDescription": string; - /** - * ファイル管理の🗑️ボタンで全てのキャッシュを削除できます。 - */ - "youCanCleanRemoteFilesCache": string; - /** - * リモートのセンシティブなファイルをキャッシュする - */ "cacheRemoteSensitiveFiles": string; - /** - * この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。 - */ "cacheRemoteSensitiveFilesDescription": string; - /** - * Botとして設定 - */ "flagAsBot": string; - /** - * このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。 - */ "flagAsBotDescription": string; - /** - * にゃああああああああああああああ!!!!!!!!!!!! - */ "flagAsCat": string; - /** - * にゃにゃにゃ?? - */ "flagAsCatDescription": string; - /** - * タイムラインにノートへの返信を表示する - */ "flagShowTimelineReplies": string; - /** - * オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。 - */ "flagShowTimelineRepliesDescription": string; - /** - * フォロー中ユーザーからのフォロリクを自動承認 - */ "autoAcceptFollowed": string; - /** - * アカウントを追加 - */ "addAccount": string; - /** - * アカウントリストの情報を更新 - */ "reloadAccountsList": string; - /** - * ログインに失敗しました - */ "loginFailed": string; - /** - * リモートで表示 - */ "showOnRemote": string; - /** - * リモートで続行 - */ - "continueOnRemote": string; - /** - * Misskey Hubからサーバーを選択 - */ - "chooseServerOnMisskeyHub": string; - /** - * サーバーのドメインを直接指定 - */ - "specifyServerHost": string; - /** - * ドメインを入力してください - */ - "inputHostName": string; - /** - * 全般 - */ "general": string; - /** - * 壁紙 - */ "wallpaper": string; - /** - * 壁紙を設定 - */ "setWallpaper": string; - /** - * 壁紙を削除 - */ "removeWallpaper": string; - /** - * 検索: {q} - */ - "searchWith": ParameterizedString<"q">; - /** - * リストがありません - */ + "searchWith": string; "youHaveNoLists": string; - /** - * {name}をフォローしますか? - */ - "followConfirm": ParameterizedString<"name">; - /** - * プロキシアカウント - */ + "followConfirm": string; "proxyAccount": string; - /** - * プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。 - */ "proxyAccountDescription": string; - /** - * ホスト - */ "host": string; - /** - * 自分を選択 - */ - "selectSelf": string; - /** - * ユーザーを選択 - */ "selectUser": string; - /** - * 宛先 - */ "recipient": string; - /** - * 注釈 - */ "annotation": string; - /** - * 連合 - */ "federation": string; - /** - * サーバー - */ "instances": string; - /** - * 初観測 - */ "registeredAt": string; - /** - * 直近のリクエスト受信 - */ "latestRequestReceivedAt": string; - /** - * 直近のステータス - */ "latestStatus": string; - /** - * ストレージ使用量 - */ "storageUsage": string; - /** - * チャート - */ "charts": string; - /** - * 1時間ごと - */ "perHour": string; - /** - * 1日ごと - */ "perDay": string; - /** - * アクティビティの配送を停止 - */ "stopActivityDelivery": string; - /** - * このサーバーをブロック - */ "blockThisInstance": string; - /** - * サーバーをサイレンス - */ - "silenceThisInstance": string; - /** - * サーバーをメディアサイレンス - */ - "mediaSilenceThisInstance": string; - /** - * 操作 - */ "operations": string; - /** - * ソフトウェア - */ "software": string; - /** - * ソフトウェア名 - */ - "softwareName": string; - /** - * バージョン - */ "version": string; - /** - * メタデータ - */ "metadata": string; - /** - * {n}つのファイル - */ - "withNFiles": ParameterizedString<"n">; - /** - * モニター - */ + "withNFiles": string; "monitor": string; - /** - * ジョブキュー - */ "jobQueue": string; - /** - * CPUとメモリ - */ "cpuAndMemory": string; - /** - * ネットワーク - */ "network": string; - /** - * ディスク - */ "disk": string; - /** - * サーバー情報 - */ "instanceInfo": string; - /** - * 統計 - */ "statistics": string; - /** - * キューをクリア - */ "clearQueue": string; - /** - * キューをクリアしますか? - */ "clearQueueConfirmTitle": string; - /** - * 未配達の投稿は配送されなくなります。通常この操作を行う必要はありません。 - */ "clearQueueConfirmText": string; - /** - * キャッシュをクリア - */ "clearCachedFiles": string; - /** - * キャッシュされたリモートファイルをすべて削除しますか? - */ "clearCachedFilesConfirm": string; - /** - * ブロックしたサーバー - */ "blockedInstances": string; - /** - * ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。 - */ "blockedInstancesDescription": string; - /** - * サイレンスしたサーバー - */ - "silencedInstances": string; - /** - * サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。 - */ - "silencedInstancesDescription": string; - /** - * メディアサイレンスしたサーバー - */ - "mediaSilencedInstances": string; - /** - * メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。 - */ - "mediaSilencedInstancesDescription": string; - /** - * 連合を許可するサーバー - */ - "federationAllowedHosts": string; - /** - * 連合を許可するサーバーのホストを改行で区切って設定します。 - */ - "federationAllowedHostsDescription": string; - /** - * ミュートとブロック - */ "muteAndBlock": string; - /** - * ミュートしたユーザー - */ "mutedUsers": string; - /** - * ブロックしたユーザー - */ "blockedUsers": string; - /** - * ユーザーはいません - */ "noUsers": string; - /** - * プロフィールを編集 - */ "editProfile": string; - /** - * このノートを削除しますか? - */ "noteDeleteConfirm": string; - /** - * これ以上ピン留めできません - */ "pinLimitExceeded": string; - /** - * 完了 - */ + "intro": string; "done": string; - /** - * 処理中 - */ "processing": string; - /** - * プレビュー - */ "preview": string; - /** - * デフォルト - */ "default": string; - /** - * デフォルト: {value} - */ - "defaultValueIs": ParameterizedString<"value">; - /** - * 絵文字はありません - */ + "defaultValueIs": string; "noCustomEmojis": string; - /** - * ジョブはありません - */ "noJobs": string; - /** - * 連合中 - */ "federating": string; - /** - * ブロック中 - */ "blocked": string; - /** - * 配信停止 - */ "suspended": string; - /** - * 全て - */ "all": string; - /** - * 購読中 - */ "subscribing": string; - /** - * 配信中 - */ "publishing": string; - /** - * 応答なし - */ "notResponding": string; - /** - * サーバーのフォロー - */ "instanceFollowing": string; - /** - * サーバーのフォロワー - */ "instanceFollowers": string; - /** - * サーバーのユーザー - */ "instanceUsers": string; - /** - * パスワードを変更 - */ "changePassword": string; - /** - * セキュリティ - */ "security": string; - /** - * 入力が一致しません。 - */ "retypedNotMatch": string; - /** - * 現在のパスワード - */ "currentPassword": string; - /** - * 新しいパスワード - */ "newPassword": string; - /** - * 新しいパスワード(再入力) - */ "newPasswordRetype": string; - /** - * ファイルを添付 - */ "attachFile": string; - /** - * もっと! - */ "more": string; - /** - * ハイライト - */ "featured": string; - /** - * ユーザー名かユーザーID - */ "usernameOrUserId": string; - /** - * ユーザーが見つかりません - */ "noSuchUser": string; - /** - * 照会 - */ "lookup": string; - /** - * お知らせ - */ "announcements": string; - /** - * 画像URL - */ "imageUrl": string; - /** - * 削除 - */ "remove": string; - /** - * 削除しました - */ "removed": string; - /** - * 「{x}」を削除しますか? - */ - "removeAreYouSure": ParameterizedString<"x">; - /** - * 「{x}」を削除しますか? - */ - "deleteAreYouSure": ParameterizedString<"x">; - /** - * リセットしますか? - */ + "removeAreYouSure": string; + "deleteAreYouSure": string; "resetAreYouSure": string; - /** - * よろしいですか? - */ - "areYouSure": string; - /** - * 保存しました - */ "saved": string; - /** - * アップロード - */ + "messaging": string; "upload": string; - /** - * オリジナル画像を保持 - */ "keepOriginalUploading": string; - /** - * 画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。 - */ "keepOriginalUploadingDescription": string; - /** - * ドライブから - */ "fromDrive": string; - /** - * URLから - */ "fromUrl": string; - /** - * URLアップロード - */ "uploadFromUrl": string; - /** - * アップロードしたいファイルのURL - */ "uploadFromUrlDescription": string; - /** - * アップロードをリクエストしました - */ "uploadFromUrlRequested": string; - /** - * アップロードが完了するまで時間がかかる場合があります。 - */ "uploadFromUrlMayTakeTime": string; - /** - * {n}個のファイルをアップロード - */ - "uploadNFiles": ParameterizedString<"n">; - /** - * みつける - */ "explore": string; - /** - * 既読 - */ "messageRead": string; - /** - * これより過去の履歴はありません - */ "noMoreHistory": string; - /** - * チャットを始める - */ - "startChat": string; - /** - * {n}人が読みました - */ - "nUsersRead": ParameterizedString<"n">; - /** - * {0}に同意 - */ - "agreeTo": ParameterizedString<"0">; - /** - * 同意する - */ + "startMessaging": string; + "nUsersRead": string; + "agreeTo": string; "agree": string; - /** - * 下記に同意する - */ "agreeBelow": string; - /** - * 基本的な注意事項 - */ "basicNotesBeforeCreateAccount": string; - /** - * 利用規約 - */ "termsOfService": string; - /** - * 始める - */ "start": string; - /** - * ホーム - */ "home": string; - /** - * リモートユーザーのため、情報が不完全です。 - */ "remoteUserCaution": string; - /** - * アクティビティ - */ "activity": string; - /** - * 画像 - */ "images": string; - /** - * 画像 - */ "image": string; - /** - * 誕生日 - */ "birthday": string; - /** - * {age}歳 - */ - "yearsOld": ParameterizedString<"age">; - /** - * 登録日 - */ + "yearsOld": string; "registeredDate": string; - /** - * 場所 - */ "location": string; - /** - * テーマ - */ "theme": string; - /** - * ライトモードで使うテーマ - */ "themeForLightMode": string; - /** - * ダークモードで使うテーマ - */ "themeForDarkMode": string; - /** - * ライト - */ "light": string; - /** - * ダーク - */ "dark": string; - /** - * 明るいテーマ - */ "lightThemes": string; - /** - * 暗いテーマ - */ "darkThemes": string; - /** - * デバイスのダークモードと同期する - */ "syncDeviceDarkMode": string; - /** - * 「{x}」がオンになっています。同期をオフにして手動でモードを切り替えますか? - */ - "switchDarkModeManuallyWhenSyncEnabledConfirm": ParameterizedString<"x">; - /** - * ドライブ - */ "drive": string; - /** - * ファイル名 - */ "fileName": string; - /** - * ファイルを選択 - */ "selectFile": string; - /** - * ファイルを選択 - */ "selectFiles": string; - /** - * フォルダーを選択 - */ "selectFolder": string; - /** - * フォルダーを選択 - */ "selectFolders": string; - /** - * ファイルが選択されていません - */ - "fileNotSelected": string; - /** - * ファイル名を変更 - */ "renameFile": string; - /** - * フォルダー名 - */ "folderName": string; - /** - * フォルダーを作成 - */ "createFolder": string; - /** - * フォルダー名を変更 - */ "renameFolder": string; - /** - * フォルダーを削除 - */ "deleteFolder": string; - /** - * フォルダー - */ - "folder": string; - /** - * ファイルを追加 - */ "addFile": string; - /** - * ファイルを表示 - */ - "showFile": string; - /** - * ドライブは空です - */ "emptyDrive": string; - /** - * フォルダーは空です - */ "emptyFolder": string; - /** - * 削除できません - */ "unableToDelete": string; - /** - * 新しいファイル名を入力してください - */ "inputNewFileName": string; - /** - * 新しいキャプションを入力してください - */ "inputNewDescription": string; - /** - * 新しいフォルダ名を入力してください - */ "inputNewFolderName": string; - /** - * 移動先のフォルダーは、移動するフォルダーのサブフォルダーです。 - */ "circularReferenceFolder": string; - /** - * このフォルダは空でないため、削除できません。 - */ "hasChildFilesOrFolders": string; - /** - * URLをコピー - */ "copyUrl": string; - /** - * 名前を変更 - */ "rename": string; - /** - * アイコン - */ "avatar": string; - /** - * バナー - */ "banner": string; - /** - * センシティブなメディアの表示 - */ "displayOfSensitiveMedia": string; - /** - * サーバーとの接続が失われたとき - */ "whenServerDisconnected": string; - /** - * サーバーから切断されました - */ "disconnectedFromServer": string; - /** - * リロード - */ "reload": string; - /** - * なにもしない - */ "doNothing": string; - /** - * リロードしますか? - */ "reloadConfirm": string; - /** - * ウォッチ - */ "watch": string; - /** - * ウォッチ解除 - */ "unwatch": string; - /** - * 許可 - */ "accept": string; - /** - * 拒否 - */ "reject": string; - /** - * 通常 - */ "normal": string; - /** - * サーバー名 - */ "instanceName": string; - /** - * サーバーの紹介 - */ "instanceDescription": string; - /** - * 管理者の名前 - */ "maintainerName": string; - /** - * 管理者のメールアドレス - */ "maintainerEmail": string; - /** - * 利用規約URL - */ "tosUrl": string; - /** - * 今年 - */ "thisYear": string; - /** - * 今月 - */ "thisMonth": string; - /** - * 今日 - */ "today": string; - /** - * {day}日 - */ - "dayX": ParameterizedString<"day">; - /** - * {month}月 - */ - "monthX": ParameterizedString<"month">; - /** - * {year}年 - */ - "yearX": ParameterizedString<"year">; - /** - * ページ - */ + "dayX": string; + "monthX": string; + "yearX": string; "pages": string; - /** - * 連携 - */ "integration": string; - /** - * 接続する - */ "connectService": string; - /** - * 切断する - */ "disconnectService": string; - /** - * ローカルタイムラインを有効にする - */ "enableLocalTimeline": string; - /** - * グローバルタイムラインを有効にする - */ "enableGlobalTimeline": string; - /** - * これらのタイムラインを無効化しても、利便性のため管理者およびモデレーターは引き続き利用することができます。 - */ "disablingTimelinesInfo": string; - /** - * 登録 - */ "registration": string; - /** - * 招待 - */ + "enableRegistration": string; "invite": string; - /** - * ローカルユーザーひとりあたりのドライブ容量 - */ "driveCapacityPerLocalAccount": string; - /** - * リモートユーザーひとりあたりのドライブ容量 - */ "driveCapacityPerRemoteAccount": string; - /** - * メガバイト単位 - */ "inMb": string; - /** - * バナー画像のURL - */ + "iconUrl": string; "bannerUrl": string; - /** - * 背景画像のURL - */ "backgroundImageUrl": string; - /** - * 基本情報 - */ "basicInfo": string; - /** - * ピン留めユーザー - */ "pinnedUsers": string; - /** - * 「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。 - */ "pinnedUsersDescription": string; - /** - * ピン留めページ - */ "pinnedPages": string; - /** - * サーバーのトップページにピン留めしたいページのパスを改行で区切って記述します。 - */ "pinnedPagesDescription": string; - /** - * ピン留めするクリップのID - */ "pinnedClipId": string; - /** - * ピン留めされたノート - */ "pinnedNotes": string; - /** - * hCaptcha - */ "hcaptcha": string; - /** - * hCaptchaを有効にする - */ "enableHcaptcha": string; - /** - * サイトキー - */ "hcaptchaSiteKey": string; - /** - * シークレットキー - */ "hcaptchaSecretKey": string; - /** - * mCaptcha - */ - "mcaptcha": string; - /** - * mCaptchaを有効にする - */ - "enableMcaptcha": string; - /** - * サイトキー - */ - "mcaptchaSiteKey": string; - /** - * シークレットキー - */ - "mcaptchaSecretKey": string; - /** - * mCaptchaのインスタンスのURL - */ - "mcaptchaInstanceUrl": string; - /** - * reCAPTCHA - */ "recaptcha": string; - /** - * reCAPTCHAを有効にする - */ "enableRecaptcha": string; - /** - * サイトキー - */ "recaptchaSiteKey": string; - /** - * シークレットキー - */ "recaptchaSecretKey": string; - /** - * Turnstile - */ "turnstile": string; - /** - * Turnstileを有効にする - */ "enableTurnstile": string; - /** - * サイトキー - */ "turnstileSiteKey": string; - /** - * シークレットキー - */ "turnstileSecretKey": string; - /** - * 複数のCaptchaを使用すると干渉を起こす可能性があります。他のCaptchaを無効にしますか?キャンセルして複数のCaptchaを有効化したままにすることも可能です。 - */ "avoidMultiCaptchaConfirm": string; - /** - * アンテナ - */ "antennas": string; - /** - * アンテナの管理 - */ "manageAntennas": string; - /** - * 名前 - */ "name": string; - /** - * 受信ソース - */ "antennaSource": string; - /** - * 受信キーワード - */ "antennaKeywords": string; - /** - * 除外キーワード - */ "antennaExcludeKeywords": string; - /** - * Botアカウントを除外 - */ - "antennaExcludeBots": string; - /** - * スペースで区切るとAND指定になり、改行で区切るとOR指定になります - */ "antennaKeywordsDescription": string; - /** - * 新しいノートを通知する - */ "notifyAntenna": string; - /** - * ファイルが添付されたノートのみ - */ "withFileAntenna": string; - /** - * センシティブなチャンネルのノートを除外 - */ - "excludeNotesInSensitiveChannel": string; - /** - * ブラウザへのプッシュ通知を有効にする - */ "enableServiceworker": string; - /** - * ユーザー名を改行で区切って指定します - */ "antennaUsersDescription": string; - /** - * 大文字小文字を区別する - */ "caseSensitive": string; - /** - * 返信を含む - */ "withReplies": string; - /** - * 次のアカウントに接続されています - */ "connectedTo": string; - /** - * 投稿と返信 - */ "notesAndReplies": string; - /** - * ファイル付き - */ "withFiles": string; - /** - * サイレンス - */ "silence": string; - /** - * サイレンスしますか? - */ "silenceConfirm": string; - /** - * サイレンス解除 - */ "unsilence": string; - /** - * サイレンス解除しますか? - */ "unsilenceConfirm": string; - /** - * 人気のユーザー - */ "popularUsers": string; - /** - * 最近投稿したユーザー - */ "recentlyUpdatedUsers": string; - /** - * 最近登録したユーザー - */ "recentlyRegisteredUsers": string; - /** - * 最近発見されたユーザー - */ "recentlyDiscoveredUsers": string; - /** - * {count}のユーザーがいます - */ - "exploreUsersCount": ParameterizedString<"count">; - /** - * Fediverseを探索 - */ + "exploreUsersCount": string; "exploreFediverse": string; - /** - * 人気のタグ - */ "popularTags": string; - /** - * リスト - */ "userList": string; - /** - * 情報 - */ "about": string; - /** - * Misskeyについて - */ "aboutMisskey": string; - /** - * 管理者 - */ "administrator": string; - /** - * 確認コード - */ "token": string; - /** - * 二要素認証 - */ "2fa": string; - /** - * 二要素認証のセットアップ - */ - "setupOf2fa": string; - /** - * 認証アプリ - */ "totp": string; - /** - * 認証アプリを使ってワンタイムパスワードを入力 - */ "totpDescription": string; - /** - * モデレーター - */ "moderator": string; - /** - * モデレーション - */ "moderation": string; - /** - * モデレーションノート - */ - "moderationNote": string; - /** - * モデレーター間でだけ共有されるメモを記入することができます。 - */ - "moderationNoteDescription": string; - /** - * モデレーションノートを追加する - */ - "addModerationNote": string; - /** - * モデログ - */ - "moderationLogs": string; - /** - * {n}人が投稿 - */ - "nUsersMentioned": ParameterizedString<"n">; - /** - * セキュリティキー・パスキー - */ + "nUsersMentioned": string; "securityKeyAndPasskey": string; - /** - * セキュリティキー - */ "securityKey": string; - /** - * 最後の使用 - */ "lastUsed": string; - /** - * 最後の使用: {t} - */ - "lastUsedAt": ParameterizedString<"t">; - /** - * 登録を解除 - */ + "lastUsedAt": string; "unregister": string; - /** - * パスワードレスログイン - */ "passwordLessLogin": string; - /** - * パスワードを使用せず、セキュリティキーやパスキーなどのみでログインします - */ "passwordLessLoginDescription": string; - /** - * パスワードをリセット - */ "resetPassword": string; - /** - * 新しいパスワードは「{password}」です - */ - "newPasswordIs": ParameterizedString<"password">; - /** - * UIのアニメーションを減らす - */ + "newPasswordIs": string; "reduceUiAnimation": string; - /** - * 共有 - */ "share": string; - /** - * 見つかりません - */ "notFound": string; - /** - * 指定されたURLに該当するページはありませんでした。 - */ "notFoundDescription": string; - /** - * 既定アップロード先 - */ "uploadFolder": string; - /** - * すべての通知を既読にする - */ + "cacheClear": string; "markAsReadAllNotifications": string; - /** - * すべての投稿を既読にする - */ "markAsReadAllUnreadNotes": string; - /** - * すべてのチャットを既読にする - */ "markAsReadAllTalkMessages": string; - /** - * ヘルプ - */ "help": string; - /** - * ここにメッセージを入力 - */ "inputMessageHere": string; - /** - * 閉じる - */ "close": string; - /** - * 招待 - */ "invites": string; - /** - * メンバー - */ "members": string; - /** - * 譲渡 - */ "transfer": string; - /** - * タイトル - */ "title": string; - /** - * テキスト - */ "text": string; - /** - * 有効にする - */ "enable": string; - /** - * 次 - */ "next": string; - /** - * 再入力 - */ "retype": string; - /** - * {user}のノート - */ - "noteOf": ParameterizedString<"user">; - /** - * 引用付き - */ + "noteOf": string; "quoteAttached": string; - /** - * 引用として添付しますか? - */ "quoteQuestion": string; - /** - * クリップボードのテキストが長いです。テキストファイルとして添付しますか? - */ - "attachAsFileQuestion": string; - /** - * メッセージに添付できるファイルはひとつです - */ + "noMessagesYet": string; + "newMessageExists": string; "onlyOneFileCanBeAttached": string; - /** - * 続行する前に、登録またはログインが必要です - */ "signinRequired": string; - /** - * 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります - */ - "signinOrContinueOnRemote": string; - /** - * 招待 - */ "invitations": string; - /** - * 招待コード - */ "invitationCode": string; - /** - * 確認しています - */ "checking": string; - /** - * 利用できます - */ "available": string; - /** - * 利用できません - */ "unavailable": string; - /** - * a~z、A~Z、0~9、_が使えます - */ "usernameInvalidFormat": string; - /** - * 短すぎます - */ "tooShort": string; - /** - * 長すぎます - */ "tooLong": string; - /** - * 弱いパスワード - */ "weakPassword": string; - /** - * 普通のパスワード - */ "normalPassword": string; - /** - * 強いパスワード - */ "strongPassword": string; - /** - * 一致しました - */ "passwordMatched": string; - /** - * 一致していません - */ "passwordNotMatched": string; - /** - * {x}でログイン - */ - "signinWith": ParameterizedString<"x">; - /** - * ログインできませんでした。ユーザー名とパスワードを確認してください。 - */ + "signinWith": string; "signinFailed": string; - /** - * もしくは - */ "or": string; - /** - * 言語 - */ "language": string; - /** - * UIの表示言語 - */ "uiLanguage": string; - /** - * {x}について - */ - "aboutX": ParameterizedString<"x">; - /** - * 絵文字のスタイル - */ + "aboutX": string; "emojiStyle": string; - /** - * ネイティブ - */ "native": string; - /** - * メニューのスタイル - */ - "menuStyle": string; - /** - * スタイル - */ - "style": string; - /** - * ドロワー - */ - "drawer": string; - /** - * ポップアップ - */ - "popup": string; - /** - * ノートのアクションをホバー時のみ表示する - */ + "disableDrawer": string; "showNoteActionsOnlyHover": string; - /** - * ノートのリアクション数を表示する - */ - "showReactionsCount": string; - /** - * 履歴はありません - */ "noHistory": string; - /** - * ログイン履歴 - */ "signinHistory": string; - /** - * 高度なMFMを有効にする - */ "enableAdvancedMfm": string; - /** - * 動きのあるMFMを有効にする - */ "enableAnimatedMfm": string; - /** - * やっています - */ "doing": string; - /** - * カテゴリ - */ "category": string; - /** - * タグ - */ "tags": string; - /** - * このドキュメントのソース - */ "docSource": string; - /** - * アカウントを作成 - */ "createAccount": string; - /** - * 既存のアカウント - */ "existingAccount": string; - /** - * 再生成 - */ "regenerate": string; - /** - * フォントサイズ - */ "fontSize": string; - /** - * 画像が1枚のみのメディアリストの高さ - */ "mediaListWithOneImageAppearance": string; - /** - * {x}を上限に - */ - "limitTo": ParameterizedString<"x">; - /** - * フォロー申請はありません - */ + "limitTo": string; "noFollowRequests": string; - /** - * 画像を新しいタブで開く - */ "openImageInNewTab": string; - /** - * ダッシュボード - */ "dashboard": string; - /** - * ローカル - */ "local": string; - /** - * リモート - */ "remote": string; - /** - * 合計 - */ "total": string; - /** - * 前週比 - */ "weekOverWeekChanges": string; - /** - * 前日比 - */ "dayOverDayChanges": string; - /** - * アピアランス - */ "appearance": string; - /** - * クライアント設定 - */ "clientSettings": string; - /** - * アカウント設定 - */ "accountSettings": string; - /** - * プロモーション - */ "promotion": string; - /** - * プロモート - */ "promote": string; - /** - * 日数 - */ "numberOfDays": string; - /** - * このノートを非表示 - */ "hideThisNote": string; - /** - * タイムラインにおすすめのノートを表示する - */ "showFeaturedNotesInTimeline": string; - /** - * オブジェクトストレージ - */ "objectStorage": string; - /** - * オブジェクトストレージを使用 - */ "useObjectStorage": string; - /** - * Base URL - */ "objectStorageBaseUrl": string; - /** - * 参照に使用するURL。CDNやProxyを使用している場合はそのURL、S3: 'https://.s3.amazonaws.com'、GCS等: 'https://storage.googleapis.com/'。 - */ "objectStorageBaseUrlDesc": string; - /** - * Bucket - */ "objectStorageBucket": string; - /** - * 使用サービスのbucket名を指定してください。 - */ "objectStorageBucketDesc": string; - /** - * Prefix - */ "objectStoragePrefix": string; - /** - * このprefixのディレクトリ下に格納されます。 - */ "objectStoragePrefixDesc": string; - /** - * Endpoint - */ "objectStorageEndpoint": string; - /** - * S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。''または':'のように指定します。 - */ "objectStorageEndpointDesc": string; - /** - * Region - */ "objectStorageRegion": string; - /** - * 'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。 - */ "objectStorageRegionDesc": string; - /** - * SSLを使用する - */ "objectStorageUseSSL": string; - /** - * API接続にhttpsを使用しない場合はオフにしてください - */ "objectStorageUseSSLDesc": string; - /** - * Proxyを利用する - */ "objectStorageUseProxy": string; - /** - * API接続にproxyを利用しない場合はオフにしてください - */ "objectStorageUseProxyDesc": string; - /** - * アップロード時に'public-read'を設定する - */ "objectStorageSetPublicRead": string; - /** - * s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。 - */ "s3ForcePathStyleDesc": string; - /** - * サーバーログ - */ "serverLogs": string; - /** - * 全て削除 - */ "deleteAll": string; - /** - * タイムライン上部に投稿フォームを表示する - */ "showFixedPostForm": string; - /** - * タイムライン上部に投稿フォームを表示する(チャンネル) - */ "showFixedPostFormInChannel": string; - /** - * フォローする際、デフォルトで返信をTLに含むようにする - */ - "withRepliesByDefaultForNewlyFollowed": string; - /** - * 新しいノートがあります - */ "newNoteRecived": string; - /** - * 新しいノート - */ - "newNote": string; - /** - * サウンド - */ "sounds": string; - /** - * サウンド - */ "sound": string; - /** - * 通知音の設定 - */ - "notificationSoundSettings": string; - /** - * 聴く - */ "listen": string; - /** - * なし - */ "none": string; - /** - * ページで表示 - */ "showInPage": string; - /** - * ポップアウト - */ "popout": string; - /** - * 音量 - */ "volume": string; - /** - * マスター音量 - */ "masterVolume": string; - /** - * サウンドを出力しない - */ - "notUseSound": string; - /** - * Misskeyがアクティブな時のみサウンドを出力する - */ - "useSoundOnlyWhenActive": string; - /** - * 詳細 - */ "details": string; - /** - * リノートの詳細 - */ - "renoteDetails": string; - /** - * 絵文字を選択 - */ "chooseEmoji": string; - /** - * 操作を完了できません - */ "unableToProcess": string; - /** - * 最近使用 - */ "recentUsed": string; - /** - * インストール - */ "install": string; - /** - * アンインストール - */ "uninstall": string; - /** - * インストールされたアプリ - */ "installedApps": string; - /** - * ありません - */ "nothing": string; - /** - * インストール日時 - */ "installedDate": string; - /** - * 最終使用日時 - */ "lastUsedDate": string; - /** - * 状態 - */ "state": string; - /** - * ソート - */ "sort": string; - /** - * 昇順 - */ "ascendingOrder": string; - /** - * 降順 - */ "descendingOrder": string; - /** - * スクラッチパッド - */ "scratchpad": string; - /** - * スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。 - */ "scratchpadDescription": string; - /** - * UIインスペクター - */ - "uiInspector": string; - /** - * メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。 - */ - "uiInspectorDescription": string; - /** - * 出力 - */ "output": string; - /** - * スクリプト - */ "script": string; - /** - * Pagesのスクリプトを無効にする - */ "disablePagesScript": string; - /** - * リモートユーザー情報の更新 - */ "updateRemoteUser": string; - /** - * アイコンを解除 - */ - "unsetUserAvatar": string; - /** - * アイコンを解除しますか? - */ - "unsetUserAvatarConfirm": string; - /** - * バナーを解除 - */ - "unsetUserBanner": string; - /** - * バナーを解除しますか? - */ - "unsetUserBannerConfirm": string; - /** - * すべてのファイルを削除 - */ "deleteAllFiles": string; - /** - * すべてのファイルを削除しますか? - */ "deleteAllFilesConfirm": string; - /** - * フォローを全解除 - */ "removeAllFollowing": string; - /** - * {host}からのフォローをすべて解除します。そのサーバーがもう存在しなくなった場合などに実行してください。 - */ - "removeAllFollowingDescription": ParameterizedString<"host">; - /** - * このユーザーは凍結されています。 - */ + "removeAllFollowingDescription": string; "userSuspended": string; - /** - * このユーザーはサイレンスされています。 - */ "userSilenced": string; - /** - * アカウントが凍結されています - */ "yourAccountSuspendedTitle": string; - /** - * このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。 - */ "yourAccountSuspendedDescription": string; - /** - * トークンが無効です - */ "tokenRevoked": string; - /** - * ログイントークンが失効しています。ログインし直してください。 - */ "tokenRevokedDescription": string; - /** - * アカウントは削除されています - */ "accountDeleted": string; - /** - * このアカウントは削除されています。 - */ "accountDeletedDescription": string; - /** - * メニュー - */ "menu": string; - /** - * 分割線 - */ "divider": string; - /** - * 項目を追加 - */ "addItem": string; - /** - * 並び替え - */ "rearrange": string; - /** - * リレー - */ "relays": string; - /** - * リレーの追加 - */ "addRelay": string; - /** - * inboxのURL - */ "inboxUrl": string; - /** - * 追加済みのリレー - */ "addedRelays": string; - /** - * プッシュ通知を行うには有効にする必要があります。 - */ "serviceworkerInfo": string; - /** - * 削除された投稿 - */ "deletedNote": string; - /** - * 非公開の投稿 - */ "invisibleNote": string; - /** - * 自動でもっと見る - */ "enableInfiniteScroll": string; - /** - * 公開範囲 - */ "visibility": string; - /** - * アンケート - */ "poll": string; - /** - * 内容を隠す - */ "useCw": string; - /** - * プレイヤーを開く - */ "enablePlayer": string; - /** - * プレイヤーを閉じる - */ "disablePlayer": string; - /** - * ポストを展開する - */ "expandTweet": string; - /** - * テーマエディター - */ "themeEditor": string; - /** - * 説明 - */ "description": string; - /** - * キャプションを付ける - */ "describeFile": string; - /** - * キャプションを入力 - */ "enterFileDescription": string; - /** - * 作者 - */ "author": string; - /** - * 未保存の変更があります。破棄しますか? - */ "leaveConfirm": string; - /** - * 管理 - */ "manage": string; - /** - * プラグイン - */ "plugins": string; - /** - * 設定のバックアップ - */ "preferencesBackups": string; - /** - * デッキ - */ "deck": string; - /** - * デッキ解除 - */ "undeck": string; - /** - * モーダルにぼかし効果を使用 - */ "useBlurEffectForModal": string; - /** - * フル機能リアクションピッカーを使用 - */ "useFullReactionPicker": string; - /** - * 幅 - */ "width": string; - /** - * 高さ - */ "height": string; - /** - * 大 - */ "large": string; - /** - * 中 - */ "medium": string; - /** - * 小 - */ "small": string; - /** - * アクセストークンの発行 - */ "generateAccessToken": string; - /** - * 権限 - */ "permission": string; - /** - * 管理者権限 - */ - "adminPermission": string; - /** - * 全て有効にする - */ "enableAll": string; - /** - * 全て無効にする - */ "disableAll": string; - /** - * アカウントへのアクセス許可 - */ "tokenRequested": string; - /** - * このプラグインはここで設定した権限を行使できるようになります。 - */ "pluginTokenRequestedDescription": string; - /** - * 通知の種類 - */ "notificationType": string; - /** - * 編集 - */ "edit": string; - /** - * メールサーバー - */ "emailServer": string; - /** - * メール配信機能を有効化する - */ "enableEmail": string; - /** - * メールアドレスの確認やパスワードリセットの際に使います - */ "emailConfigInfo": string; - /** - * メール - */ "email": string; - /** - * メールアドレス - */ "emailAddress": string; - /** - * SMTP サーバーの設定 - */ "smtpConfig": string; - /** - * ホスト - */ "smtpHost": string; - /** - * ポート - */ "smtpPort": string; - /** - * ユーザー名 - */ "smtpUser": string; - /** - * パスワード - */ "smtpPass": string; - /** - * ユーザー名とパスワードを空欄にすることで、SMTP認証を無効化出来ます - */ "emptyToDisableSmtpAuth": string; - /** - * SMTP 接続に暗黙的なSSL/TLSを使用する - */ "smtpSecure": string; - /** - * STARTTLS使用時はオフにします。 - */ "smtpSecureInfo": string; - /** - * 配信テスト - */ "testEmail": string; - /** - * ワードミュート - */ "wordMute": string; - /** - * 指定した語句を含むノートを最小化します。最小化されたノートをクリックすることで表示することができます。 - */ - "wordMuteDescription": string; - /** - * ハードワードミュート - */ - "hardWordMute": string; - /** - * ミュートされたワードを表示 - */ - "showMutedWord": string; - /** - * 指定した語句を含むノートを隠します。ワードミュートとは異なり、ノートは完全に表示されなくなります。 - */ - "hardWordMuteDescription": string; - /** - * 正規表現エラー - */ "regexpError": string; - /** - * {tab}ワードミュートの{line}行目の正規表現にエラーが発生しました: - */ - "regexpErrorDescription": ParameterizedString<"tab" | "line">; - /** - * サーバーミュート - */ + "regexpErrorDescription": string; "instanceMute": string; - /** - * {name}が何かを言いました - */ - "userSaysSomething": ParameterizedString<"name">; - /** - * {name}が「{word}」について何かを言いました - */ - "userSaysSomethingAbout": ParameterizedString<"name" | "word">; - /** - * アクティブにする - */ + "userSaysSomething": string; "makeActive": string; - /** - * 表示 - */ "display": string; - /** - * コピー - */ "copy": string; - /** - * クリップボードにコピーされました - */ - "copiedToClipboard": string; - /** - * メトリクス - */ "metrics": string; - /** - * 概要 - */ "overview": string; - /** - * ログ - */ "logs": string; - /** - * 遅延 - */ "delayed": string; - /** - * データベース - */ "database": string; - /** - * チャンネル - */ "channel": string; - /** - * 作成 - */ "create": string; - /** - * 通知設定 - */ "notificationSetting": string; - /** - * 表示する通知の種別を選択してください。 - */ "notificationSettingDesc": string; - /** - * グローバル設定を使う - */ "useGlobalSetting": string; - /** - * オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。 - */ "useGlobalSettingDesc": string; - /** - * その他 - */ "other": string; - /** - * ログイントークンを再生成 - */ "regenerateLoginToken": string; - /** - * ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。 - */ "regenerateLoginTokenDescription": string; - /** - * カスタム絵文字を検索する時のキーワードになります。 - */ - "theKeywordWhenSearchingForCustomEmoji": string; - /** - * スペースで区切って複数設定できます。 - */ "setMultipleBySeparatingWithSpace": string; - /** - * ファイルIDまたはURL - */ "fileIdOrUrl": string; - /** - * 動作 - */ "behavior": string; - /** - * サンプル - */ "sample": string; - /** - * 通報 - */ "abuseReports": string; - /** - * 通報 - */ "reportAbuse": string; - /** - * リノートを通報 - */ - "reportAbuseRenote": string; - /** - * {name}を通報する - */ - "reportAbuseOf": ParameterizedString<"name">; - /** - * 通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。 - */ + "reportAbuseOf": string; "fillAbuseReportDescription": string; - /** - * 内容が送信されました。ご報告ありがとうございました。 - */ "abuseReported": string; - /** - * 通報者 - */ "reporter": string; - /** - * 通報先 - */ "reporteeOrigin": string; - /** - * 通報元 - */ "reporterOrigin": string; - /** - * 送信 - */ + "forwardReport": string; + "forwardReportIsAnonymous": string; "send": string; - /** - * 新しいタブで開く - */ + "abuseMarkAsResolved": string; "openInNewTab": string; - /** - * サイドビューで開く - */ "openInSideView": string; - /** - * デフォルトのナビゲーション - */ "defaultNavigationBehaviour": string; - /** - * これらの設定を編集するとアカウントが破損する可能性があります。 - */ "editTheseSettingsMayBreakAccount": string; - /** - * ノートのサーバー情報 - */ "instanceTicker": string; - /** - * {x}を待っています - */ - "waitingFor": ParameterizedString<"x">; - /** - * ランダム - */ + "waitingFor": string; "random": string; - /** - * システム - */ "system": string; - /** - * UI切り替え - */ "switchUi": string; - /** - * デスクトップ - */ "desktop": string; - /** - * クリップ - */ "clip": string; - /** - * 新規作成 - */ "createNew": string; - /** - * 任意 - */ "optional": string; - /** - * 新しいクリップを作成 - */ "createNewClip": string; - /** - * クリップ解除 - */ "unclip": string; - /** - * このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか? - */ - "confirmToUnclipAlreadyClippedNote": ParameterizedString<"name">; - /** - * パブリック - */ + "confirmToUnclipAlreadyClippedNote": string; "public": string; - /** - * 非公開 - */ - "private": string; - /** - * Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。 - */ - "i18nInfo": ParameterizedString<"link">; - /** - * アクセストークンの管理 - */ + "i18nInfo": string; "manageAccessTokens": string; - /** - * アカウント情報 - */ "accountInfo": string; - /** - * ノートの数 - */ "notesCount": string; - /** - * 返信した数 - */ "repliesCount": string; - /** - * リノートした数 - */ "renotesCount": string; - /** - * 返信された数 - */ "repliedCount": string; - /** - * リノートされた数 - */ "renotedCount": string; - /** - * フォロー数 - */ "followingCount": string; - /** - * フォロワー数 - */ "followersCount": string; - /** - * リアクションした数 - */ "sentReactionsCount": string; - /** - * リアクションされた数 - */ "receivedReactionsCount": string; - /** - * アンケートに投票した数 - */ "pollVotesCount": string; - /** - * アンケートに投票された数 - */ "pollVotedCount": string; - /** - * はい - */ "yes": string; - /** - * いいえ - */ "no": string; - /** - * ドライブのファイル数 - */ "driveFilesCount": string; - /** - * ドライブ使用量 - */ "driveUsage": string; - /** - * クローラーによるインデックスを拒否 - */ "noCrawle": string; - /** - * 外部の検索エンジンにあなたのユーザーページ、ノート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。 - */ "noCrawleDescription": string; - /** - * フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。 - */ "lockedAccountInfo": string; - /** - * デフォルトでメディアをセンシティブ設定にする - */ "alwaysMarkSensitive": string; - /** - * 添付画像のサムネイルをオリジナル画質にする - */ "loadRawImages": string; - /** - * アニメーション画像を再生しない - */ "disableShowingAnimatedImages": string; - /** - * メディアがセンシティブであることを分かりやすく表示 - */ - "highlightSensitiveMedia": string; - /** - * 確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。 - */ "verificationEmailSent": string; - /** - * 未設定 - */ "notSet": string; - /** - * メールアドレスが確認されました - */ "emailVerified": string; - /** - * お気に入りノートの数 - */ "noteFavoritesCount": string; - /** - * Pageにいいねした数 - */ "pageLikesCount": string; - /** - * Pageにいいねされた数 - */ "pageLikedCount": string; - /** - * 連絡先 - */ "contact": string; - /** - * システムのデフォルトのフォントを使う - */ "useSystemFont": string; - /** - * クリップ - */ "clips": string; - /** - * 実験的機能 - */ "experimentalFeatures": string; - /** - * 実験的 - */ "experimental": string; - /** - * これは実験的な機能です。仕様が変更されたり、正常に動作しなかったりする可能性があります。 - */ "thisIsExperimentalFeature": string; - /** - * 開発者 - */ "developer": string; - /** - * アカウントを見つけやすくする - */ "makeExplorable": string; - /** - * オフにすると、「みつける」にアカウントが載らなくなります。 - */ "makeExplorableDescription": string; - /** - * 複製 - */ + "showGapBetweenNotesInTimeline": string; "duplicate": string; - /** - * 左 - */ "left": string; - /** - * 中央 - */ "center": string; - /** - * 広い - */ "wide": string; - /** - * 狭い - */ "narrow": string; - /** - * 設定はページリロード後に反映されます。 - */ "reloadToApplySetting": string; - /** - * 反映には再起動が必要です。 - */ "needReloadToApply": string; - /** - * 反映にはサーバーの再起動が必要です。 - */ - "needToRestartServerToApply": string; - /** - * タイトルバーを表示する - */ "showTitlebar": string; - /** - * キャッシュをクリア - */ "clearCache": string; - /** - * {n}人がオンライン - */ - "onlineUsersCount": ParameterizedString<"n">; - /** - * {n}ユーザー - */ - "nUsers": ParameterizedString<"n">; - /** - * {n}ノート - */ - "nNotes": ParameterizedString<"n">; - /** - * エラーリポートを送信 - */ + "onlineUsersCount": string; + "nUsers": string; + "nNotes": string; "sendErrorReports": string; - /** - * オンにすると、問題が発生したときにエラーの詳細情報がMisskeyに共有され、ソフトウェアの品質向上に役立てることができます。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴などが含まれます。 - */ "sendErrorReportsDescription": string; - /** - * マイテーマ - */ "myTheme": string; - /** - * 背景 - */ "backgroundColor": string; - /** - * アクセント - */ "accentColor": string; - /** - * 文字 - */ "textColor": string; - /** - * 名前を付けて保存 - */ "saveAs": string; - /** - * 高度 - */ "advanced": string; - /** - * 高度な設定 - */ "advancedSettings": string; - /** - * 値 - */ "value": string; - /** - * 作成日時 - */ "createdAt": string; - /** - * 更新日時 - */ "updatedAt": string; - /** - * 保存しますか? - */ "saveConfirm": string; - /** - * 削除しますか? - */ "deleteConfirm": string; - /** - * 有効な値ではありません。 - */ "invalidValue": string; - /** - * レジストリ - */ "registry": string; - /** - * アカウントを閉鎖する - */ "closeAccount": string; - /** - * 現在のバージョン - */ "currentVersion": string; - /** - * 最新のバージョン - */ "latestVersion": string; - /** - * お使いのクライアントは最新です。 - */ "youAreRunningUpToDateClient": string; - /** - * 新しいバージョンのクライアントが利用可能です。 - */ "newVersionOfClientAvailable": string; - /** - * 使用量 - */ "usageAmount": string; - /** - * 容量 - */ "capacity": string; - /** - * 使用中 - */ "inUse": string; - /** - * コードを編集 - */ "editCode": string; - /** - * 適用 - */ "apply": string; - /** - * サーバーからのお知らせを受け取る - */ "receiveAnnouncementFromInstance": string; - /** - * メール通知 - */ "emailNotification": string; - /** - * 公開 - */ "publish": string; - /** - * チャンネル内検索 - */ "inChannelSearch": string; - /** - * 右クリックでリアクションピッカーを開く - */ "useReactionPickerForContextMenu": string; - /** - * {users}が入力中 - */ - "typingUsers": ParameterizedString<"users">; - /** - * 特定の日付にジャンプ - */ + "typingUsers": string; "jumpToSpecifiedDate": string; - /** - * 過去のタイムラインを表示しています - */ "showingPastTimeline": string; - /** - * クリア - */ "clear": string; - /** - * 全て既読にする - */ "markAllAsRead": string; - /** - * 戻る - */ "goBack": string; - /** - * いいね解除しますか? - */ "unlikeConfirm": string; - /** - * フルビュー - */ "fullView": string; - /** - * フルビュー解除 - */ "quitFullView": string; - /** - * 説明を追加 - */ "addDescription": string; - /** - * 個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。 - */ "userPagePinTip": string; - /** - * 宛先に含まれていないメンションがあります - */ "notSpecifiedMentionWarning": string; - /** - * 情報 - */ "info": string; - /** - * ユーザー情報 - */ "userInfo": string; - /** - * 不明 - */ "unknown": string; - /** - * オンライン状態 - */ "onlineStatus": string; - /** - * オンライン状態を隠す - */ "hideOnlineStatus": string; - /** - * オンライン状態を隠すと、検索などの一部機能において利便性が低下することがあります。 - */ "hideOnlineStatusDescription": string; - /** - * オンライン - */ "online": string; - /** - * アクティブ - */ "active": string; - /** - * オフライン - */ "offline": string; - /** - * 非推奨 - */ "notRecommended": string; - /** - * Botプロテクション - */ "botProtection": string; - /** - * サーバーブロック・サイレンス - */ "instanceBlocking": string; - /** - * アカウントを選択 - */ "selectAccount": string; - /** - * アカウントを切り替え - */ "switchAccount": string; - /** - * 有効 - */ "enabled": string; - /** - * 無効 - */ "disabled": string; - /** - * クイックアクション - */ "quickAction": string; - /** - * ユーザー - */ "user": string; - /** - * 管理 - */ "administration": string; - /** - * アカウント - */ "accounts": string; - /** - * 切り替え - */ "switch": string; - /** - * 管理者情報が設定されていません。 - */ "noMaintainerInformationWarning": string; - /** - * 問い合わせ先URLが設定されていません。 - */ - "noInquiryUrlWarning": string; - /** - * Botプロテクションが設定されていません。 - */ "noBotProtectionWarning": string; - /** - * 設定する - */ "configure": string; - /** - * ギャラリーへ投稿 - */ "postToGallery": string; - /** - * このハッシュタグで投稿 - */ "postToHashtag": string; - /** - * ギャラリー - */ "gallery": string; - /** - * 最近の投稿 - */ "recentPosts": string; - /** - * 人気の投稿 - */ "popularPosts": string; - /** - * ノートで共有 - */ "shareWithNote": string; - /** - * 広告 - */ "ads": string; - /** - * 期限 - */ "expiration": string; - /** - * 開始期間 - */ "startingperiod": string; - /** - * メモ - */ "memo": string; - /** - * 優先度 - */ "priority": string; - /** - * 高 - */ "high": string; - /** - * 中 - */ "middle": string; - /** - * 低 - */ "low": string; - /** - * メールアドレスの設定がされていません。 - */ "emailNotConfiguredWarning": string; - /** - * 比率 - */ "ratio": string; - /** - * 本文をプレビュー - */ "previewNoteText": string; - /** - * カスタムCSS - */ "customCss": string; - /** - * この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。 - */ "customCssWarn": string; - /** - * グローバル - */ "global": string; - /** - * アイコンを四角形で表示 - */ "squareAvatars": string; - /** - * 送信 - */ "sent": string; - /** - * 受信 - */ "received": string; - /** - * 検索結果 - */ "searchResult": string; - /** - * ハッシュタグ - */ "hashtags": string; - /** - * トラブルシューティング - */ "troubleshooting": string; - /** - * UIにぼかし効果を使用 - */ "useBlurEffect": string; - /** - * 詳しく - */ "learnMore": string; - /** - * Misskeyが更新されました! - */ "misskeyUpdated": string; - /** - * 更新情報を見る - */ "whatIsNew": string; - /** - * 翻訳 - */ "translate": string; - /** - * {x}から翻訳 - */ - "translatedFrom": ParameterizedString<"x">; - /** - * アカウントの削除が進行中です - */ + "translatedFrom": string; "accountDeletionInProgress": string; - /** - * サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。 - */ "usernameInfo": string; - /** - * 藍モード - */ "aiChanMode": string; - /** - * 開発者モード - */ "devMode": string; - /** - * CWを維持する - */ "keepCw": string; - /** - * Pub/Subのアカウント - */ "pubSub": string; - /** - * 直近の通信 - */ "lastCommunication": string; - /** - * 解決済み - */ "resolved": string; - /** - * 未解決 - */ "unresolved": string; - /** - * フォロワーを解除 - */ "breakFollow": string; - /** - * フォロワー解除しますか? - */ "breakFollowConfirm": string; - /** - * オンになっています - */ "itsOn": string; - /** - * オフになっています - */ "itsOff": string; - /** - * オン - */ "on": string; - /** - * オフ - */ "off": string; - /** - * アカウント登録にメールアドレスを必須にする - */ "emailRequiredForSignup": string; - /** - * 未読 - */ "unread": string; - /** - * フィルタ - */ "filter": string; - /** - * コントロールパネル - */ "controlPanel": string; - /** - * アカウントを管理 - */ "manageAccounts": string; - /** - * リアクション一覧を公開する - */ "makeReactionsPublic": string; - /** - * あなたがしたリアクション一覧を誰でも見れるようにします。 - */ "makeReactionsPublicDescription": string; - /** - * クラシック - */ "classic": string; - /** - * スレッドをミュート - */ "muteThread": string; - /** - * スレッドのミュートを解除 - */ "unmuteThread": string; - /** - * フォローの公開範囲 - */ - "followingVisibility": string; - /** - * フォロワーの公開範囲 - */ - "followersVisibility": string; - /** - * さらにスレッドを見る - */ + "ffVisibility": string; + "ffVisibilityDescription": string; "continueThread": string; - /** - * アカウントが削除されます。よろしいですか? - */ "deleteAccountConfirm": string; - /** - * パスワードが間違っています。 - */ "incorrectPassword": string; - /** - * ワンタイムパスワードが間違っているか、期限切れになっています。 - */ - "incorrectTotp": string; - /** - * 「{choice}」に投票しますか? - */ - "voteConfirm": ParameterizedString<"choice">; - /** - * 隠す - */ + "voteConfirm": string; "hide": string; - /** - * モバイルデバイスのときドロワーで表示 - */ "useDrawerReactionPickerForMobile": string; - /** - * おかえりなさい、{name}さん - */ - "welcomeBackWithName": ParameterizedString<"name">; - /** - * [{ok}]を押して、メールアドレスの確認を完了してください。 - */ - "clickToFinishEmailVerification": ParameterizedString<"ok">; - /** - * デバイスタイプ - */ + "welcomeBackWithName": string; + "clickToFinishEmailVerification": string; "overridedDeviceKind": string; - /** - * スマートフォン - */ "smartphone": string; - /** - * タブレット - */ "tablet": string; - /** - * 自動 - */ "auto": string; - /** - * テーマカラー - */ "themeColor": string; - /** - * サイズ - */ "size": string; - /** - * 列の数 - */ "numberOfColumn": string; - /** - * 検索 - */ "searchByGoogle": string; - /** - * サーバーデフォルトのライトテーマ - */ "instanceDefaultLightTheme": string; - /** - * サーバーデフォルトのダークテーマ - */ "instanceDefaultDarkTheme": string; - /** - * オブジェクト形式のテーマコードを記入します。 - */ "instanceDefaultThemeDescription": string; - /** - * ミュートする期限 - */ "mutePeriod": string; - /** - * 期限 - */ "period": string; - /** - * 無期限 - */ "indefinitely": string; - /** - * 10分 - */ "tenMinutes": string; - /** - * 1時間 - */ "oneHour": string; - /** - * 1日 - */ "oneDay": string; - /** - * 1週間 - */ "oneWeek": string; - /** - * 1ヶ月 - */ "oneMonth": string; - /** - * 3ヶ月 - */ - "threeMonths": string; - /** - * 1年 - */ - "oneYear": string; - /** - * 3日 - */ - "threeDays": string; - /** - * 反映されるまで時間がかかる場合があります。 - */ "reflectMayTakeTime": string; - /** - * アカウント情報の取得に失敗しました - */ "failedToFetchAccountInformation": string; - /** - * レート制限を超えました - */ "rateLimitExceeded": string; - /** - * 画像のクロップ - */ "cropImage": string; - /** - * 画像をクロップしますか? - */ "cropImageAsk": string; - /** - * クロップする - */ "cropYes": string; - /** - * そのまま使う - */ "cropNo": string; - /** - * ファイル - */ "file": string; - /** - * 直近{n}時間 - */ - "recentNHours": ParameterizedString<"n">; - /** - * 直近{n}日 - */ - "recentNDays": ParameterizedString<"n">; - /** - * メールサーバーの設定がされていません。 - */ + "recentNHours": string; + "recentNDays": string; "noEmailServerWarning": string; - /** - * 未対応の通報があります。 - */ "thereIsUnresolvedAbuseReportWarning": string; - /** - * 推奨 - */ "recommended": string; - /** - * チェック - */ "check": string; - /** - * このユーザーのドライブ容量上限を変更 - */ "driveCapOverrideLabel": string; - /** - * 0以下を指定すると解除されます。 - */ "driveCapOverrideCaption": string; - /** - * 閲覧するには管理者アカウントでログインしている必要があります。 - */ "requireAdminForView": string; - /** - * システムにより自動で作成・管理されているアカウントです。 - */ "isSystemAccount": string; - /** - * この操作を行うには {x} と入力してください - */ - "typeToConfirm": ParameterizedString<"x">; - /** - * アカウント削除 - */ + "typeToConfirm": string; "deleteAccount": string; - /** - * ドキュメント - */ "document": string; - /** - * ページキャッシュ数 - */ "numberOfPageCache": string; - /** - * 多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。 - */ "numberOfPageCacheDescription": string; - /** - * ログアウトしますか? - */ "logoutConfirm": string; - /** - * ログアウトするとクライアントの設定情報がブラウザから消去されます。再ログイン時に設定情報を復元できるようにするためには、設定の自動バックアップを有効にしてください。 - */ - "logoutWillClearClientData": string; - /** - * 最終利用日時 - */ "lastActiveDate": string; - /** - * ステータスバー - */ "statusbar": string; - /** - * 選択してください - */ "pleaseSelect": string; - /** - * 反転 - */ "reverse": string; - /** - * 色付き - */ "colored": string; - /** - * 更新間隔 - */ "refreshInterval": string; - /** - * ラベル - */ "label": string; - /** - * タイプ - */ "type": string; - /** - * 速度 - */ "speed": string; - /** - * 遅い - */ "slow": string; - /** - * 速い - */ "fast": string; - /** - * センシティブなメディアの検出 - */ "sensitiveMediaDetection": string; - /** - * ローカルのみ - */ "localOnly": string; - /** - * リモートのみ - */ "remoteOnly": string; - /** - * アップロード失敗 - */ "failedToUpload": string; - /** - * 不適切な内容を含む可能性があると判定されたためアップロードできません。 - */ "cannotUploadBecauseInappropriate": string; - /** - * ドライブの空き容量が無いためアップロードできません。 - */ "cannotUploadBecauseNoFreeSpace": string; - /** - * ファイルサイズの制限を超えているためアップロードできません。 - */ "cannotUploadBecauseExceedsFileSizeLimit": string; - /** - * 許可されていないファイル種別のためアップロードできません。 - */ - "cannotUploadBecauseUnallowedFileType": string; - /** - * ベータ - */ "beta": string; - /** - * 自動センシティブ判定 - */ "enableAutoSensitive": string; - /** - * 利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。 - */ "enableAutoSensitiveDescription": string; - /** - * ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。 - */ "activeEmailValidationDescription": string; - /** - * ナビゲーションバー - */ "navbar": string; - /** - * シャッフル - */ "shuffle": string; - /** - * アカウント - */ "account": string; - /** - * 移動 - */ "move": string; - /** - * プッシュ通知 - */ "pushNotification": string; - /** - * プッシュ通知を有効化 - */ "subscribePushNotification": string; - /** - * プッシュ通知を停止する - */ "unsubscribePushNotification": string; - /** - * プッシュ通知は有効です - */ "pushNotificationAlreadySubscribed": string; - /** - * ブラウザかサーバーがプッシュ通知に非対応 - */ "pushNotificationNotSupported": string; - /** - * 通知が既読になったらプッシュ通知を削除する - */ "sendPushNotificationReadMessage": string; - /** - * 端末の電池消費量が増加する可能性があります。 - */ "sendPushNotificationReadMessageCaption": string; - /** - * 最大化 - */ "windowMaximize": string; - /** - * 最小化 - */ "windowMinimize": string; - /** - * 元に戻す - */ "windowRestore": string; - /** - * キャプション - */ "caption": string; - /** - * Botアカウントでログイン中 - */ "loggedInAsBot": string; - /** - * ツール - */ "tools": string; - /** - * 読み込めません - */ "cannotLoad": string; - /** - * プロフィール表示回数 - */ "numberOfProfileView": string; - /** - * いいね! - */ "like": string; - /** - * いいねを解除 - */ "unlike": string; - /** - * いいね数 - */ "numberOfLikes": string; - /** - * 表示 - */ "show": string; - /** - * 今後表示しない - */ "neverShow": string; - /** - * また後で - */ "remindMeLater": string; - /** - * Misskeyを気に入っていただけましたか? - */ "didYouLikeMisskey": string; - /** - * Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします! - */ - "pleaseDonate": ParameterizedString<"host">; - /** - * 対応するソースコードは{anchor}から利用可能です。 - */ - "correspondingSourceIsAvailable": ParameterizedString<"anchor">; - /** - * ロール - */ + "pleaseDonate": string; "roles": string; - /** - * ロール - */ "role": string; - /** - * ロールはありません - */ "noRole": string; - /** - * 一般ユーザー - */ "normalUser": string; - /** - * 未定義 - */ "undefined": string; - /** - * アサイン - */ "assign": string; - /** - * アサインを解除 - */ "unassign": string; - /** - * 色 - */ "color": string; - /** - * カスタム絵文字の管理 - */ "manageCustomEmojis": string; - /** - * アバターデコレーションの管理 - */ - "manageAvatarDecorations": string; - /** - * これ以上作成することはできません。 - */ "youCannotCreateAnymore": string; - /** - * 一時的に利用できません - */ "cannotPerformTemporary": string; - /** - * 操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。 - */ "cannotPerformTemporaryDescription": string; - /** - * パラメータエラー - */ "invalidParamError": string; - /** - * リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる・許可されていない文字を入力している等の可能性もあります。 - */ "invalidParamErrorDescription": string; - /** - * 操作が拒否されました - */ "permissionDeniedError": string; - /** - * このアカウントにはこの操作を行うための権限がありません。 - */ "permissionDeniedErrorDescription": string; - /** - * プリセット - */ "preset": string; - /** - * プリセットから選択 - */ "selectFromPresets": string; - /** - * 実績 - */ "achievements": string; - /** - * サーバーの応答が無効です - */ "gotInvalidResponseError": string; - /** - * サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。 - */ "gotInvalidResponseErrorDescription": string; - /** - * この投稿は迷惑になる可能性があります。 - */ "thisPostMayBeAnnoying": string; - /** - * ホームに投稿 - */ "thisPostMayBeAnnoyingHome": string; - /** - * やめる - */ "thisPostMayBeAnnoyingCancel": string; - /** - * このまま投稿 - */ "thisPostMayBeAnnoyingIgnore": string; - /** - * リノートのスマート省略 - */ "collapseRenotes": string; - /** - * リアクションやリノートをしたことがあるノートをたたんで表示します。 - */ - "collapseRenotesDescription": string; - /** - * サーバー内部エラー - */ "internalServerError": string; - /** - * サーバー内部で予期しないエラーが発生しました。 - */ "internalServerErrorDescription": string; - /** - * エラー情報をコピー - */ "copyErrorInfo": string; - /** - * このサーバーに登録する - */ "joinThisServer": string; - /** - * 他のサーバーを探す - */ "exploreOtherServers": string; - /** - * タイムラインを見てみる - */ "letsLookAtTimeline": string; - /** - * 連合なしにしますか? - */ "disableFederationConfirm": string; - /** - * 連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。 - */ "disableFederationConfirmWarn": string; - /** - * 連合なしにする - */ "disableFederationOk": string; - /** - * 現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。 - */ "invitationRequiredToRegister": string; - /** - * このサーバーではメール配信はサポートされていません - */ "emailNotSupported": string; - /** - * チャンネルに投稿 - */ "postToTheChannel": string; - /** - * 後から変更できません。 - */ "cannotBeChangedLater": string; - /** - * リアクションの受け入れ - */ "reactionAcceptance": string; - /** - * いいねのみ - */ "likeOnly": string; - /** - * 全て (リモートはいいねのみ) - */ "likeOnlyForRemote": string; - /** - * 非センシティブのみ - */ "nonSensitiveOnly": string; - /** - * 非センシティブのみ (リモートはいいねのみ) - */ "nonSensitiveOnlyForLocalLikeOnlyForRemote": string; - /** - * 自分に割り当てられたロール - */ "rolesAssignedToMe": string; - /** - * パスワードリセットしますか? - */ "resetPasswordConfirm": string; - /** - * センシティブワード - */ "sensitiveWords": string; - /** - * 設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。 - */ "sensitiveWordsDescription": string; - /** - * スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。 - */ "sensitiveWordsDescription2": string; - /** - * 禁止ワード - */ - "prohibitedWords": string; - /** - * 設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。 - */ - "prohibitedWordsDescription": string; - /** - * スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。 - */ - "prohibitedWordsDescription2": string; - /** - * 非表示ハッシュタグ - */ - "hiddenTags": string; - /** - * 設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。 - */ - "hiddenTagsDescription": string; - /** - * ノート検索は利用できません。 - */ "notesSearchNotAvailable": string; - /** - * ライセンス - */ "license": string; - /** - * お気に入り解除しますか? - */ "unfavoriteConfirm": string; - /** - * 自分のクリップ - */ "myClips": string; - /** - * ドライブクリーナー - */ "drivecleaner": string; - /** - * すべてのキューを今すぐ再試行 - */ "retryAllQueuesNow": string; - /** - * 今すぐ再試行しますか? - */ "retryAllQueuesConfirmTitle": string; - /** - * 一時的にサーバーの負荷が増大することがあります。 - */ "retryAllQueuesConfirmText": string; - /** - * リモートユーザーのチャートを生成 - */ "enableChartsForRemoteUser": string; - /** - * リモートサーバーのチャートを生成 - */ "enableChartsForFederatedInstances": string; - /** - * リモートサーバーの情報を取得 - */ - "enableStatsForFederatedInstances": string; - /** - * ノートのアクションにクリップを追加 - */ "showClipButtonInNoteFooter": string; - /** - * リアクションの表示サイズ - */ - "reactionsDisplaySize": string; - /** - * リアクションの最大横幅を制限し、縮小して表示する - */ - "limitWidthOfReaction": string; - /** - * ノートIDまたはURL - */ + "largeNoteReactions": string; "noteIdOrUrl": string; - /** - * 動画 - */ "video": string; - /** - * 動画 - */ "videos": string; - /** - * 音声 - */ - "audio": string; - /** - * 音声 - */ - "audioFiles": string; - /** - * データセーバー - */ "dataSaver": string; - /** - * アカウントの移行 - */ "accountMigration": string; - /** - * このユーザーは新しいアカウントに移行しました: - */ "accountMoved": string; - /** - * このアカウントは移行されています - */ "accountMovedShort": string; - /** - * この操作はできません - */ "operationForbidden": string; - /** - * 常に広告を表示する - */ "forceShowAds": string; - /** - * メモを追加 - */ "addMemo": string; - /** - * メモを編集 - */ "editMemo": string; - /** - * リアクション一覧 - */ "reactionsList": string; - /** - * リノート一覧 - */ "renotesList": string; - /** - * 通知の表示 - */ "notificationDisplay": string; - /** - * 左上 - */ "leftTop": string; - /** - * 右上 - */ "rightTop": string; - /** - * 左下 - */ "leftBottom": string; - /** - * 右下 - */ "rightBottom": string; - /** - * スタック方向 - */ "stackAxis": string; - /** - * 縦 - */ "vertical": string; - /** - * 横 - */ "horizontal": string; - /** - * 位置 - */ "position": string; - /** - * サーバールール - */ "serverRules": string; - /** - * このサーバーに登録するには、以下の内容を確認し同意する必要があります。 - */ "pleaseConfirmBelowBeforeSignup": string; - /** - * 続けるには、全ての「同意する」にチェックが入っている必要があります。 - */ "pleaseAgreeAllToContinue": string; - /** - * 続ける - */ "continue": string; - /** - * 予約ユーザー名 - */ "preservedUsernames": string; - /** - * 予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。 - */ "preservedUsernamesDescription": string; - /** - * このファイルからノートを作成 - */ "createNoteFromTheFile": string; - /** - * アーカイブ - */ "archive": string; - /** - * アーカイブ済み - */ - "archived": string; - /** - * アーカイブ解除 - */ - "unarchive": string; - /** - * {name}をアーカイブしますか? - */ - "channelArchiveConfirmTitle": ParameterizedString<"name">; - /** - * アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。 - */ + "channelArchiveConfirmTitle": string; "channelArchiveConfirmDescription": string; - /** - * このチャンネルはアーカイブされています。 - */ "thisChannelArchived": string; - /** - * ノートの表示 - */ "displayOfNote": string; - /** - * 初期設定 - */ "initialAccountSetting": string; - /** - * フォロー中 - */ "youFollowing": string; - /** - * 生成AIによる学習を拒否 - */ "preventAiLearning": string; - /** - * 外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。 - */ "preventAiLearningDescription": string; - /** - * オプション - */ "options": string; - /** - * ユーザー指定 - */ "specifyUser": string; - /** - * 照会しますか? - */ - "lookupConfirm": string; - /** - * ハッシュタグのページを開きますか? - */ - "openTagPageConfirm": string; - /** - * ホスト指定 - */ - "specifyHost": string; - /** - * プレビューできません - */ "failedToPreviewUrl": string; - /** - * 更新 - */ "update": string; - /** - * リアクションとして使えるロール - */ "rolesThatCanBeUsedThisEmojiAsReaction": string; - /** - * ロールの指定が一つもない場合、誰でもリアクションとして使えます。 - */ "rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription": string; - /** - * ロールは公開ロールである必要があります。 - */ "rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn": string; - /** - * リアクションを取り消しますか? - */ "cancelReactionConfirm": string; - /** - * リアクションを変更しますか? - */ "changeReactionConfirm": string; - /** - * あとで - */ "later": string; - /** - * Misskeyへ - */ "goToMisskey": string; - /** - * 絵文字の追加辞書 - */ "additionalEmojiDictionary": string; - /** - * インストール済み - */ "installed": string; - /** - * ブランディング - */ "branding": string; - /** - * サーバーのマシン情報を公開する - */ "enableServerMachineStats": string; - /** - * ユーザーごとのIdenticon生成を有効にする - */ "enableIdenticonGeneration": string; - /** - * オフにするとパフォーマンスが向上します。 - */ "turnOffToImprovePerformance": string; - /** - * 招待コードを作成 - */ - "createInviteCode": string; - /** - * オプションを指定して作成 - */ - "createWithOptions": string; - /** - * 作成数 - */ - "createCount": string; - /** - * 招待コードを作成しました - */ - "inviteCodeCreated": string; - /** - * 作成できる招待コードの数が上限に達しています。 - */ - "inviteLimitExceeded": string; - /** - * 作成できる招待コード: 残り {limit} 個 - */ - "createLimitRemaining": ParameterizedString<"limit">; - /** - * {time}で最大 {limit} 個の招待コードを作成できます。 - */ - "inviteLimitResetCycle": ParameterizedString<"time" | "limit">; - /** - * 有効期限 - */ - "expirationDate": string; - /** - * 有効期限を設けない - */ - "noExpirationDate": string; - /** - * 招待コードが使用された日時 - */ - "inviteCodeUsedAt": string; - /** - * 招待コードを使用したユーザー - */ - "registeredUserUsingInviteCode": string; - /** - * メール認証待ち - */ - "waitingForMailAuth": string; - /** - * 招待コードを作成したユーザー - */ - "inviteCodeCreator": string; - /** - * 使用日時 - */ - "usedAt": string; - /** - * 未使用 - */ - "unused": string; - /** - * 使用済み - */ - "used": string; - /** - * 期限切れ - */ - "expired": string; - /** - * 同意しますか? - */ - "doYouAgree": string; - /** - * 重要ですので必ずお読みください。 - */ - "beSureToReadThisAsItIsImportant": string; - /** - * 「{x}」の内容をよく読み、同意します。 - */ - "iHaveReadXCarefullyAndAgree": ParameterizedString<"x">; - /** - * ダイアログ - */ - "dialog": string; - /** - * アイコン - */ - "icon": string; - /** - * あなたへ - */ - "forYou": string; - /** - * 現在のお知らせ - */ - "currentAnnouncements": string; - /** - * 過去のお知らせ - */ - "pastAnnouncements": string; - /** - * 未読のお知らせがあります。 - */ - "youHaveUnreadAnnouncements": string; - /** - * ブラウザまたはデバイスの指示に従って、セキュリティキーまたはパスキーを使用してください。 - */ - "useSecurityKey": string; - /** - * 返信 - */ - "replies": string; - /** - * リノート - */ - "renotes": string; - /** - * 返信を見る - */ - "loadReplies": string; - /** - * 会話を見る - */ - "loadConversation": string; - /** - * ピン留めされたリスト - */ - "pinnedList": string; - /** - * デバイスの画面を常にオンにする - */ - "keepScreenOn": string; - /** - * このリンク先の所有者であることが確認されました - */ - "verifiedLink": string; - /** - * 投稿を通知 - */ - "notifyNotes": string; - /** - * 投稿の通知を解除 - */ - "unnotifyNotes": string; - /** - * 認証 - */ - "authentication": string; - /** - * 続けるには認証を行ってください - */ - "authenticationRequiredToContinue": string; - /** - * 日時 - */ - "dateAndTime": string; - /** - * リノートを表示 - */ - "showRenotes": string; - /** - * 編集済み - */ - "edited": string; - /** - * 通知の受信設定 - */ - "notificationRecieveConfig": string; - /** - * 相互フォロー - */ - "mutualFollow": string; - /** - * フォロー中またはフォロワー - */ - "followingOrFollower": string; - /** - * ファイル付きのみ - */ - "fileAttachedOnly": string; - /** - * TLに他の人への返信を含める - */ - "showRepliesToOthersInTimeline": string; - /** - * TLに他の人への返信を含めない - */ - "hideRepliesToOthersInTimeline": string; - /** - * TLに現在フォロー中の人全員の返信を含めるようにする - */ - "showRepliesToOthersInTimelineAll": string; - /** - * TLに現在フォロー中の人全員の返信を含めないようにする - */ - "hideRepliesToOthersInTimelineAll": string; - /** - * この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか? - */ - "confirmShowRepliesAll": string; - /** - * この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか? - */ - "confirmHideRepliesAll": string; - /** - * 外部サービス - */ - "externalServices": string; - /** - * ソースコード - */ - "sourceCode": string; - /** - * ソースコードはまだ提供されていません。この問題の修正について管理者に問い合わせてください。 - */ - "sourceCodeIsNotYetProvided": string; - /** - * リポジトリURL - */ - "repositoryUrl": string; - /** - * ソースコードが公開されているリポジトリがある場合、そのURLを記入します。Misskeyを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://github.com/misskey-dev/misskey と記入します。 - */ - "repositoryUrlDescription": string; - /** - * リポジトリを公開していない場合、代わりにtarballを提供する必要があります。詳細は.config/example.ymlを参照してください。 - */ - "repositoryUrlOrTarballRequired": string; - /** - * フィードバック - */ - "feedback": string; - /** - * フィードバックURL - */ - "feedbackUrl": string; - /** - * 運営者情報 - */ - "impressum": string; - /** - * 運営者情報URL - */ - "impressumUrl": string; - /** - * ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。 - */ - "impressumDescription": string; - /** - * プライバシーポリシー - */ - "privacyPolicy": string; - /** - * プライバシーポリシーURL - */ - "privacyPolicyUrl": string; - /** - * 利用規約・プライバシーポリシー - */ - "tosAndPrivacyPolicy": string; - /** - * アイコンデコレーション - */ - "avatarDecorations": string; - /** - * 付ける - */ - "attach": string; - /** - * 外す - */ - "detach": string; - /** - * 全て外す - */ - "detachAll": string; - /** - * 角度 - */ - "angle": string; - /** - * 反転 - */ - "flip": string; - /** - * アイコンのデコレーションを表示 - */ - "showAvatarDecorations": string; - /** - * 離してリロード - */ - "releaseToRefresh": string; - /** - * リロード中 - */ - "refreshing": string; - /** - * 引っ張ってリロード - */ - "pullDownToRefresh": string; - /** - * 通知をグルーピング - */ - "useGroupedNotifications": string; - /** - * メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。 - */ - "signupPendingError": string; - /** - * 「内容を隠す」がオンの場合は注釈の記述が必要です。 - */ - "cwNotationRequired": string; - /** - * リアクションする - */ - "doReaction": string; - /** - * コード - */ - "code": string; - /** - * 設定の反映にはリロードが必要です。 - */ - "reloadRequiredToApplySettings": string; - /** - * 残り: {n} - */ - "remainingN": ParameterizedString<"n">; - /** - * 現在の内容に上書きされますがよろしいですか? - */ - "overwriteContentConfirm": string; - /** - * 季節に応じた画面の演出 - */ - "seasonalScreenEffect": string; - /** - * デコる - */ - "decorate": string; - /** - * 装飾を追加 - */ - "addMfmFunction": string; - /** - * 高度なMFMのピッカーを表示する - */ - "enableQuickAddMfmFunction": string; - /** - * バブルゲーム - */ - "bubbleGame": string; - /** - * 効果音 - */ - "sfx": string; - /** - * サウンドが再生されます - */ - "soundWillBePlayed": string; - /** - * リプレイを見る - */ - "showReplay": string; - /** - * リプレイ - */ - "replay": string; - /** - * リプレイ中 - */ - "replaying": string; - /** - * リプレイを終了 - */ - "endReplay": string; - /** - * リプレイデータをコピー - */ - "copyReplayData": string; - /** - * ランキング - */ - "ranking": string; - /** - * 直近{n}日 - */ - "lastNDays": ParameterizedString<"n">; - /** - * タイトルへ - */ - "backToTitle": string; - /** - * お住まいの地域 - */ - "hemisphere": string; - /** - * センシティブなファイルを含むノートを表示 - */ - "withSensitive": string; - /** - * {name}のセンシティブなファイルを含む投稿 - */ - "userSaysSomethingSensitive": ParameterizedString<"name">; - /** - * スワイプしてタブを切り替える - */ - "enableHorizontalSwipe": string; - /** - * 読み込み中 - */ - "loading": string; - /** - * やめる - */ - "surrender": string; - /** - * リトライ - */ - "gameRetry": string; - /** - * 使用しない場合は空欄にしてください - */ - "notUsePleaseLeaveBlank": string; - /** - * ワンタイムパスワードを使う - */ - "useTotp": string; - /** - * バックアップコードを使う - */ - "useBackupCode": string; - /** - * アプリを起動 - */ - "launchApp": string; - /** - * 動画・音声の再生にブラウザのUIを使用する - */ - "useNativeUIForVideoAudioPlayer": string; - /** - * オリジナルのファイル名を保持 - */ - "keepOriginalFilename": string; - /** - * この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。 - */ - "keepOriginalFilenameDescription": string; - /** - * 説明文はありません - */ - "noDescription": string; - /** - * フォローの際常に確認する - */ - "alwaysConfirmFollow": string; - /** - * お問い合わせ - */ - "inquiry": string; - /** - * もう一度お試しください。 - */ - "tryAgain": string; - /** - * センシティブなメディアを表示するとき確認する - */ - "confirmWhenRevealingSensitiveMedia": string; - /** - * センシティブなメディアです。表示しますか? - */ - "sensitiveMediaRevealConfirm": string; - /** - * 作成したリスト - */ - "createdLists": string; - /** - * 作成したアンテナ - */ - "createdAntennas": string; - /** - * {x}から - */ - "fromX": ParameterizedString<"x">; - /** - * 埋め込みコードを生成 - */ - "genEmbedCode": string; - /** - * このユーザーのノート一覧 - */ - "noteOfThisUser": string; - /** - * これ以上このクリップにノートを追加できません。 - */ - "clipNoteLimitExceeded": string; - /** - * パフォーマンス - */ - "performance": string; - /** - * 変更あり - */ - "modified": string; - /** - * 破棄 - */ - "discard": string; - /** - * {n}件の変更があります - */ - "thereAreNChanges": ParameterizedString<"n">; - /** - * パスキーでログイン - */ - "signinWithPasskey": string; - /** - * 登録されていないパスキーです。 - */ - "unknownWebAuthnKey": string; - /** - * パスキーの検証に失敗しました。 - */ - "passkeyVerificationFailed": string; - /** - * パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。 - */ - "passkeyVerificationSucceededButPasswordlessLoginDisabled": string; - /** - * フォロワーへのメッセージ - */ - "messageToFollower": string; - /** - * 対象 - */ - "target": string; - /** - * CAPTCHAのテストを目的とした機能です。本番環境で使用しないでください。 - */ - "testCaptchaWarning": string; - /** - * 禁止ワード(ユーザーの名前) - */ - "prohibitedWordsForNameOfUser": string; - /** - * このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。 - */ - "prohibitedWordsForNameOfUserDescription": string; - /** - * 変更しようとした名前に禁止された文字列が含まれています - */ - "yourNameContainsProhibitedWords": string; - /** - * 名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。 - */ - "yourNameContainsProhibitedWordsDescription": string; - /** - * 投稿者により、表示にはログインが必要と設定されています - */ - "thisContentsAreMarkedAsSigninRequiredByAuthor": string; - /** - * ロックダウン - */ - "lockdown": string; - /** - * アカウントを選択してください - */ - "pleaseSelectAccount": string; - /** - * 利用可能なロール - */ - "availableRoles": string; - /** - * 注意事項を理解した上でオンにします。 - */ - "acknowledgeNotesAndEnable": string; - /** - * このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。 - */ - "federationSpecified": string; - /** - * このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。 - */ - "federationDisabled": string; - /** - * リアクションする際に確認する - */ - "confirmOnReact": string; - /** - * " {emoji} " をリアクションしますか? - */ - "reactAreYouSure": ParameterizedString<"emoji">; - /** - * このメディアをセンシティブとして設定しますか? - */ - "markAsSensitiveConfirm": string; - /** - * このメディアのセンシティブ指定を解除しますか? - */ - "unmarkAsSensitiveConfirm": string; - /** - * 環境設定 - */ - "preferences": string; - /** - * アクセシビリティ - */ - "accessibility": string; - /** - * 設定のプロファイル - */ - "preferencesProfile": string; - /** - * 設定IDをコピー - */ - "copyPreferenceId": string; - /** - * 初期値に戻す - */ - "resetToDefaultValue": string; - /** - * アカウントで上書き - */ - "overrideByAccount": string; - /** - * 無題 - */ - "untitled": string; - /** - * 名前はありません - */ - "noName": string; - /** - * スキップ - */ - "skip": string; - /** - * 復元 - */ - "restore": string; - /** - * デバイス間で同期 - */ - "syncBetweenDevices": string; - /** - * サーバーに設定値が存在します - */ - "preferenceSyncConflictTitle": string; - /** - * 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どうしますか? - */ - "preferenceSyncConflictText": string; - /** - * 統合する - */ - "preferenceSyncConflictChoiceMerge": string; - /** - * サーバーの設定値で上書き - */ - "preferenceSyncConflictChoiceServer": string; - /** - * デバイスの設定値で上書き - */ - "preferenceSyncConflictChoiceDevice": string; - /** - * 同期の有効化をキャンセル - */ - "preferenceSyncConflictChoiceCancel": string; - /** - * ペースト - */ - "paste": string; - /** - * 絵文字パレット - */ - "emojiPalette": string; - /** - * 投稿フォーム - */ - "postForm": string; - /** - * 文字数 - */ - "textCount": string; - /** - * 情報 - */ - "information": string; - /** - * チャット - */ - "chat": string; - /** - * 旧設定情報を移行 - */ - "migrateOldSettings": string; - /** - * 通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。 - */ - "migrateOldSettings_description": string; - /** - * 圧縮 - */ - "compress": string; - /** - * 右 - */ - "right": string; - /** - * 下 - */ - "bottom": string; - /** - * 上 - */ - "top": string; - /** - * 埋め込み - */ - "embed": string; - /** - * 設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます) - */ - "settingsMigrating": string; - /** - * 読み取り専用 - */ - "readonly": string; - /** - * デッキへ戻る - */ - "goToDeck": string; - /** - * 連合ジョブ - */ - "federationJobs": string; - /** - * ドライブでは、過去にアップロードしたファイルの一覧が表示されます。
- * ノートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。
- * ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。
- * フォルダを作って整理することもできます。 - */ - "driveAboutTip": string; - /** - * スクロールして閉じる - */ - "scrollToClose": string; - /** - * アドバイス - */ - "advice": string; - /** - * リアルタイムモード - */ - "realtimeMode": string; - /** - * オンにする - */ - "turnItOn": string; - /** - * オフにする - */ - "turnItOff": string; - /** - * 絵文字ミュート - */ - "emojiMute": string; - /** - * 絵文字ミュート解除 - */ - "emojiUnmute": string; - /** - * {x}をミュート - */ - "muteX": ParameterizedString<"x">; - /** - * {x}のミュートを解除 - */ - "unmuteX": ParameterizedString<"x">; - /** - * 中止 - */ - "abort": string; - /** - * ヒントとコツ - */ - "tip": string; - /** - * 全ての「ヒントとコツ」を再表示 - */ - "redisplayAllTips": string; - /** - * 全ての「ヒントとコツ」を非表示 - */ - "hideAllTips": string; - "_chat": { - /** - * まだメッセージはありません - */ - "noMessagesYet": string; - /** - * 新しいメッセージ - */ - "newMessage": string; - /** - * 個人チャット - */ - "individualChat": string; - /** - * 特定ユーザーとの一対一のチャットができます。 - */ - "individualChat_description": string; - /** - * ルームチャット - */ - "roomChat": string; - /** - * 複数人でのチャットができます。 - * また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。 - */ - "roomChat_description": string; - /** - * ルームを作成 - */ - "createRoom": string; - /** - * ユーザーを招待してチャットを始めましょう - */ - "inviteUserToChat": string; - /** - * 作成したルーム - */ - "yourRooms": string; - /** - * 参加中のルーム - */ - "joiningRooms": string; - /** - * 招待 - */ - "invitations": string; - /** - * 招待はありません - */ - "noInvitations": string; - /** - * 履歴 - */ - "history": string; - /** - * 履歴はありません - */ - "noHistory": string; - /** - * ルームはありません - */ - "noRooms": string; - /** - * ユーザーを招待 - */ - "inviteUser": string; - /** - * 送信した招待 - */ - "sentInvitations": string; - /** - * 参加 - */ - "join": string; - /** - * 無視 - */ - "ignore": string; - /** - * ルームから退出 - */ - "leave": string; - /** - * メンバー - */ - "members": string; - /** - * メッセージを検索 - */ - "searchMessages": string; - /** - * ホーム - */ - "home": string; - /** - * 送信 - */ - "send": string; - /** - * 改行 - */ - "newline": string; - /** - * このルームをミュート - */ - "muteThisRoom": string; - /** - * ルームを削除 - */ - "deleteRoom": string; - /** - * このサーバー、またはこのアカウントでチャットは有効化されていません。 - */ - "chatNotAvailableForThisAccountOrServer": string; - /** - * このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。 - */ - "chatIsReadOnlyForThisAccountOrServer": string; - /** - * 相手のアカウントでチャット機能が使えない状態になっています。 - */ - "chatNotAvailableInOtherAccount": string; - /** - * このユーザーとのチャットを開始できません - */ - "cannotChatWithTheUser": string; - /** - * チャットが使えない状態になっているか、相手がチャットを開放していません。 - */ - "cannotChatWithTheUser_description": string; - /** - * あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。 - */ - "youAreNotAMemberOfThisRoomButInvited": string; - /** - * 招待を承認しますか? - */ - "doYouAcceptInvitation": string; - /** - * チャットする - */ - "chatWithThisUser": string; - /** - * このユーザーはフォロワーからのみチャットを受け付けています。 - */ - "thisUserAllowsChatOnlyFromFollowers": string; - /** - * このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。 - */ - "thisUserAllowsChatOnlyFromFollowing": string; - /** - * このユーザーは相互フォローのユーザーからのみチャットを受け付けています。 - */ - "thisUserAllowsChatOnlyFromMutualFollowing": string; - /** - * このユーザーは誰からもチャットを受け付けていません。 - */ - "thisUserNotAllowedChatAnyone": string; - /** - * チャットを許可する相手 - */ - "chatAllowedUsers": string; - /** - * 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。 - */ - "chatAllowedUsers_note": string; - "_chatAllowedUsers": { - /** - * 誰でも - */ - "everyone": string; - /** - * 自分のフォロワーのみ - */ - "followers": string; - /** - * 自分がフォローしているユーザーのみ - */ - "following": string; - /** - * 相互フォローのユーザーのみ - */ - "mutual": string; - /** - * 誰も許可しない - */ - "none": string; - }; - }; - "_emojiPalette": { - /** - * パレット - */ - "palettes": string; - /** - * パレットのデバイス間同期を有効にする - */ - "enableSyncBetweenDevicesForPalettes": string; - /** - * メインで使用するパレット - */ - "paletteForMain": string; - /** - * リアクションで使用するパレット - */ - "paletteForReaction": string; - }; - "_settings": { - /** - * ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。 - */ - "driveBanner": string; - /** - * プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。 - */ - "pluginBanner": string; - /** - * サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。 - */ - "notificationsBanner": string; - /** - * API - */ - "api": string; - /** - * Webhook - */ - "webhook": string; - /** - * サービス連携 - */ - "serviceConnection": string; - /** - * 外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。 - */ - "serviceConnectionBanner": string; - /** - * アカウントのデータ - */ - "accountData": string; - /** - * アカウントデータのアーカイブをエクスポート/インポートして管理できます。 - */ - "accountDataBanner": string; - /** - * 非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。 - */ - "muteAndBlockBanner": string; - /** - * クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。 - */ - "accessibilityBanner": string; - /** - * コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。 - */ - "privacyBanner": string; - /** - * パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。 - */ - "securityBanner": string; - /** - * 好みに応じた、クライアントの全体的な動作の設定が行えます。 - */ - "preferencesBanner": string; - /** - * 好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。 - */ - "appearanceBanner": string; - /** - * クライアントで再生するサウンドの設定が行えます。 - */ - "soundsBanner": string; - /** - * タイムラインとノート - */ - "timelineAndNote": string; - /** - * 全てのテキスト要素を選択可能にする - */ - "makeEveryTextElementsSelectable": string; - /** - * 有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。 - */ - "makeEveryTextElementsSelectable_description": string; - /** - * アイコンをスクロールに追従させる - */ - "useStickyIcons": string; - /** - * 高品質な画像のプレースホルダを表示 - */ - "enableHighQualityImagePlaceholders": string; - /** - * UIのアニメーション - */ - "uiAnimations": string; - /** - * ナビゲーションバーに副ボタンを表示 - */ - "showNavbarSubButtons": string; - /** - * オンのとき - */ - "ifOn": string; - /** - * オフのとき - */ - "ifOff": string; - /** - * デバイス間でインストールしたテーマを同期 - */ - "enableSyncThemesBetweenDevices": string; - /** - * ひっぱって更新 - */ - "enablePullToRefresh": string; - /** - * マウスでは、ホイールを押し込みながらドラッグします。 - */ - "enablePullToRefresh_description": string; - /** - * サーバーと接続を確立し、リアルタイムでコンテンツを更新します。通信量とバッテリーの消費が多くなる場合があります。 - */ - "realtimeMode_description": string; - /** - * コンテンツの取得頻度 - */ - "contentsUpdateFrequency": string; - /** - * 高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。 - */ - "contentsUpdateFrequency_description": string; - /** - * リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。 - */ - "contentsUpdateFrequency_description2": string; - /** - * URLプレビューを表示する - */ - "showUrlPreview": string; - "_chat": { - /** - * 送信者の名前を表示 - */ - "showSenderName": string; - /** - * Enterで送信 - */ - "sendOnEnter": string; - }; - }; - "_preferencesProfile": { - /** - * プロファイル名 - */ - "profileName": string; - /** - * このデバイスを識別する名前を設定してください。 - */ - "profileNameDescription": string; - /** - * 例: 「メインPC」、「スマホ」など - */ - "profileNameDescription2": string; - /** - * プロファイルの管理 - */ - "manageProfiles": string; - }; - "_preferencesBackup": { - /** - * 自動バックアップ - */ - "autoBackup": string; - /** - * バックアップから復元 - */ - "restoreFromBackup": string; - /** - * バックアップが見つかりませんでした - */ - "noBackupsFoundTitle": string; - /** - * 自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。 - */ - "noBackupsFoundDescription": string; - /** - * 復元するバックアップを選択してください - */ - "selectBackupToRestore": string; - /** - * 自動バックアップを有効にするにはプロファイル名の設定が必要です。 - */ - "youNeedToNameYourProfileToEnableAutoBackup": string; - /** - * このデバイスで設定の自動バックアップは有効になっていません。 - */ - "autoPreferencesBackupIsNotEnabledForThisDevice": string; - /** - * 設定のバックアップが見つかりました - */ - "backupFound": string; - }; - "_accountSettings": { - /** - * コンテンツの表示にログインを必須にする - */ - "requireSigninToViewContents": string; - /** - * あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーに情報が収集されるのを防ぐ効果が期待できます。 - */ - "requireSigninToViewContentsDescription1": string; - /** - * URLプレビュー(OGP)、Webページへの埋め込み、ノートの引用に対応していないサーバーからの表示も不可になります。 - */ - "requireSigninToViewContentsDescription2": string; - /** - * リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。 - */ - "requireSigninToViewContentsDescription3": string; - /** - * 過去のノートをフォロワーのみ表示可能にする - */ - "makeNotesFollowersOnlyBefore": string; - /** - * この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートがフォロワーのみ表示可能になります。無効に戻すと、ノートの公開状態も元に戻ります。 - */ - "makeNotesFollowersOnlyBeforeDescription": string; - /** - * 過去のノートを非公開化する - */ - "makeNotesHiddenBefore": string; - /** - * この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。 - */ - "makeNotesHiddenBeforeDescription": string; - /** - * リモートサーバーに連合されたノートには効果が及ばない場合があります。 - */ - "mayNotEffectForFederatedNotes": string; - /** - * これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。 - */ - "mayNotEffectSomeSituations": string; - /** - * 指定した時間を経過しているノート - */ - "notesHavePassedSpecifiedPeriod": string; - /** - * 指定した日時より前のノート - */ - "notesOlderThanSpecifiedDateAndTime": string; - }; - "_abuseUserReport": { - /** - * 転送 - */ - "forward": string; - /** - * 匿名のシステムアカウントとして、リモートサーバーに通報を転送します。 - */ - "forwardDescription": string; - /** - * 解決 - */ - "resolve": string; - /** - * 是認 - */ - "accept": string; - /** - * 否認 - */ - "reject": string; - /** - * 内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。 - * 内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。 - */ - "resolveTutorial": string; - }; - "_delivery": { - /** - * 配信状態 - */ - "status": string; - /** - * 配信停止 - */ - "stop": string; - /** - * 配信再開 - */ - "resume": string; - "_type": { - /** - * 配信中 - */ - "none": string; - /** - * 手動停止中 - */ - "manuallySuspended": string; - /** - * サーバー削除のため停止中 - */ - "goneSuspended": string; - /** - * サーバー応答なしのため停止中 - */ - "autoSuspendedForNotResponding": string; - /** - * 配信停止中のソフトウェアであるため停止中 - */ - "softwareSuspended": string; - }; - }; - "_bubbleGame": { - /** - * 遊び方 - */ - "howToPlay": string; - /** - * ホールド - */ - "hold": string; - "_score": { - /** - * スコア - */ - "score": string; - /** - * 稼いだ金額 - */ - "scoreYen": string; - /** - * ハイスコア - */ - "highScore": string; - /** - * 最大チェーン数 - */ - "maxChain": string; - /** - * {yen}円 - */ - "yen": ParameterizedString<"yen">; - /** - * {qty}個分 - */ - "estimatedQty": ParameterizedString<"qty">; - /** - * おにぎり {onigiriQtyWithUnit} - */ - "scoreSweets": ParameterizedString<"onigiriQtyWithUnit">; - }; - "_howToPlay": { - /** - * 位置を調整してハコにモノを落とします。 - */ - "section1": string; - /** - * 同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。 - */ - "section2": string; - /** - * モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう! - */ - "section3": string; - }; - }; - "_announcement": { - /** - * 既存ユーザーのみ - */ - "forExistingUsers": string; - /** - * 有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。 - */ - "forExistingUsersDescription": string; - /** - * 既読にするのに確認が必要 - */ - "needConfirmationToRead": string; - /** - * 有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。また、一括既読操作の対象になりません。 - */ - "needConfirmationToReadDescription": string; - /** - * お知らせを終了 - */ - "end": string; - /** - * アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。 - */ - "tooManyActiveAnnouncementDescription": string; - /** - * 既読にしますか? - */ - "readConfirmTitle": string; - /** - * 「{title}」の内容を読み、既読にします。 - */ - "readConfirmText": ParameterizedString<"title">; - /** - * 特に新規ユーザーのUXを損ねる可能性が高いため、常時掲示するための情報ではなく、即時性が求められる情報の掲示のためにお知らせを使用することを推奨します。 - */ - "shouldNotBeUsedToPresentPermanentInfo": string; - /** - * ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。 - */ - "dialogAnnouncementUxWarn": string; - /** - * 非通知 - */ - "silence": string; - /** - * オンにすると、このお知らせは通知されず、既読にする必要もなくなります。 - */ - "silenceDescription": string; - }; "_initialAccountSetting": { - /** - * アカウントの作成が完了しました! - */ "accountCreated": string; - /** - * さっそくアカウントの初期設定を行いましょう。 - */ "letsStartAccountSetup": string; - /** - * まずはあなたのプロフィールを設定しましょう。 - */ "letsFillYourProfile": string; - /** - * プロフィール設定 - */ "profileSetting": string; - /** - * プライバシー設定 - */ "privacySetting": string; - /** - * これらの設定は後から変更できます。 - */ "theseSettingsCanEditLater": string; - /** - * この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。 - */ "youCanEditMoreSettingsInSettingsPageLater": string; - /** - * タイムラインを構築するため、気になるユーザーをフォローしてみましょう。 - */ "followUsers": string; - /** - * プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。 - */ - "pushNotificationDescription": ParameterizedString<"name">; - /** - * 初期設定が完了しました! - */ + "pushNotificationDescription": string; "initialAccountSettingCompleted": string; - /** - * {name}をお楽しみください! - */ - "haveFun": ParameterizedString<"name">; - /** - * このまま{name}(Misskey)の使い方についてのチュートリアルに進むこともできますが、ここで中断してすぐに使い始めることもできます。 - */ - "youCanContinueTutorial": ParameterizedString<"name">; - /** - * チュートリアルを開始 - */ - "startTutorial": string; - /** - * 初期設定をスキップしますか? - */ + "haveFun": string; + "ifYouNeedLearnMore": string; "skipAreYouSure": string; - /** - * 初期設定をあとでやり直しますか? - */ "laterAreYouSure": string; }; - "_initialTutorial": { - /** - * チュートリアルを見る - */ - "launchTutorial": string; - /** - * チュートリアル - */ - "title": string; - /** - * よくできました - */ - "wellDone": string; - /** - * チュートリアルを終了しますか? - */ - "skipAreYouSure": string; - "_landing": { - /** - * チュートリアルへようこそ - */ - "title": string; - /** - * ここでは、Misskeyの基本的な使い方や機能を確認できます。 - */ - "description": string; - }; - "_note": { - /** - * ノートって何? - */ - "title": string; - /** - * Misskeyでの投稿は「ノート」と呼びます。ノートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。 - */ - "description": string; - /** - * 返信することができます。返信に対しての返信も可能で、スレッドのように会話を続けることもできます。 - */ - "reply": string; - /** - * そのノートを自分のタイムラインに流して共有することができます。テキストを追加して引用することも可能です。 - */ - "renote": string; - /** - * リアクションをつけることができます。詳しくは次のページで解説します。 - */ - "reaction": string; - /** - * ノートの詳細を表示したり、リンクをコピーしたりなどの様々な操作が行えます。 - */ - "menu": string; - }; - "_reaction": { - /** - * リアクションって何? - */ - "title": string; - /** - * ノートには「リアクション」をつけることができます。「いいね」では伝わらないニュアンスも、リアクションで簡単・気軽に表現できます。 - */ - "description": string; - /** - * リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください! - */ - "letsTryReacting": string; - /** - * リアクションをつけると先に進めるようになります。 - */ - "reactToContinue": string; - /** - * あなたのノートが誰かにリアクションされると、リアルタイムで通知を受け取ります。 - */ - "reactNotification": string; - /** - * 「ー」ボタンを押すとリアクションを取り消すことができます。 - */ - "reactDone": string; - }; - "_timeline": { - /** - * タイムラインのしくみ - */ - "title": string; - /** - * Misskeyには、使い方に応じて複数のタイムラインが用意されています(サーバーによってはいずれかが無効になっていることがあります)。 - */ - "description1": string; - /** - * あなたがフォローしているアカウントの投稿を見られます。 - */ - "home": string; - /** - * このサーバーにいるユーザー全員の投稿を見られます。 - */ - "local": string; - /** - * ホームタイムラインとローカルタイムラインの投稿が両方表示されます。 - */ - "social": string; - /** - * 接続している他のすべてのサーバーからの投稿を見られます。 - */ - "global": string; - /** - * それぞれのタイムラインは、画面上部でいつでも切り替えられます。 - */ - "description2": string; - /** - * その他にも、リストタイムラインやチャンネルタイムラインなどがあります。詳しくは{link}をご覧ください。 - */ - "description3": ParameterizedString<"link">; - }; - "_postNote": { - /** - * ノートの投稿設定 - */ - "title": string; - /** - * Misskeyにノートを投稿する際には、様々なオプションの設定が可能です。投稿フォームはこのようになっています。 - */ - "description1": string; - "_visibility": { - /** - * ノートを表示できる相手を制限できます。 - */ - "description": string; - /** - * すべてのユーザーに公開。 - */ - "public": string; - /** - * ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。 - */ - "home": string; - /** - * フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。 - */ - "followers": string; - /** - * 指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。 - */ - "direct": string; - /** - * 機密情報は送信する際は注意してください。 - */ - "doNotSendConfidencialOnDirect1": string; - /** - * 送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。 - */ - "doNotSendConfidencialOnDirect2": string; - /** - * 他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。 - */ - "localOnly": string; - }; - "_cw": { - /** - * 内容を隠す(CW) - */ - "title": string; - /** - * 本文のかわりに「注釈」に書いた内容が表示されます。「もっと見る」を押すと本文が表示されます。 - */ - "description": string; - "_exampleNote": { - /** - * 飯テロ注意 - */ - "cw": string; - /** - * チョコのかかったドーナツを食べました🍩😋 - */ - "note": string; - }; - /** - * サーバーのガイドラインにより必要とされるノートに指定したり、ネタバレ投稿やセンシティブな文章を自主規制したりするときに使います。 - */ - "useCases": string; - }; - }; - "_howToMakeAttachmentsSensitive": { - /** - * 添付ファイルをセンシティブにするには? - */ - "title": string; - /** - * サーバーのガイドラインにより必要とされる際や、そのまま見れる状態にしておくべきではない添付ファイルには、「センシティブ」設定を付けます。 - */ - "description": string; - /** - * 試しに、このフォームに添付された画像をセンシティブにしてみてください! - */ - "tryThisFile": string; - "_exampleNote": { - /** - * 納豆のフタ開けるのミスったわね… - */ - "note": string; - }; - /** - * 添付ファイルをセンシティブにする際は、そのファイルをクリックしてメニューを開き、「センシティブとして設定」をクリックします。 - */ - "method": string; - /** - * ファイルを添付する際は、サーバーのガイドラインに従ってセンシティブを適切に設定してください。 - */ - "sensitiveSucceeded": string; - /** - * 画像をセンシティブに設定すると先に進めるようになります。 - */ - "doItToContinue": string; - }; - "_done": { - /** - * チュートリアルは終了です🎉 - */ - "title": string; - /** - * ここで紹介した機能はほんの一部にすぎません。Misskeyの使い方をより詳しく知るには、{link}をご覧ください。 - */ - "description": ParameterizedString<"link">; - }; - }; - "_timelineDescription": { - /** - * ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。 - */ - "home": string; - /** - * ローカルタイムラインでは、このサーバーにいるユーザー全員の投稿を見られます。 - */ - "local": string; - /** - * ソーシャルタイムラインには、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。 - */ - "social": string; - /** - * グローバルタイムラインでは、接続している他のすべてのサーバーからの投稿を見られます。 - */ - "global": string; - }; "_serverRules": { - /** - * 新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。 - */ "description": string; }; - "_serverSettings": { - /** - * アイコン画像のURL - */ - "iconUrl": string; - /** - * {host}がアプリとして表示される際のアイコンを指定します。 - */ - "appIconDescription": ParameterizedString<"host">; - /** - * 例: PWAや、スマートフォンのホーム画面にブックマークとして追加された時など - */ - "appIconUsageExample": string; - /** - * 円形もしくは角丸にクロップされる場合があるため、塗り潰された余白のある背景を持つことが推奨されます。 - */ - "appIconStyleRecommendation": string; - /** - * 解像度は必ず{resolution}である必要があります。 - */ - "appIconResolutionMustBe": ParameterizedString<"resolution">; - /** - * manifest.jsonのオーバーライド - */ - "manifestJsonOverride": string; - /** - * 略称 - */ - "shortName": string; - /** - * サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。 - */ - "shortNameDescription": string; - /** - * 有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。 - */ - "fanoutTimelineDescription": string; - /** - * データベースへのフォールバック - */ - "fanoutTimelineDbFallback": string; - /** - * 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。 - */ - "fanoutTimelineDbFallbackDescription": string; - /** - * 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。 - */ - "reactionsBufferingDescription": string; - /** - * 問い合わせ先URL - */ - "inquiryUrl": string; - /** - * サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。 - */ - "inquiryUrlDescription": string; - /** - * アカウントの作成をオープンにする - */ - "openRegistration": string; - /** - * 登録を開放することはリスクが伴います。サーバーを常に監視し、トラブルが発生した際にすぐに対応できる体制がある場合のみオンにすることを推奨します。 - */ - "openRegistrationWarning": string; - /** - * 一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。 - */ - "thisSettingWillAutomaticallyOffWhenModeratorsInactive": string; - /** - * 配信停止中のソフトウェア - */ - "deliverSuspendedSoftware": string; - /** - * 脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。 - */ - "deliverSuspendedSoftwareDescription": string; - /** - * お一人様モード - */ - "singleUserMode": string; - /** - * このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。 - */ - "singleUserMode_description": string; - /** - * GETリクエストに署名する - */ - "signToActivityPubGet": string; - /** - * 通常は有効にしてください。連合の通信に関する問題がある場合に、無効にすると改善することがありますが、逆にサーバーによっては通信が不可になることがあります。 - */ - "signToActivityPubGet_description": string; - /** - * リモートファイルをプロキシする - */ - "proxyRemoteFiles": string; - /** - * 有効にすると、リモートのファイルをプロキシして提供します。画像のサムネイル生成やユーザーのプライバシー保護に役立ちます。 - */ - "proxyRemoteFiles_description": string; - /** - * ActivityPub経由の照会にリダイレクトを許可する - */ - "allowExternalApRedirect": string; - /** - * 有効にすると、他のサーバーがこのサーバーを通して第三者のコンテンツを照会することが可能になりますが、コンテンツのなりすましが発生する可能性があります。 - */ - "allowExternalApRedirect_description": string; - /** - * 非利用者に対するユーザー作成コンテンツの公開範囲 - */ - "userGeneratedContentsVisibilityForVisitor": string; - /** - * モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。 - */ - "userGeneratedContentsVisibilityForVisitor_description": string; - /** - * サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。 - */ - "userGeneratedContentsVisibilityForVisitor_description2": string; - "_userGeneratedContentsVisibilityForVisitor": { - /** - * 全て公開 - */ - "all": string; - /** - * ローカルコンテンツのみ公開し、リモートコンテンツは非公開 - */ - "localOnly": string; - /** - * 全て非公開 - */ - "none": string; - }; - }; "_accountMigration": { - /** - * 別のアカウントからこのアカウントに移行 - */ "moveFrom": string; - /** - * 別のアカウントへエイリアスを作成 - */ "moveFromSub": string; - /** - * 移行元のアカウント #{n} - */ - "moveFromLabel": ParameterizedString<"n">; - /** - * 別のアカウントからこのアカウントに移行したい場合、ここでエイリアスを作成しておく必要があります。 - * 移行元のアカウントをこのように入力してください: @username@server.example.com - * 削除するには、入力欄を空にして保存します(非推奨)。 - */ + "moveFromLabel": string; "moveFromDescription": string; - /** - * このアカウントを新しいアカウントへ移行 - */ "moveTo": string; - /** - * 移行先のアカウント: - */ "moveToLabel": string; - /** - * アカウントを移行すると、取り消すことはできません。 - */ "moveCannotBeUndone": string; - /** - * 新しいアカウントへ移行します。 - *  ・フォロワーが新しいアカウントを自動でフォローします - *  ・このアカウントからのフォローは全て解除されます - *  ・このアカウントではノートの作成などができなくなります - * - * フォロワーの移行は自動ですが、フォローの移行は手動で行う必要があります。移行前にこのアカウントでフォローエクスポートし、移行後すぐに移行先アカウントでインポートを行なってください。 - * リスト・ミュート・ブロックについても同様ですので、手動で移行する必要があります。 - * - * (この説明はこのサーバー(Misskey v13.12.0以降)の仕様です。Mastodonなどの他のActivityPubソフトウェアでは挙動が異なる場合があります。) - */ "moveAccountDescription": string; - /** - * アカウントの移行には、まずは移行先のアカウントでこのアカウントに対しエイリアスを作成します。 - * エイリアス作成後、移行先のアカウントを次のように入力してください: @username@server.example.com - */ "moveAccountHowTo": string; - /** - * 移行する - */ "startMigration": string; - /** - * 本当にこのアカウントを {account} に移行しますか?一度移行すると取り消せず、二度とこのアカウントを元の状態で使用できなくなります。 - */ - "migrationConfirm": ParameterizedString<"account">; - /** - * - * アカウントは移行されています。 - * 移行を取り消すことはできません。 - */ + "migrationConfirm": string; "movedAndCannotBeUndone": string; - /** - * このアカウントからのフォロー解除は移行操作から24時間後に実行されます。 - * このアカウントのフォロー・フォロワー数は0になっています。フォロワーの解除はされないため、あなたのフォロワーはこのアカウントのフォロワー向け投稿を引き続き閲覧できます。 - */ "postMigrationNote": string; - /** - * 移行先のアカウント: - */ "movedTo": string; }; "_achievements": { - /** - * 獲得日時 - */ "earnedAt": string; "_types": { "_notes1": { - /** - * just setting up my msky - */ "title": string; - /** - * 初めてノートを投稿した - */ "description": string; - /** - * 良いMisskeyライフを! - */ "flavor": string; }; "_notes10": { - /** - * いくつかのノート - */ "title": string; - /** - * ノートを10回投稿した - */ "description": string; }; "_notes100": { - /** - * たくさんのノート - */ "title": string; - /** - * ノートを100回投稿した - */ "description": string; }; "_notes500": { - /** - * ノートまみれ - */ "title": string; - /** - * ノートを500回投稿した - */ "description": string; }; "_notes1000": { - /** - * ノートの山 - */ "title": string; - /** - * ノートを1,000回投稿した - */ "description": string; }; "_notes5000": { - /** - * 湧き出るノート - */ "title": string; - /** - * ノートを5,000回投稿した - */ "description": string; }; "_notes10000": { - /** - * スーパーノート - */ "title": string; - /** - * ノートを10,000回投稿した - */ "description": string; }; "_notes20000": { - /** - * ニードモアノート - */ "title": string; - /** - * ノートを20,000回投稿した - */ "description": string; }; "_notes30000": { - /** - * ノートノートノート - */ "title": string; - /** - * ノートを30,000回投稿した - */ "description": string; }; "_notes40000": { - /** - * ノート工場 - */ "title": string; - /** - * ノートを40,000回投稿した - */ "description": string; }; "_notes50000": { - /** - * ノートの惑星 - */ "title": string; - /** - * ノートを50,000回投稿した - */ "description": string; }; "_notes60000": { - /** - * ノートクエーサー - */ "title": string; - /** - * ノートを60,000回投稿した - */ "description": string; }; "_notes70000": { - /** - * ブラックノートホール - */ "title": string; - /** - * ノートを70,000回投稿した - */ "description": string; }; "_notes80000": { - /** - * ノートギャラクシー - */ "title": string; - /** - * ノートを80,000回投稿した - */ "description": string; }; "_notes90000": { - /** - * ノートバース - */ "title": string; - /** - * ノートを90,000回投稿した - */ "description": string; }; "_notes100000": { - /** - * ALL YOUR NOTE ARE BELONG TO US - */ "title": string; - /** - * ノートを100,000回投稿した - */ "description": string; - /** - * そんなに書くことある? - */ "flavor": string; }; "_login3": { - /** - * ビギナーⅠ - */ "title": string; - /** - * 通算ログイン日数が3日 - */ "description": string; - /** - * 今日からね僕は ミスキストってことで - */ "flavor": string; }; "_login7": { - /** - * ビギナーⅡ - */ "title": string; - /** - * 通算ログイン日数が7日 - */ "description": string; - /** - * 慣れてきましたか? - */ "flavor": string; }; "_login15": { - /** - * ビギナーⅢ - */ "title": string; - /** - * 通算ログイン日数が15日 - */ "description": string; }; "_login30": { - /** - * ミスキストⅠ - */ "title": string; - /** - * 通算ログイン日数が30日 - */ "description": string; }; "_login60": { - /** - * ミスキストⅡ - */ "title": string; - /** - * 通算ログイン日数が60日 - */ "description": string; }; "_login100": { - /** - * ミスキストⅢ - */ "title": string; - /** - * 通算ログイン日数が100日 - */ "description": string; - /** - * そのユーザー、ミスキストにつき - */ "flavor": string; }; "_login200": { - /** - * 常連Ⅰ - */ "title": string; - /** - * 通算ログイン日数が200日 - */ "description": string; }; "_login300": { - /** - * 常連Ⅱ - */ "title": string; - /** - * 通算ログイン日数が300日 - */ "description": string; }; "_login400": { - /** - * 常連Ⅲ - */ "title": string; - /** - * 通算ログイン日数が400日 - */ "description": string; }; "_login500": { - /** - * ベテランⅠ - */ "title": string; - /** - * 通算ログイン日数が500日 - */ "description": string; - /** - * 諸君、私はノートが好きだ - */ "flavor": string; }; "_login600": { - /** - * ベテランⅡ - */ "title": string; - /** - * 通算ログイン日数が600日 - */ "description": string; }; "_login700": { - /** - * ベテランⅢ - */ "title": string; - /** - * 通算ログイン日数が700日 - */ "description": string; }; "_login800": { - /** - * ノートマスターⅠ - */ "title": string; - /** - * 通算ログイン日数が800日 - */ "description": string; }; "_login900": { - /** - * ノートマスターⅡ - */ "title": string; - /** - * 通算ログイン日数が900日 - */ "description": string; }; "_login1000": { - /** - * ノートマスターⅢ - */ "title": string; - /** - * 通算ログイン日数が1,000日 - */ "description": string; - /** - * Misskeyを使ってくれてありがとう! - */ "flavor": string; }; "_noteClipped1": { - /** - * クリップせずにはいられないな - */ "title": string; - /** - * 初めてノートをクリップした - */ "description": string; }; "_noteFavorited1": { - /** - * 星をみるひと - */ "title": string; - /** - * 初めてノートをお気に入りに登録した - */ "description": string; }; "_myNoteFavorited1": { - /** - * 星が欲しい - */ "title": string; - /** - * 自分のノートが他の人からお気に入りに登録された - */ "description": string; }; "_profileFilled": { - /** - * 準備万端 - */ "title": string; - /** - * プロフィール設定を行った - */ "description": string; }; "_markedAsCat": { - /** - * 吾輩は猫である - */ "title": string; - /** - * アカウントをCatとして設定した - */ "description": string; - /** - * 名前はまだない。 - */ "flavor": string; }; "_following1": { - /** - * はじめてのフォロー - */ "title": string; - /** - * 初めてフォローした - */ "description": string; }; "_following10": { - /** - * ついてく、ついてく - */ "title": string; - /** - * フォローが10人を超した - */ "description": string; }; "_following50": { - /** - * 友達たくさん - */ "title": string; - /** - * フォローが50人を超した - */ "description": string; }; "_following100": { - /** - * 友達100人 - */ "title": string; - /** - * フォローが100人を超した - */ "description": string; }; "_following300": { - /** - * 友達過多 - */ "title": string; - /** - * フォローが300人を超した - */ "description": string; }; "_followers1": { - /** - * はじめてのフォロワー - */ "title": string; - /** - * 初めてフォローされた - */ "description": string; }; "_followers10": { - /** - * フォローミー! - */ "title": string; - /** - * フォロワーが10人を超した - */ "description": string; }; "_followers50": { - /** - * ぞろぞろ - */ "title": string; - /** - * フォロワーが50人を超した - */ "description": string; }; "_followers100": { - /** - * 人気者 - */ "title": string; - /** - * フォロワーが100人を超した - */ "description": string; }; "_followers300": { - /** - * 一列でお並びください - */ "title": string; - /** - * フォロワーが300人を超した - */ "description": string; }; "_followers500": { - /** - * 基地局 - */ "title": string; - /** - * フォロワーが500人を超した - */ "description": string; }; "_followers1000": { - /** - * インフルエンサー - */ "title": string; - /** - * フォロワーが1,000人を超した - */ "description": string; }; "_collectAchievements30": { - /** - * 実績コレクター - */ "title": string; - /** - * 実績を30個以上獲得した - */ "description": string; }; "_viewAchievements3min": { - /** - * 実績好き - */ "title": string; - /** - * 実績一覧を3分以上眺め続けた - */ "description": string; }; "_iLoveMisskey": { - /** - * I Love Misskey - */ "title": string; - /** - * "I ❤ #Misskey"を投稿した - */ "description": string; - /** - * Misskeyを使ってくださりありがとうございます! by 開発チーム - */ "flavor": string; }; "_foundTreasure": { - /** - * 宝探し - */ "title": string; - /** - * 隠されたお宝を発見した - */ "description": string; }; "_client30min": { - /** - * ひとやすみ - */ "title": string; - /** - * クライアントを起動してから30分以上経過した - */ "description": string; }; "_client60min": { - /** - * Misskeyの見すぎ - */ "title": string; - /** - * クライアントを起動してから60分以上経過した - */ "description": string; }; "_noteDeletedWithin1min": { - /** - * いまのなし - */ "title": string; - /** - * 投稿してから1分以内にその投稿を削除した - */ "description": string; }; "_postedAtLateNight": { - /** - * 夜行性 - */ "title": string; - /** - * 深夜にノートを投稿した - */ "description": string; - /** - * そろそろ寝よう。 - */ "flavor": string; }; "_postedAt0min0sec": { - /** - * 時報 - */ "title": string; - /** - * 0分0秒にノートを投稿した - */ "description": string; - /** - * ポッ ポッ ポッ ピーン - */ "flavor": string; }; "_selfQuote": { - /** - * 自己言及 - */ "title": string; - /** - * 自分のノートを引用した - */ "description": string; }; "_htl20npm": { - /** - * 流れるTL - */ "title": string; - /** - * ホームタイムラインの流速が20npmを越す - */ "description": string; }; "_viewInstanceChart": { - /** - * アナリスト - */ "title": string; - /** - * サーバーのチャートを表示した - */ "description": string; }; "_outputHelloWorldOnScratchpad": { - /** - * Hello, world! - */ "title": string; - /** - * スクラッチパッドで hello world を出力した - */ "description": string; }; "_open3windows": { - /** - * マルチウィンドウ - */ "title": string; - /** - * ウィンドウを3つ以上開いた状態にした - */ "description": string; }; "_driveFolderCircularReference": { - /** - * 循環参照 - */ "title": string; - /** - * ドライブのフォルダを再帰的な入れ子にしようとした - */ "description": string; }; "_reactWithoutRead": { - /** - * ちゃんと読んだ? - */ "title": string; - /** - * 100文字以上のテキストを含むノートに投稿されてから3秒以内にリアクションした - */ "description": string; }; "_clickedClickHere": { - /** - * ここをクリック - */ "title": string; - /** - * ここをクリックした - */ "description": string; }; "_justPlainLucky": { - /** - * 単なるラッキー - */ "title": string; - /** - * 10秒ごとに0.005%の確率で獲得 - */ "description": string; }; "_setNameToSyuilo": { - /** - * 神様コンプレックス - */ "title": string; - /** - * 名前を syuilo に設定した - */ "description": string; }; "_passedSinceAccountCreated1": { - /** - * 一周年 - */ "title": string; - /** - * アカウント作成から1年経過した - */ "description": string; }; "_passedSinceAccountCreated2": { - /** - * 二周年 - */ "title": string; - /** - * アカウント作成から2年経過した - */ "description": string; }; "_passedSinceAccountCreated3": { - /** - * 三周年 - */ "title": string; - /** - * アカウント作成から3年経過した - */ "description": string; }; "_loggedInOnBirthday": { - /** - * ハッピーバースデー - */ "title": string; - /** - * 誕生日にログインした - */ "description": string; }; "_loggedInOnNewYearsDay": { - /** - * あけましておめでとうございます - */ "title": string; - /** - * 元日にログインした - */ "description": string; - /** - * 今年も弊サーバーをよろしくお願いします - */ "flavor": string; }; "_cookieClicked": { - /** - * クッキーをクリックするゲーム - */ "title": string; - /** - * クッキーをクリックした - */ "description": string; - /** - * ソフト間違ってない? - */ "flavor": string; }; "_brainDiver": { - /** - * Brain Diver - */ "title": string; - /** - * Brain Diverへのリンクを投稿した - */ "description": string; - /** - * Misskey-Misskey La-Tu-Ma - */ - "flavor": string; - }; - "_smashTestNotificationButton": { - /** - * テスト過剰 - */ - "title": string; - /** - * 通知のテストをごく短時間のうちに連続して行った - */ - "description": string; - }; - "_tutorialCompleted": { - /** - * Misskey初心者講座 修了証 - */ - "title": string; - /** - * チュートリアルを完了した - */ - "description": string; - }; - "_bubbleGameExplodingHead": { - /** - * 🤯 - */ - "title": string; - /** - * バブルゲームで最も大きいモノを出した - */ - "description": string; - }; - "_bubbleGameDoubleExplodingHead": { - /** - * ダブル🤯 - */ - "title": string; - /** - * バブルゲームで最も大きいモノを2つ同時に出した - */ - "description": string; - /** - * これくらいの おべんとばこに 🤯 🤯 ちょっとつめて - */ "flavor": string; }; }; }; "_role": { - /** - * ロールの作成 - */ "new": string; - /** - * ロールの編集 - */ "edit": string; - /** - * ロール名 - */ "name": string; - /** - * ロールの説明 - */ "description": string; - /** - * ロールの権限 - */ "permission": string; - /** - * モデレーターは基本的なモデレーションに関する操作を行えます。 - * 管理者はサーバーの全ての設定を変更できます。 - */ "descriptionOfPermission": string; - /** - * アサイン - */ "assignTarget": string; - /** - * マニュアルは誰がこのロールに含まれるかを手動で管理します。 - * コンディショナルは条件を設定し、それに合致するユーザーが自動で含まれるようになります。 - */ "descriptionOfAssignTarget": string; - /** - * マニュアル - */ "manual": string; - /** - * マニュアルロール - */ - "manualRoles": string; - /** - * コンディショナル - */ "conditional": string; - /** - * コンディショナルロール - */ - "conditionalRoles": string; - /** - * 条件 - */ "condition": string; - /** - * これはコンディショナルロールです。 - */ "isConditionalRole": string; - /** - * 公開ロール - */ "isPublic": string; - /** - * ユーザーのプロフィールでこのロールが表示されます。 - */ "descriptionOfIsPublic": string; - /** - * オプション - */ "options": string; - /** - * ポリシー - */ "policies": string; - /** - * ベースロール - */ "baseRole": string; - /** - * ベースロールの値を使用 - */ "useBaseValue": string; - /** - * アサインするロールを選択 - */ "chooseRoleToAssign": string; - /** - * アイコン画像のURL - */ "iconUrl": string; - /** - * バッジとして表示 - */ "asBadge": string; - /** - * オンにすると、ユーザー名の横にロールのアイコンが表示されます。 - */ "descriptionOfAsBadge": string; - /** - * ユーザーを見つけやすくする - */ "isExplorable": string; - /** - * オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。 - */ "descriptionOfIsExplorable": string; - /** - * 表示順 - */ "displayOrder": string; - /** - * 数値が大きいほどUI上で先頭に表示されます。 - */ "descriptionOfDisplayOrder": string; - /** - * アサイン状態を移行先アカウントにも引き継ぐ - */ - "preserveAssignmentOnMoveAccount": string; - /** - * オンにすると、このロールが付与されたアカウントが移行された際に、移行先アカウントにもこのロールが引き継がれるようになります。 - */ - "preserveAssignmentOnMoveAccount_description": string; - /** - * モデレーターのメンバー編集を許可 - */ "canEditMembersByModerator": string; - /** - * オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。 - */ "descriptionOfCanEditMembersByModerator": string; - /** - * 優先度 - */ "priority": string; "_priority": { - /** - * 低 - */ "low": string; - /** - * 中 - */ "middle": string; - /** - * 高 - */ "high": string; }; "_options": { - /** - * グローバルタイムラインの閲覧 - */ "gtlAvailable": string; - /** - * ローカルタイムラインの閲覧 - */ "ltlAvailable": string; - /** - * パブリック投稿の許可 - */ "canPublicNote": string; - /** - * ノート内の最大メンション数 - */ - "mentionMax": string; - /** - * サーバー招待コードの発行 - */ "canInvite": string; - /** - * 招待コードの作成可能数 - */ - "inviteLimit": string; - /** - * 招待コードの発行間隔 - */ - "inviteLimitCycle": string; - /** - * 招待コードの有効期限 - */ - "inviteExpirationTime": string; - /** - * カスタム絵文字の管理 - */ "canManageCustomEmojis": string; - /** - * アバターデコレーションの管理 - */ - "canManageAvatarDecorations": string; - /** - * ドライブ容量 - */ "driveCapacity": string; - /** - * アップロード可能な最大ファイルサイズ - */ - "maxFileSize": string; - /** - * ファイルにNSFWを常に付与 - */ "alwaysMarkNsfw": string; - /** - * アイコンとバナーの更新を許可 - */ - "canUpdateBioMedia": string; - /** - * ノートのピン留めの最大数 - */ "pinMax": string; - /** - * アンテナの作成可能数 - */ "antennaMax": string; - /** - * ワードミュートの最大文字数 - */ "wordMuteMax": string; - /** - * Webhookの作成可能数 - */ "webhookMax": string; - /** - * クリップの作成可能数 - */ "clipMax": string; - /** - * クリップ内のノートの最大数 - */ "noteEachClipsMax": string; - /** - * ユーザーリストの作成可能数 - */ "userListMax": string; - /** - * ユーザーリスト内のユーザーの最大数 - */ "userEachUserListsMax": string; - /** - * レートリミット - */ "rateLimitFactor": string; - /** - * 小さいほど制限が緩和され、大きいほど制限が強化されます。 - */ "descriptionOfRateLimitFactor": string; - /** - * 広告の非表示 - */ "canHideAds": string; - /** - * ノート検索の利用 - */ "canSearchNotes": string; - /** - * 翻訳機能の利用 - */ - "canUseTranslator": string; - /** - * アイコンデコレーションの最大取付個数 - */ - "avatarDecorationLimit": string; - /** - * アンテナのインポートを許可 - */ - "canImportAntennas": string; - /** - * ブロックのインポートを許可 - */ - "canImportBlocking": string; - /** - * フォローのインポートを許可 - */ - "canImportFollowing": string; - /** - * ミュートのインポートを許可 - */ - "canImportMuting": string; - /** - * リストのインポートを許可 - */ - "canImportUserLists": string; - /** - * チャットを許可 - */ - "chatAvailability": string; - /** - * アップロード可能なファイル種別 - */ - "uploadableFileTypes": string; - /** - * MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*) - */ - "uploadableFileTypes_caption": string; - /** - * ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。 - */ - "uploadableFileTypes_caption2": ParameterizedString<"x">; }; "_condition": { - /** - * マニュアルロールにアサイン済み - */ - "roleAssignedTo": string; - /** - * ローカルユーザー - */ "isLocal": string; - /** - * リモートユーザー - */ "isRemote": string; - /** - * 猫ユーザー - */ - "isCat": string; - /** - * botユーザー - */ - "isBot": string; - /** - * サスペンド済みユーザー - */ - "isSuspended": string; - /** - * 鍵アカウントユーザー - */ - "isLocked": string; - /** - * 「アカウントを見つけやすくする」が有効なユーザー - */ - "isExplorable": string; - /** - * アカウント作成から~以内 - */ "createdLessThan": string; - /** - * アカウント作成から~経過 - */ "createdMoreThan": string; - /** - * フォロワー数が~以下 - */ "followersLessThanOrEq": string; - /** - * フォロワー数が~以上 - */ "followersMoreThanOrEq": string; - /** - * フォロー数が~以下 - */ "followingLessThanOrEq": string; - /** - * フォロー数が~以上 - */ "followingMoreThanOrEq": string; - /** - * 投稿数が~以下 - */ "notesLessThanOrEq": string; - /** - * 投稿数が~以上 - */ "notesMoreThanOrEq": string; - /** - * ~かつ~ - */ "and": string; - /** - * ~または~ - */ "or": string; - /** - * ~ではない - */ "not": string; }; }; "_sensitiveMediaDetection": { - /** - * 機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。 - */ "description": string; - /** - * 検出感度 - */ "sensitivity": string; - /** - * 感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。 - */ "sensitivityDescription": string; - /** - * センシティブフラグを設定する - */ "setSensitiveFlagAutomatically": string; - /** - * この設定をオフにしても内部的に判定結果は保持されます。 - */ "setSensitiveFlagAutomaticallyDescription": string; - /** - * 動画の解析を有効化 - */ "analyzeVideos": string; - /** - * 静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。 - */ "analyzeVideosDescription": string; }; "_emailUnavailable": { - /** - * 既に使用されています - */ "used": string; - /** - * 形式が正しくありません - */ "format": string; - /** - * 恒久的に使用可能なアドレスではありません - */ "disposable": string; - /** - * 正しいメールサーバーではありません - */ "mx": string; - /** - * メールサーバーが応答しません - */ "smtp": string; - /** - * このメールアドレスでは登録できません - */ - "banned": string; }; "_ffVisibility": { - /** - * 公開 - */ "public": string; - /** - * フォロワーだけに公開 - */ "followers": string; - /** - * 非公開 - */ "private": string; }; "_signup": { - /** - * ほとんど完了です - */ "almostThere": string; - /** - * あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。 - */ "emailAddressInfo": string; - /** - * 入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。 - */ - "emailSent": ParameterizedString<"email">; + "emailSent": string; }; "_accountDelete": { - /** - * アカウントの削除 - */ "accountDelete": string; - /** - * アカウントの削除は負荷のかかる処理であるため、作成したコンテンツの数やアップロードしたファイルの数が多いと完了までに時間がかかることがあります。 - */ "mayTakeTime": string; - /** - * アカウントの削除が完了する際は、登録してあったメールアドレス宛に通知を送信します。 - */ "sendEmail": string; - /** - * アカウント削除をリクエスト - */ "requestAccountDelete": string; - /** - * 削除処理が開始されました。 - */ "started": string; - /** - * 削除が進行中 - */ "inProgress": string; }; "_ad": { - /** - * 戻る - */ "back": string; - /** - * この広告の表示頻度を下げる - */ "reduceFrequencyOfThisAd": string; - /** - * 表示しない - */ "hide": string; - /** - * 曜日はサーバーのタイムゾーンを元に指定されます。 - */ "timezoneinfo": string; - /** - * 広告配信設定 - */ - "adsSettings": string; - /** - * リアルタイム更新中に広告を配信する間隔(ノートの個数) - */ - "notesPerOneAd": string; - /** - * 0でリアルタイム更新時の広告配信を無効 - */ - "setZeroToDisable": string; - /** - * 広告の配信間隔が極めて短いため、ユーザー体験が著しく損われる可能性があります。 - */ - "adsTooClose": string; }; "_forgotPassword": { - /** - * アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。 - */ "enterEmail": string; - /** - * メールアドレスを登録していない場合は、管理者までお問い合わせください。 - */ "ifNoEmail": string; - /** - * このサーバーではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。 - */ "contactAdmin": string; }; "_gallery": { - /** - * 自分の投稿 - */ "my": string; - /** - * いいねした投稿 - */ "liked": string; - /** - * いいね! - */ "like": string; - /** - * いいね解除 - */ "unlike": string; }; "_email": { "_follow": { - /** - * フォローされました - */ "title": string; }; "_receiveFollowRequest": { - /** - * フォローリクエストを受け取りました - */ "title": string; }; }; "_plugin": { - /** - * プラグインのインストール - */ "install": string; - /** - * 信頼できないプラグインはインストールしないでください。 - */ "installWarn": string; - /** - * プラグインの管理 - */ "manage": string; - /** - * ソースを表示 - */ - "viewSource": string; - /** - * ログを表示 - */ - "viewLog": string; }; "_preferencesBackups": { - /** - * 作成したバックアップ - */ "list": string; - /** - * 新規保存 - */ "saveNew": string; - /** - * ファイルを読み込み - */ "loadFile": string; - /** - * このデバイスに適用 - */ "apply": string; - /** - * 上書き保存 - */ "save": string; - /** - * バックアップ名を入力 - */ "inputName": string; - /** - * 保存できません - */ "cannotSave": string; - /** - * バックアップ名「{name}」は既に存在します。違う名前を指定してください。 - */ - "nameAlreadyExists": ParameterizedString<"name">; - /** - * バックアップ「{name}」を現在のデバイスに適用しますか?現在のデバイス設定は失われます。 - */ - "applyConfirm": ParameterizedString<"name">; - /** - * {name}に上書き保存しますか? - */ - "saveConfirm": ParameterizedString<"name">; - /** - * {name}を削除しますか? - */ - "deleteConfirm": ParameterizedString<"name">; - /** - * 「{old}」を「{new}」に変更しますか? - */ - "renameConfirm": ParameterizedString<"old" | "new">; - /** - * バックアップはありません。「新規保存」で現在のクライアント設定をサーバーに保存できます。 - */ + "nameAlreadyExists": string; + "applyConfirm": string; + "saveConfirm": string; + "deleteConfirm": string; + "renameConfirm": string; "noBackups": string; - /** - * 作成日時: {date} {time} - */ - "createdAt": ParameterizedString<"date" | "time">; - /** - * 更新日時: {date} {time} - */ - "updatedAt": ParameterizedString<"date" | "time">; - /** - * 読み込みできません - */ + "createdAt": string; + "updatedAt": string; "cannotLoad": string; - /** - * ファイル形式が違います。 - */ "invalidFile": string; }; "_registry": { - /** - * スコープ - */ "scope": string; - /** - * キー - */ "key": string; - /** - * キー - */ "keys": string; - /** - * ドメイン - */ "domain": string; - /** - * キーを作成 - */ "createKey": string; }; "_aboutMisskey": { - /** - * Misskeyはsyuiloによって2014年から開発されている、オープンソースのソフトウェアです。 - */ "about": string; - /** - * コントリビューター - */ "contributors": string; - /** - * 全てのコントリビューター - */ "allContributors": string; - /** - * ソースコード - */ "source": string; - /** - * オリジナル - */ - "original": string; - /** - * {name}はオリジナルのMisskeyを改変したバージョンを使用しています。 - */ - "thisIsModifiedVersion": ParameterizedString<"name">; - /** - * Misskeyを翻訳 - */ "translation": string; - /** - * Misskeyに寄付 - */ "donate": string; - /** - * 他にも多くの方が支援してくれています。ありがとうございます🥰 - */ "morePatrons": string; - /** - * 支援者 - */ "patrons": string; - /** - * プロジェクトメンバー - */ - "projectMembers": string; }; "_displayOfSensitiveMedia": { - /** - * センシティブ設定されたメディアを隠す - */ "respect": string; - /** - * センシティブ設定されたメディアを隠さない - */ "ignore": string; - /** - * 常にメディアを隠す - */ "force": string; }; "_instanceTicker": { - /** - * 表示しない - */ "none": string; - /** - * リモートユーザーに表示 - */ "remote": string; - /** - * 常に表示 - */ "always": string; }; "_serverDisconnectedBehavior": { - /** - * 自動でリロード - */ "reload": string; - /** - * ダイアログで警告 - */ "dialog": string; - /** - * 控えめに警告 - */ "quiet": string; }; "_channel": { - /** - * チャンネルを作成 - */ "create": string; - /** - * チャンネルを編集 - */ "edit": string; - /** - * バナーを設定 - */ "setBanner": string; - /** - * バナーを削除 - */ "removeBanner": string; - /** - * トレンド - */ "featured": string; - /** - * 管理中 - */ "owned": string; - /** - * フォロー中 - */ "following": string; - /** - * {n}人が参加中 - */ - "usersCount": ParameterizedString<"n">; - /** - * {n}投稿があります - */ - "notesCount": ParameterizedString<"n">; - /** - * 名前と説明 - */ + "usersCount": string; + "notesCount": string; "nameAndDescription": string; - /** - * 名前のみ - */ "nameOnly": string; - /** - * チャンネル外へのリノートと引用リノートを許可する - */ - "allowRenoteToExternal": string; }; "_menuDisplay": { - /** - * 横 - */ "sideFull": string; - /** - * 横(アイコン) - */ "sideIcon": string; - /** - * 上部 - */ "top": string; - /** - * 隠す - */ "hide": string; }; "_wordMute": { - /** - * ミュートするワード - */ "muteWords": string; - /** - * スペースで区切るとAND指定になり、改行で区切るとOR指定になります。 - */ "muteWordsDescription": string; - /** - * キーワードをスラッシュで囲むと正規表現になります。 - */ "muteWordsDescription2": string; + "softDescription": string; + "hardDescription": string; + "soft": string; + "hard": string; + "mutedNotes": string; }; "_instanceMute": { - /** - * ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。 - */ "instanceMuteDescription": string; - /** - * 改行で区切って設定します - */ "instanceMuteDescription2": string; - /** - * 設定したサーバーのノートを隠します。 - */ "title": string; - /** - * ミュートするサーバー - */ "heading": string; }; "_theme": { - /** - * テーマを探す - */ "explore": string; - /** - * テーマのインストール - */ "install": string; - /** - * テーマの管理 - */ "manage": string; - /** - * テーマコード - */ "code": string; - /** - * 説明 - */ "description": string; - /** - * {name}をインストールしました - */ - "installed": ParameterizedString<"name">; - /** - * インストールされたテーマ - */ + "installed": string; "installedThemes": string; - /** - * 標準のテーマ - */ "builtinThemes": string; - /** - * サーバーのテーマ - */ - "instanceTheme": string; - /** - * そのテーマは既にインストールされています - */ "alreadyInstalled": string; - /** - * テーマの形式が間違っています - */ "invalid": string; - /** - * テーマを作る - */ "make": string; - /** - * ベース - */ "base": string; - /** - * 定数を追加 - */ "addConstant": string; - /** - * 定数 - */ "constant": string; - /** - * デフォルト値 - */ "defaultValue": string; - /** - * 色 - */ "color": string; - /** - * プロパティを参照 - */ "refProp": string; - /** - * 定数を参照 - */ "refConst": string; - /** - * キー - */ "key": string; - /** - * 関数 - */ "func": string; - /** - * 関数の種類 - */ "funcKind": string; - /** - * 引数 - */ "argument": string; - /** - * 元にするプロパティの名前 - */ "basedProp": string; - /** - * 不透明度 - */ "alpha": string; - /** - * 暗さ - */ "darken": string; - /** - * 明るさ - */ "lighten": string; - /** - * 定数名を入力してください - */ "inputConstantName": string; - /** - * ここにテーマコードを貼り付けて、エディターにインポートできます - */ "importInfo": string; - /** - * 定数 {const} を削除しても良いですか? - */ - "deleteConstantConfirm": ParameterizedString<"const">; + "deleteConstantConfirm": string; "keys": { - /** - * アクセント - */ "accent": string; - /** - * 背景 - */ "bg": string; - /** - * 文字 - */ "fg": string; - /** - * フォーカス - */ "focus": string; - /** - * インジケーター - */ "indicator": string; - /** - * パネル - */ "panel": string; - /** - * 影 - */ "shadow": string; - /** - * ヘッダー - */ "header": string; - /** - * ナビゲーションバーの背景 - */ "navBg": string; - /** - * ナビゲーションバーの文字 - */ "navFg": string; - /** - * ナビゲーションバー文字(アクティブ) - */ + "navHoverFg": string; "navActive": string; - /** - * ナビゲーションバーのインジケーター - */ "navIndicator": string; - /** - * リンク - */ "link": string; - /** - * ハッシュタグ - */ "hashtag": string; - /** - * メンション - */ "mention": string; - /** - * あなた宛てメンション - */ "mentionMe": string; - /** - * リノート - */ "renote": string; - /** - * モーダルの背景 - */ "modalBg": string; - /** - * 分割線 - */ "divider": string; - /** - * スクロールバーの取っ手 - */ "scrollbarHandle": string; - /** - * スクロールバーの取っ手(ホバー) - */ "scrollbarHandleHover": string; - /** - * 日付ラベルの文字 - */ "dateLabelFg": string; - /** - * 情報の背景 - */ "infoBg": string; - /** - * 情報の文字 - */ "infoFg": string; - /** - * 警告の背景 - */ "infoWarnBg": string; - /** - * 警告の文字 - */ "infoWarnFg": string; - /** - * 通知トーストの背景 - */ + "cwBg": string; + "cwFg": string; + "cwHoverBg": string; "toastBg": string; - /** - * 通知トーストの文字 - */ "toastFg": string; - /** - * ボタンの背景 - */ "buttonBg": string; - /** - * ボタンの背景 (ホバー) - */ "buttonHoverBg": string; - /** - * 入力ボックスの縁取り - */ "inputBorder": string; - /** - * バッジ - */ + "listItemHoverBg": string; + "driveFolderBg": string; + "wallpaperOverlay": string; "badge": string; - /** - * チャットの背景 - */ "messageBg": string; - /** - * 強調された文字 - */ + "accentDarken": string; + "accentLighten": string; "fgHighlighted": string; }; }; "_sfx": { - /** - * ノート - */ "note": string; - /** - * ノート(自分) - */ "noteMy": string; - /** - * 通知 - */ "notification": string; - /** - * リアクション選択時 - */ - "reaction": string; - /** - * チャットのメッセージ - */ - "chatMessage": string; - }; - "_soundSettings": { - /** - * ドライブの音声を使用 - */ - "driveFile": string; - /** - * ドライブのファイルを選択してください - */ - "driveFileWarn": string; - /** - * このファイルは対応していません - */ - "driveFileTypeWarn": string; - /** - * 音声ファイルを選択してください - */ - "driveFileTypeWarnDescription": string; - /** - * 音声が長すぎます - */ - "driveFileDurationWarn": string; - /** - * 長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか? - */ - "driveFileDurationWarnDescription": string; - /** - * 音声が読み込めませんでした。設定を変更してください - */ - "driveFileError": string; + "chat": string; + "chatBg": string; + "antenna": string; + "channel": string; }; "_ago": { - /** - * 未来 - */ "future": string; - /** - * たった今 - */ "justNow": string; - /** - * {n}秒前 - */ - "secondsAgo": ParameterizedString<"n">; - /** - * {n}分前 - */ - "minutesAgo": ParameterizedString<"n">; - /** - * {n}時間前 - */ - "hoursAgo": ParameterizedString<"n">; - /** - * {n}日前 - */ - "daysAgo": ParameterizedString<"n">; - /** - * {n}週間前 - */ - "weeksAgo": ParameterizedString<"n">; - /** - * {n}ヶ月前 - */ - "monthsAgo": ParameterizedString<"n">; - /** - * {n}年前 - */ - "yearsAgo": ParameterizedString<"n">; - /** - * 日時の解析に失敗 - */ + "secondsAgo": string; + "minutesAgo": string; + "hoursAgo": string; + "daysAgo": string; + "weeksAgo": string; + "monthsAgo": string; + "yearsAgo": string; "invalid": string; }; - "_timeIn": { - /** - * {n}秒後 - */ - "seconds": ParameterizedString<"n">; - /** - * {n}分後 - */ - "minutes": ParameterizedString<"n">; - /** - * {n}時間後 - */ - "hours": ParameterizedString<"n">; - /** - * {n}日後 - */ - "days": ParameterizedString<"n">; - /** - * {n}週間後 - */ - "weeks": ParameterizedString<"n">; - /** - * {n}ヶ月後 - */ - "months": ParameterizedString<"n">; - /** - * {n}年後 - */ - "years": ParameterizedString<"n">; - }; "_time": { - /** - * 秒 - */ "second": string; - /** - * 分 - */ "minute": string; - /** - * 時間 - */ "hour": string; - /** - * 日 - */ "day": string; }; + "_timelineTutorial": { + "title": string; + "step1_1": string; + "step1_2": string; + "step2_1": string; + "step2_2": string; + "step3_1": string; + "step3_2": string; + "step4_1": string; + "step4_2": string; + }; "_2fa": { - /** - * 既に設定は完了しています。 - */ "alreadyRegistered": string; - /** - * 認証アプリの設定を開始 - */ "registerTOTP": string; - /** - * まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。 - */ - "step1": ParameterizedString<"a" | "b">; - /** - * 次に、表示されているQRコードをアプリでスキャンするか、ボタンをクリックして端末上でアプリを開きます。 - */ + "passwordToTOTP": string; + "step1": string; "step2": string; - /** - * デスクトップアプリを使用する場合は次のURIを入力します - */ - "step2Uri": string; - /** - * 確認コードを入力 - */ + "step2Click": string; + "step2Url": string; "step3Title": string; - /** - * アプリに表示されている確認コード(トークン)を入力します。 - */ "step3": string; - /** - * 設定が完了しました - */ - "setupCompleted": string; - /** - * これからログインするときも、同じようにコードを入力します。 - */ "step4": string; - /** - * お使いのブラウザはセキュリティキーに対応していません。 - */ "securityKeyNotSupported": string; - /** - * セキュリティキー・パスキーを登録するには、まず認証アプリの設定を行なってください。 - */ "registerTOTPBeforeKey": string; - /** - * FIDO2をサポートするハードウェアセキュリティキー、端末の生体認証やPINロック、パスキーといった、WebAuthn由来の鍵を登録します。 - */ "securityKeyInfo": string; - /** - * セキュリティキー・パスキーを登録する - */ + "chromePasskeyNotSupported": string; "registerSecurityKey": string; - /** - * キーの名前を入力 - */ "securityKeyName": string; - /** - * ブラウザの指示に従い、セキュリティキーやパスキーを登録してください - */ "tapSecurityKey": string; - /** - * セキュリティキーを削除 - */ "removeKey": string; - /** - * {name}を削除しますか? - */ - "removeKeyConfirm": ParameterizedString<"name">; - /** - * セキュリティキーが登録されている場合、認証アプリの設定は解除できません。 - */ + "removeKeyConfirm": string; "whyTOTPOnlyRenew": string; - /** - * 認証アプリを再設定 - */ "renewTOTP": string; - /** - * 今までの認証アプリの確認コードおよびバックアップコードは使用できなくなります - */ "renewTOTPConfirm": string; - /** - * 再設定する - */ "renewTOTPOk": string; - /** - * やめておく - */ "renewTOTPCancel": string; - /** - * このウィザードを閉じる前に、以下のバックアップコードを確認してください。 - */ - "checkBackupCodesBeforeCloseThisWizard": string; - /** - * バックアップコード - */ - "backupCodes": string; - /** - * 認証アプリが使用できなくなった場合、以下のバックアップコードを使ってアカウントにアクセスできます。これらのコードは必ず安全な場所に保管してください。各コードは一回だけ使用できます。 - */ - "backupCodesDescription": string; - /** - * バックアップコードが使用されました。認証アプリが使えなくなっている場合、なるべく早く認証アプリを再設定してください。 - */ - "backupCodeUsedWarning": string; - /** - * バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。 - */ - "backupCodesExhaustedWarning": string; - /** - * 詳細なガイドはこちら - */ - "moreDetailedGuideHere": string; }; "_permissions": { - /** - * アカウントの情報を見る - */ "read:account": string; - /** - * アカウントの情報を変更する - */ "write:account": string; - /** - * ブロックを見る - */ "read:blocks": string; - /** - * ブロックを操作する - */ "write:blocks": string; - /** - * ドライブを見る - */ "read:drive": string; - /** - * ドライブを操作する - */ "write:drive": string; - /** - * お気に入りを見る - */ "read:favorites": string; - /** - * お気に入りを操作する - */ "write:favorites": string; - /** - * フォローの情報を見る - */ "read:following": string; - /** - * フォロー・フォロー解除する - */ "write:following": string; - /** - * チャットを見る - */ "read:messaging": string; - /** - * チャットを操作する - */ "write:messaging": string; - /** - * ミュートを見る - */ "read:mutes": string; - /** - * ミュートを操作する - */ "write:mutes": string; - /** - * ノートを作成・削除する - */ "write:notes": string; - /** - * 通知を見る - */ "read:notifications": string; - /** - * 通知を操作する - */ "write:notifications": string; - /** - * リアクションを見る - */ "read:reactions": string; - /** - * リアクションを操作する - */ "write:reactions": string; - /** - * 投票する - */ "write:votes": string; - /** - * ページを見る - */ "read:pages": string; - /** - * ページを操作する - */ "write:pages": string; - /** - * ページのいいねを見る - */ "read:page-likes": string; - /** - * ページのいいねを操作する - */ "write:page-likes": string; - /** - * ユーザーグループを見る - */ "read:user-groups": string; - /** - * ユーザーグループを操作する - */ "write:user-groups": string; - /** - * チャンネルを見る - */ "read:channels": string; - /** - * チャンネルを操作する - */ "write:channels": string; - /** - * ギャラリーを見る - */ "read:gallery": string; - /** - * ギャラリーを操作する - */ "write:gallery": string; - /** - * ギャラリーのいいねを見る - */ "read:gallery-likes": string; - /** - * ギャラリーのいいねを操作する - */ "write:gallery-likes": string; - /** - * Playを見る - */ - "read:flash": string; - /** - * Playを操作する - */ - "write:flash": string; - /** - * Playのいいねを見る - */ - "read:flash-likes": string; - /** - * Playのいいねを操作する - */ - "write:flash-likes": string; - /** - * ユーザーからの通報を見る - */ - "read:admin:abuse-user-reports": string; - /** - * ユーザーアカウントを削除する - */ - "write:admin:delete-account": string; - /** - * ユーザーのすべてのファイルを削除する - */ - "write:admin:delete-all-files-of-a-user": string; - /** - * データベースインデックスに関する情報を見る - */ - "read:admin:index-stats": string; - /** - * データベーステーブルに関する情報を見る - */ - "read:admin:table-stats": string; - /** - * ユーザーのIPアドレスを見る - */ - "read:admin:user-ips": string; - /** - * インスタンスのメタデータを見る - */ - "read:admin:meta": string; - /** - * ユーザーのパスワードをリセットする - */ - "write:admin:reset-password": string; - /** - * ユーザーからの通報を解決する - */ - "write:admin:resolve-abuse-user-report": string; - /** - * メールを送る - */ - "write:admin:send-email": string; - /** - * サーバーの情報を見る - */ - "read:admin:server-info": string; - /** - * モデレーションログを見る - */ - "read:admin:show-moderation-log": string; - /** - * ユーザーのプライベートな情報を見る - */ - "read:admin:show-user": string; - /** - * ユーザーを凍結する - */ - "write:admin:suspend-user": string; - /** - * ユーザーのアバターを削除する - */ - "write:admin:unset-user-avatar": string; - /** - * ユーザーのバーナーを削除する - */ - "write:admin:unset-user-banner": string; - /** - * ユーザーの凍結を解除する - */ - "write:admin:unsuspend-user": string; - /** - * インスタンスのメタデータを操作する - */ - "write:admin:meta": string; - /** - * モデレーションノートを操作する - */ - "write:admin:user-note": string; - /** - * ロールを操作する - */ - "write:admin:roles": string; - /** - * ロールを見る - */ - "read:admin:roles": string; - /** - * リレーを操作する - */ - "write:admin:relays": string; - /** - * リレーを見る - */ - "read:admin:relays": string; - /** - * 招待コードを操作する - */ - "write:admin:invite-codes": string; - /** - * 招待コードを見る - */ - "read:admin:invite-codes": string; - /** - * お知らせを操作する - */ - "write:admin:announcements": string; - /** - * お知らせを見る - */ - "read:admin:announcements": string; - /** - * アバターデコレーションを操作する - */ - "write:admin:avatar-decorations": string; - /** - * アバターデコレーションを見る - */ - "read:admin:avatar-decorations": string; - /** - * 連合に関する情報を操作する - */ - "write:admin:federation": string; - /** - * ユーザーアカウントを操作する - */ - "write:admin:account": string; - /** - * ユーザーに関する情報を見る - */ - "read:admin:account": string; - /** - * 絵文字を操作する - */ - "write:admin:emoji": string; - /** - * 絵文字を見る - */ - "read:admin:emoji": string; - /** - * ジョブキューを操作する - */ - "write:admin:queue": string; - /** - * ジョブキューに関する情報を見る - */ - "read:admin:queue": string; - /** - * プロモーションノートを操作する - */ - "write:admin:promo": string; - /** - * ユーザーのドライブを操作する - */ - "write:admin:drive": string; - /** - * ユーザーのドライブの関する情報を見る - */ - "read:admin:drive": string; - /** - * 管理者用のWebsocket APIを使う - */ - "read:admin:stream": string; - /** - * 広告を操作する - */ - "write:admin:ad": string; - /** - * 広告を見る - */ - "read:admin:ad": string; - /** - * 招待コードを作成する - */ - "write:invite-codes": string; - /** - * 招待コードを取得する - */ - "read:invite-codes": string; - /** - * クリップのいいねを操作する - */ - "write:clip-favorite": string; - /** - * クリップのいいねを見る - */ - "read:clip-favorite": string; - /** - * 連合に関する情報を取得する - */ - "read:federation": string; - /** - * 違反を報告する - */ - "write:report-abuse": string; - /** - * チャットを操作する - */ - "write:chat": string; - /** - * チャットを閲覧する - */ - "read:chat": string; }; "_auth": { - /** - * アプリへのアクセス許可 - */ "shareAccessTitle": string; - /** - * 「{name}」がアカウントにアクセスすることを許可しますか? - */ - "shareAccess": ParameterizedString<"name">; - /** - * アカウントへのアクセスを許可しますか? - */ + "shareAccess": string; "shareAccessAsk": string; - /** - * {name}は次の権限を要求しています - */ - "permission": ParameterizedString<"name">; - /** - * このアプリは次の権限を要求しています - */ + "permission": string; "permissionAsk": string; - /** - * アプリケーションに戻ってやっていってください - */ "pleaseGoBack": string; - /** - * アプリケーションに戻っています - */ "callback": string; - /** - * アクセスを許可しました - */ - "accepted": string; - /** - * アクセスを拒否しました - */ "denied": string; - /** - * 以下のユーザーとして操作しています - */ - "scopeUser": string; - /** - * アプリケーションにアクセス許可を与えるには、ログインが必要です。 - */ "pleaseLogin": string; - /** - * アクセスを許可すると、自動で以下のURLに遷移します - */ - "byClickingYouWillBeRedirectedToThisUrl": string; }; "_antennaSources": { - /** - * 全てのノート - */ "all": string; - /** - * フォローしているユーザーのノート - */ "homeTimeline": string; - /** - * 指定した一人または複数のユーザーのノート - */ "users": string; - /** - * 指定したリストのユーザーのノート - */ "userList": string; - /** - * 指定した一人または複数のユーザーを除いた全てのノート - */ - "userBlacklist": string; }; "_weekday": { - /** - * 日曜日 - */ "sunday": string; - /** - * 月曜日 - */ "monday": string; - /** - * 火曜日 - */ "tuesday": string; - /** - * 水曜日 - */ "wednesday": string; - /** - * 木曜日 - */ "thursday": string; - /** - * 金曜日 - */ "friday": string; - /** - * 土曜日 - */ "saturday": string; }; "_widgets": { - /** - * プロフィール - */ "profile": string; - /** - * サーバー情報 - */ "instanceInfo": string; - /** - * 付箋 - */ "memo": string; - /** - * 通知 - */ "notifications": string; - /** - * タイムライン - */ "timeline": string; - /** - * カレンダー - */ "calendar": string; - /** - * トレンド - */ "trends": string; - /** - * 時計 - */ "clock": string; - /** - * RSSリーダー - */ "rss": string; - /** - * RSSティッカー - */ "rssTicker": string; - /** - * アクティビティ - */ "activity": string; - /** - * フォト - */ "photos": string; - /** - * デジタル時計 - */ "digitalClock": string; - /** - * UNIX時計 - */ "unixClock": string; - /** - * 連合 - */ "federation": string; - /** - * サーバークラウド - */ "instanceCloud": string; - /** - * 投稿フォーム - */ "postForm": string; - /** - * スライドショー - */ "slideshow": string; - /** - * ボタン - */ "button": string; - /** - * オンラインユーザー - */ "onlineUsers": string; - /** - * ジョブキュー - */ "jobQueue": string; - /** - * サーバーメトリクス - */ "serverMetric": string; - /** - * AiScriptコンソール - */ "aiscript": string; - /** - * AiScript App - */ "aiscriptApp": string; - /** - * 藍 - */ "aichan": string; - /** - * ユーザーリスト - */ "userList": string; "_userList": { - /** - * リストを選択 - */ "chooseList": string; }; - /** - * クリッカー - */ "clicker": string; - /** - * 今日誕生日のユーザー - */ - "birthdayFollowings": string; - /** - * チャット - */ - "chat": string; }; "_cw": { - /** - * 隠す - */ "hide": string; - /** - * もっと見る - */ "show": string; - /** - * {count}文字 - */ - "chars": ParameterizedString<"count">; - /** - * {count}ファイル - */ - "files": ParameterizedString<"count">; + "chars": string; + "files": string; }; "_poll": { - /** - * 選択肢は最低2つ必要です - */ "noOnlyOneChoice": string; - /** - * 選択肢{n} - */ - "choiceN": ParameterizedString<"n">; - /** - * これ以上追加できません - */ + "choiceN": string; "noMore": string; - /** - * 複数回答可 - */ "canMultipleVote": string; - /** - * 期限 - */ "expiration": string; - /** - * 無期限 - */ "infinite": string; - /** - * 日時指定 - */ "at": string; - /** - * 経過指定 - */ "after": string; - /** - * 期日 - */ "deadlineDate": string; - /** - * 時間 - */ "deadlineTime": string; - /** - * 期間 - */ "duration": string; - /** - * {n}票 - */ - "votesCount": ParameterizedString<"n">; - /** - * 計{n}票 - */ - "totalVotes": ParameterizedString<"n">; - /** - * 投票する - */ + "votesCount": string; + "totalVotes": string; "vote": string; - /** - * 結果を見る - */ "showResult": string; - /** - * 投票済み - */ "voted": string; - /** - * 終了済み - */ "closed": string; - /** - * 終了まであと{d}日{h}時間 - */ - "remainingDays": ParameterizedString<"d" | "h">; - /** - * 終了まであと{h}時間{m}分 - */ - "remainingHours": ParameterizedString<"h" | "m">; - /** - * 終了まであと{m}分{s}秒 - */ - "remainingMinutes": ParameterizedString<"m" | "s">; - /** - * 終了まであと{s}秒 - */ - "remainingSeconds": ParameterizedString<"s">; + "remainingDays": string; + "remainingHours": string; + "remainingMinutes": string; + "remainingSeconds": string; }; "_visibility": { - /** - * パブリック - */ "public": string; - /** - * 全てのユーザーに公開 - */ "publicDescription": string; - /** - * ホーム - */ "home": string; - /** - * ホームタイムラインのみに公開 - */ "homeDescription": string; - /** - * フォロワー - */ "followers": string; - /** - * 自分のフォロワーのみに公開 - */ "followersDescription": string; - /** - * ダイレクト - */ "specified": string; - /** - * 指定したユーザーのみに公開 - */ "specifiedDescription": string; - /** - * 連合なし - */ "disableFederation": string; - /** - * 他サーバーへの配信を行いません - */ "disableFederationDescription": string; }; "_postForm": { - /** - * このノートに返信... - */ "replyPlaceholder": string; - /** - * このノートを引用... - */ "quotePlaceholder": string; - /** - * チャンネルに投稿... - */ "channelPlaceholder": string; "_placeholders": { - /** - * いまどうしてる? - */ "a": string; - /** - * 何かありましたか? - */ "b": string; - /** - * 何をお考えですか? - */ "c": string; - /** - * 言いたいことは? - */ "d": string; - /** - * ここに書いてください - */ "e": string; - /** - * あなたが書くのを待っています... - */ "f": string; }; }; "_profile": { - /** - * 名前 - */ "name": string; - /** - * ユーザー名 - */ "username": string; - /** - * 自己紹介 - */ "description": string; - /** - * ハッシュタグを含めることができます。 - */ "youCanIncludeHashtags": string; - /** - * 追加情報 - */ "metadata": string; - /** - * 追加情報を編集 - */ "metadataEdit": string; - /** - * プロフィールに表として追加情報を表示することができます。 - */ "metadataDescription": string; - /** - * ラベル - */ "metadataLabel": string; - /** - * 内容 - */ "metadataContent": string; - /** - * アイコン画像を変更 - */ "changeAvatar": string; - /** - * バナー画像を変更 - */ "changeBanner": string; - /** - * 内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。 - */ - "verifiedLinkDescription": string; - /** - * 最大{max}つまでデコレーションを付けられます。 - */ - "avatarDecorationMax": ParameterizedString<"max">; - /** - * フォローされた時のメッセージ - */ - "followedMessage": string; - /** - * フォローされた時に相手に表示する短いメッセージを設定できます。 - */ - "followedMessageDescription": string; - /** - * フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。 - */ - "followedMessageDescriptionForLockedAccount": string; }; "_exportOrImport": { - /** - * 全てのノート - */ "allNotes": string; - /** - * お気に入りにしたノート - */ "favoritedNotes": string; - /** - * クリップ - */ - "clips": string; - /** - * フォロー - */ "followingList": string; - /** - * ミュート - */ "muteList": string; - /** - * ブロック - */ "blockingList": string; - /** - * リスト - */ "userLists": string; - /** - * ミュートしているユーザーを除外 - */ "excludeMutingUsers": string; - /** - * 使われていないアカウントを除外 - */ "excludeInactiveUsers": string; - /** - * 返信をTLに含むかの情報がファイルにない場合に、インポートした人による返信をTLに含むようにする - */ - "withReplies": string; }; "_charts": { - /** - * 連合 - */ "federation": string; - /** - * リクエスト - */ "apRequest": string; - /** - * ユーザーの増減 - */ "usersIncDec": string; - /** - * ユーザーの合計 - */ "usersTotal": string; - /** - * アクティブユーザー数 - */ "activeUsers": string; - /** - * ノートの増減 - */ "notesIncDec": string; - /** - * ローカルのノートの増減 - */ "localNotesIncDec": string; - /** - * リモートのノートの増減 - */ "remoteNotesIncDec": string; - /** - * ノートの合計 - */ "notesTotal": string; - /** - * ファイルの増減 - */ "filesIncDec": string; - /** - * ファイルの合計 - */ "filesTotal": string; - /** - * ストレージ使用量の増減 - */ "storageUsageIncDec": string; - /** - * ストレージ使用量の合計 - */ "storageUsageTotal": string; }; "_instanceCharts": { - /** - * リクエスト - */ "requests": string; - /** - * ユーザーの増減 - */ "users": string; - /** - * ユーザーの累積 - */ "usersTotal": string; - /** - * ノートの増減 - */ "notes": string; - /** - * ノートの累積 - */ "notesTotal": string; - /** - * フォロー/フォロワーの増減 - */ "ff": string; - /** - * フォロー/フォロワーの累積 - */ "ffTotal": string; - /** - * キャッシュサイズの増減 - */ "cacheSize": string; - /** - * キャッシュサイズの累積 - */ "cacheSizeTotal": string; - /** - * ファイル数の増減 - */ "files": string; - /** - * ファイル数の累積 - */ "filesTotal": string; }; "_timelines": { - /** - * ホーム - */ "home": string; - /** - * ローカル - */ "local": string; - /** - * ソーシャル - */ "social": string; - /** - * グローバル - */ "global": string; }; "_play": { - /** - * Playの作成 - */ "new": string; - /** - * Playの編集 - */ "edit": string; - /** - * Playを作成しました - */ "created": string; - /** - * Playを更新しました - */ "updated": string; - /** - * Playを削除しました - */ "deleted": string; - /** - * Play設定 - */ "pageSetting": string; - /** - * このPlayを編集 - */ "editThisPage": string; - /** - * ソースを表示 - */ "viewSource": string; - /** - * 自分のPlay - */ "my": string; - /** - * いいねしたPlay - */ "liked": string; - /** - * 人気 - */ "featured": string; - /** - * タイトル - */ "title": string; - /** - * スクリプト - */ "script": string; - /** - * 説明 - */ "summary": string; - /** - * 非公開に設定するとプロフィールに表示されなくなりますが、URLを知っている人は引き続きアクセスできます。 - */ - "visibilityDescription": string; }; "_pages": { - /** - * ページの作成 - */ "newPage": string; - /** - * ページの編集 - */ "editPage": string; - /** - * ソースを表示中 - */ "readPage": string; - /** - * ページ設定 - */ + "created": string; + "updated": string; + "deleted": string; "pageSetting": string; - /** - * 指定されたページURLは既に存在しています - */ "nameAlreadyExists": string; - /** - * 不正なページURLです - */ "invalidNameTitle": string; - /** - * 空白でないか確認してください - */ "invalidNameText": string; - /** - * このページを編集 - */ "editThisPage": string; - /** - * ソースを表示 - */ "viewSource": string; - /** - * ページを見る - */ "viewPage": string; - /** - * いいね - */ "like": string; - /** - * いいね解除 - */ "unlike": string; - /** - * 自分のページ - */ "my": string; - /** - * いいねしたページ - */ "liked": string; - /** - * 人気 - */ "featured": string; - /** - * インスペクター - */ "inspector": string; - /** - * コンテンツ - */ "contents": string; - /** - * ページブロック - */ "content": string; - /** - * 変数 - */ "variables": string; - /** - * タイトル - */ "title": string; - /** - * ページURL - */ "url": string; - /** - * ページの要約 - */ "summary": string; - /** - * 中央寄せ - */ "alignCenter": string; - /** - * ピン留めされているときにタイトルを非表示 - */ "hideTitleWhenPinned": string; - /** - * フォント - */ "font": string; - /** - * セリフ - */ "fontSerif": string; - /** - * サンセリフ - */ "fontSansSerif": string; - /** - * アイキャッチ画像を設定 - */ "eyeCatchingImageSet": string; - /** - * アイキャッチ画像を削除 - */ "eyeCatchingImageRemove": string; - /** - * ブロックを追加 - */ "chooseBlock": string; - /** - * セクションタイトルを入力 - */ - "enterSectionTitle": string; - /** - * 種類を選択 - */ "selectType": string; - /** - * コンテンツ - */ "contentBlocks": string; - /** - * 入力 - */ "inputBlocks": string; - /** - * 特殊 - */ "specialBlocks": string; "blocks": { - /** - * テキスト - */ "text": string; - /** - * テキストエリア - */ "textarea": string; - /** - * セクション - */ "section": string; - /** - * 画像 - */ "image": string; - /** - * ボタン - */ "button": string; - /** - * 動的ブロック - */ - "dynamic": string; - /** - * このブロックは廃止されています。今後は{play}を利用してください。 - */ - "dynamicDescription": ParameterizedString<"play">; - /** - * ノート埋め込み - */ "note": string; "_note": { - /** - * ノートID - */ "id": string; - /** - * ノートURLをペーストして設定することもできます。 - */ "idDescription": string; - /** - * 詳細な表示 - */ "detailed": string; }; }; }; "_relayStatus": { - /** - * 承認待ち - */ "requesting": string; - /** - * 承認済み - */ "accepted": string; - /** - * 拒否済み - */ "rejected": string; }; "_notification": { - /** - * ファイルがアップロードされました - */ "fileUploaded": string; - /** - * {name}からのメンション - */ - "youGotMention": ParameterizedString<"name">; - /** - * {name}からのリプライ - */ - "youGotReply": ParameterizedString<"name">; - /** - * {name}による引用 - */ - "youGotQuote": ParameterizedString<"name">; - /** - * {name}がリノートしました - */ - "youRenoted": ParameterizedString<"name">; - /** - * フォローされました - */ + "youGotMention": string; + "youGotReply": string; + "youGotQuote": string; + "youRenoted": string; "youWereFollowed": string; - /** - * フォローリクエストが来ました - */ "youReceivedFollowRequest": string; - /** - * フォローリクエストが承認されました - */ "yourFollowRequestAccepted": string; - /** - * アンケートの結果が出ました - */ "pollEnded": string; - /** - * 新しい投稿 - */ - "newNote": string; - /** - * アンテナ {name} - */ - "unreadAntennaNote": ParameterizedString<"name">; - /** - * ロールが付与されました - */ - "roleAssigned": string; - /** - * チャットルームへ招待されました - */ - "chatRoomInvitationReceived": string; - /** - * プッシュ通知の更新をしました - */ + "unreadAntennaNote": string; "emptyPushNotificationMessage": string; - /** - * 実績を獲得 - */ "achievementEarned": string; - /** - * 通知テスト - */ - "testNotification": string; - /** - * 通知の表示を確かめる - */ - "checkNotificationBehavior": string; - /** - * テスト通知を送信する - */ - "sendTestNotification": string; - /** - * 通知はこのように表示されます - */ - "notificationWillBeDisplayedLikeThis": string; - /** - * {n}人がリアクションしました - */ - "reactedBySomeUsers": ParameterizedString<"n">; - /** - * {n}人がいいねしました - */ - "likedBySomeUsers": ParameterizedString<"n">; - /** - * {n}人がリノートしました - */ - "renotedBySomeUsers": ParameterizedString<"n">; - /** - * {n}人にフォローされました - */ - "followedBySomeUsers": ParameterizedString<"n">; - /** - * 通知の履歴をリセットする - */ - "flushNotification": string; - /** - * {x}のエクスポートが完了しました - */ - "exportOfXCompleted": ParameterizedString<"x">; - /** - * ログインがありました - */ - "login": string; - /** - * アクセストークンが作成されました - */ - "createToken": string; - /** - * 心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。 - */ - "createTokenDescription": ParameterizedString<"text">; "_types": { - /** - * すべて - */ "all": string; - /** - * ユーザーの新規投稿 - */ - "note": string; - /** - * フォロー - */ "follow": string; - /** - * メンション - */ "mention": string; - /** - * リプライ - */ "reply": string; - /** - * リノート - */ "renote": string; - /** - * 引用 - */ "quote": string; - /** - * リアクション - */ "reaction": string; - /** - * アンケートが終了 - */ "pollEnded": string; - /** - * フォロー申請を受け取った - */ "receiveFollowRequest": string; - /** - * フォローが受理された - */ "followRequestAccepted": string; - /** - * ロールが付与された - */ - "roleAssigned": string; - /** - * チャットルームへ招待された - */ - "chatRoomInvitationReceived": string; - /** - * 実績の獲得 - */ "achievementEarned": string; - /** - * エクスポートが完了した - */ - "exportCompleted": string; - /** - * ログイン - */ - "login": string; - /** - * アクセストークンの作成 - */ - "createToken": string; - /** - * 通知のテスト - */ - "test": string; - /** - * 連携アプリからの通知 - */ "app": string; }; "_actions": { - /** - * フォローバック - */ "followBack": string; - /** - * 返信 - */ "reply": string; - /** - * リノート - */ "renote": string; }; }; "_deck": { - /** - * 常にメインカラムを表示 - */ "alwaysShowMainColumn": string; - /** - * カラムの寄せ - */ "columnAlign": string; - /** - * カラム間のマージン - */ - "columnGap": string; - /** - * デッキメニューの位置 - */ - "deckMenuPosition": string; - /** - * ナビゲーションバーの位置 - */ - "navbarPosition": string; - /** - * カラムを追加 - */ "addColumn": string; - /** - * 新着ノート通知の設定 - */ - "newNoteNotificationSettings": string; - /** - * カラムの設定 - */ "configureColumn": string; - /** - * 左に移動 - */ "swapLeft": string; - /** - * 右に移動 - */ "swapRight": string; - /** - * 上に移動 - */ "swapUp": string; - /** - * 下に移動 - */ "swapDown": string; - /** - * 左にスタック - */ "stackLeft": string; - /** - * 右に出す - */ "popRight": string; - /** - * プロファイル - */ "profile": string; - /** - * 新規プロファイル - */ "newProfile": string; - /** - * プロファイルを削除 - */ "deleteProfile": string; - /** - * カラムを組み合わせて自分だけのインターフェイスを作りましょう! - */ "introduction": string; - /** - * カラムを追加するには、画面の + をクリックします。 - */ "introduction2": string; - /** - * カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください - */ "widgetsIntroduction": string; - /** - * 非ルートページは簡易UIで表示 - */ - "useSimpleUiForNonRootPages": string; - /** - * 「幅を自動調整」が有効の場合、これが幅の最小値となります - */ - "usedAsMinWidthWhenFlexible": string; - /** - * 幅を自動調整 - */ - "flexible": string; - /** - * プロファイル情報のデバイス間同期を有効にする - */ - "enableSyncBetweenDevicesForProfiles": string; "_columns": { - /** - * メイン - */ "main": string; - /** - * ウィジェット - */ "widgets": string; - /** - * 通知 - */ "notifications": string; - /** - * タイムライン - */ "tl": string; - /** - * アンテナ - */ "antenna": string; - /** - * リスト - */ "list": string; - /** - * チャンネル - */ "channel": string; - /** - * あなた宛て - */ "mentions": string; - /** - * ダイレクト - */ "direct": string; - /** - * ロールタイムライン - */ "roleTimeline": string; - /** - * チャット - */ - "chat": string; }; }; "_dialog": { - /** - * 最大文字数を超えています! 現在 {current} / 制限 {max} - */ - "charactersExceeded": ParameterizedString<"current" | "max">; - /** - * 最小文字数を下回っています! 現在 {current} / 制限 {min} - */ - "charactersBelow": ParameterizedString<"current" | "min">; + "charactersExceeded": string; + "charactersBelow": string; }; "_disabledTimeline": { - /** - * 無効化されたタイムライン - */ "title": string; - /** - * 現在のロールでは、このタイムラインを使用することはできません。 - */ "description": string; }; "_drivecleaner": { - /** - * サイズが大きい順 - */ "orderBySizeDesc": string; - /** - * 追加日が古い順 - */ "orderByCreatedAtAsc": string; }; "_webhookSettings": { - /** - * Webhookを作成 - */ "createWebhook": string; - /** - * Webhookを編集 - */ - "modifyWebhook": string; - /** - * 名前 - */ "name": string; - /** - * シークレット - */ "secret": string; - /** - * トリガー - */ - "trigger": string; - /** - * 有効 - */ + "events": string; "active": string; "_events": { - /** - * フォローしたとき - */ "follow": string; - /** - * フォローされたとき - */ "followed": string; - /** - * ノートを投稿したとき - */ "note": string; - /** - * 返信されたとき - */ "reply": string; - /** - * Renoteされたとき - */ "renote": string; - /** - * リアクションがあったとき - */ "reaction": string; - /** - * メンションされたとき - */ "mention": string; }; - "_systemEvents": { - /** - * ユーザーから通報があったとき - */ - "abuseReport": string; - /** - * ユーザーからの通報を処理したとき - */ - "abuseReportResolved": string; - /** - * ユーザーが作成されたとき - */ - "userCreated": string; - /** - * モデレーターが一定期間非アクティブになったとき - */ - "inactiveModeratorsWarning": string; - /** - * モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき - */ - "inactiveModeratorsInvitationOnlyChanged": string; - }; - /** - * Webhookを削除しますか? - */ - "deleteConfirm": string; - /** - * スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。 - */ - "testRemarks": string; - }; - "_abuseReport": { - "_notificationRecipient": { - /** - * 通報の通知先を追加 - */ - "createRecipient": string; - /** - * 通報の通知先を編集 - */ - "modifyRecipient": string; - /** - * 通知先の種類 - */ - "recipientType": string; - "_recipientType": { - /** - * メール - */ - "mail": string; - /** - * Webhook - */ - "webhook": string; - "_captions": { - /** - * モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ) - */ - "mail": string; - /** - * 指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信) - */ - "webhook": string; - }; - }; - /** - * キーワード - */ - "keywords": string; - /** - * 通知先ユーザー - */ - "notifiedUser": string; - /** - * 使用するWebhook - */ - "notifiedWebhook": string; - /** - * 通知先を削除しますか? - */ - "deleteConfirm": string; - }; - }; - "_moderationLogTypes": { - /** - * ロールを作成 - */ - "createRole": string; - /** - * ロールを削除 - */ - "deleteRole": string; - /** - * ロールを更新 - */ - "updateRole": string; - /** - * ロールへアサイン - */ - "assignRole": string; - /** - * ロールのアサイン解除 - */ - "unassignRole": string; - /** - * 凍結 - */ - "suspend": string; - /** - * 凍結解除 - */ - "unsuspend": string; - /** - * カスタム絵文字追加 - */ - "addCustomEmoji": string; - /** - * カスタム絵文字更新 - */ - "updateCustomEmoji": string; - /** - * カスタム絵文字削除 - */ - "deleteCustomEmoji": string; - /** - * サーバー設定更新 - */ - "updateServerSettings": string; - /** - * ユーザーのモデレーションノート更新 - */ - "updateUserNote": string; - /** - * ファイルを削除 - */ - "deleteDriveFile": string; - /** - * ノートを削除 - */ - "deleteNote": string; - /** - * 全体のお知らせを作成 - */ - "createGlobalAnnouncement": string; - /** - * ユーザーへお知らせを作成 - */ - "createUserAnnouncement": string; - /** - * 全体のお知らせを更新 - */ - "updateGlobalAnnouncement": string; - /** - * ユーザーのお知らせを更新 - */ - "updateUserAnnouncement": string; - /** - * 全体のお知らせを削除 - */ - "deleteGlobalAnnouncement": string; - /** - * ユーザーのお知らせを削除 - */ - "deleteUserAnnouncement": string; - /** - * パスワードをリセット - */ - "resetPassword": string; - /** - * リモートサーバーを停止 - */ - "suspendRemoteInstance": string; - /** - * リモートサーバーを再開 - */ - "unsuspendRemoteInstance": string; - /** - * リモートサーバーのモデレーションノート更新 - */ - "updateRemoteInstanceNote": string; - /** - * ファイルをセンシティブ付与 - */ - "markSensitiveDriveFile": string; - /** - * ファイルをセンシティブ解除 - */ - "unmarkSensitiveDriveFile": string; - /** - * 通報を解決 - */ - "resolveAbuseReport": string; - /** - * 通報を転送 - */ - "forwardAbuseReport": string; - /** - * 通報のモデレーションノート更新 - */ - "updateAbuseReportNote": string; - /** - * 招待コードを作成 - */ - "createInvitation": string; - /** - * 広告を作成 - */ - "createAd": string; - /** - * 広告を削除 - */ - "deleteAd": string; - /** - * 広告を更新 - */ - "updateAd": string; - /** - * アイコンデコレーションを作成 - */ - "createAvatarDecoration": string; - /** - * アイコンデコレーションを更新 - */ - "updateAvatarDecoration": string; - /** - * アイコンデコレーションを削除 - */ - "deleteAvatarDecoration": string; - /** - * ユーザーのアイコンを解除 - */ - "unsetUserAvatar": string; - /** - * ユーザーのバナーを解除 - */ - "unsetUserBanner": string; - /** - * SystemWebhookを作成 - */ - "createSystemWebhook": string; - /** - * SystemWebhookを更新 - */ - "updateSystemWebhook": string; - /** - * SystemWebhookを削除 - */ - "deleteSystemWebhook": string; - /** - * 通報の通知先を作成 - */ - "createAbuseReportNotificationRecipient": string; - /** - * 通報の通知先を更新 - */ - "updateAbuseReportNotificationRecipient": string; - /** - * 通報の通知先を削除 - */ - "deleteAbuseReportNotificationRecipient": string; - /** - * アカウントを削除 - */ - "deleteAccount": string; - /** - * ページを削除 - */ - "deletePage": string; - /** - * Playを削除 - */ - "deleteFlash": string; - /** - * ギャラリーの投稿を削除 - */ - "deleteGalleryPost": string; - /** - * チャットルームを削除 - */ - "deleteChatRoom": string; - /** - * プロキシアカウントの説明を更新 - */ - "updateProxyAccountDescription": string; - }; - "_fileViewer": { - /** - * ファイルの詳細 - */ - "title": string; - /** - * ファイルタイプ - */ - "type": string; - /** - * ファイルサイズ - */ - "size": string; - /** - * URL - */ - "url": string; - /** - * 追加日 - */ - "uploadedAt": string; - /** - * 添付されているノート - */ - "attachedNotes": string; - /** - * このページは、このファイルをアップロードしたユーザーしか閲覧できません。 - */ - "thisPageCanBeSeenFromTheAuthor": string; - }; - "_externalResourceInstaller": { - /** - * 外部サイトからインストール - */ - "title": string; - /** - * 配布元が信頼できるかを確認した上でインストールしてください。 - */ - "checkVendorBeforeInstall": string; - "_plugin": { - /** - * このプラグインをインストールしますか? - */ - "title": string; - }; - "_theme": { - /** - * このテーマをインストールしますか? - */ - "title": string; - }; - "_meta": { - /** - * 基本のカラースキーム - */ - "base": string; - }; - "_vendorInfo": { - /** - * 配布元情報 - */ - "title": string; - /** - * 参照したエンドポイント - */ - "endpoint": string; - /** - * ファイル整合性の確認 - */ - "hashVerify": string; - }; - "_errors": { - "_invalidParams": { - /** - * パラメータが不足しています - */ - "title": string; - /** - * 外部サイトからデータを取得するために必要な情報が不足しています。URLをお確かめください。 - */ - "description": string; - }; - "_resourceTypeNotSupported": { - /** - * この外部リソースには対応していません - */ - "title": string; - /** - * この外部サイトから取得したリソースの種別には対応していません。サイト管理者にお問い合わせください。 - */ - "description": string; - }; - "_failedToFetch": { - /** - * データの取得に失敗しました - */ - "title": string; - /** - * 外部サイトとの通信に失敗しました。もう一度試しても改善しない場合、サイト管理者にお問い合わせください。 - */ - "fetchErrorDescription": string; - /** - * 外部サイトから取得したデータが読み取れませんでした。サイト管理者にお問い合わせください。 - */ - "parseErrorDescription": string; - }; - "_hashUnmatched": { - /** - * 正しいデータが取得できませんでした - */ - "title": string; - /** - * 提供されたデータの整合性の確認に失敗しました。セキュリティ上、インストールは続行できません。サイト管理者にお問い合わせください。 - */ - "description": string; - }; - "_pluginParseFailed": { - /** - * AiScript エラー - */ - "title": string; - /** - * データは取得できたものの、AiScriptの解析時にエラーがあったため読み込めませんでした。プラグインの作者にお問い合わせください。エラーの詳細はJavascriptコンソールをご確認ください。 - */ - "description": string; - }; - "_pluginInstallFailed": { - /** - * プラグインのインストールに失敗しました - */ - "title": string; - /** - * プラグインのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。 - */ - "description": string; - }; - "_themeParseFailed": { - /** - * テーマ解析エラー - */ - "title": string; - /** - * データは取得できたものの、テーマファイルの解析時にエラーがあったため読み込めませんでした。テーマの作者にお問い合わせください。エラーの詳細はJavascriptコンソールをご確認ください。 - */ - "description": string; - }; - "_themeInstallFailed": { - /** - * テーマのインストールに失敗しました - */ - "title": string; - /** - * テーマのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。 - */ - "description": string; - }; - }; - }; - "_dataSaver": { - "_media": { - /** - * メディアの読み込みを無効化 - */ - "title": string; - /** - * 画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。 - */ - "description": string; - }; - "_avatar": { - /** - * アイコン画像のアニメーションを無効化 - */ - "title": string; - /** - * アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。 - */ - "description": string; - }; - "_urlPreviewThumbnail": { - /** - * URLプレビューのサムネイルを非表示 - */ - "title": string; - /** - * URLプレビューのサムネイル画像が読み込まれなくなります。 - */ - "description": string; - }; - "_disableUrlPreview": { - /** - * URLプレビューを無効化 - */ - "title": string; - /** - * URLプレビュー機能を無効化します。サムネイル画像だけと違い、リンク先の情報の読み込み自体を削減できます。 - */ - "description": string; - }; - "_code": { - /** - * コードハイライトを非表示 - */ - "title": string; - /** - * MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。 - */ - "description": string; - }; - }; - "_hemisphere": { - /** - * 北半球 - */ - "N": string; - /** - * 南半球 - */ - "S": string; - /** - * 一部のクライアント設定で、季節を判定するために使用します。 - */ - "caption": string; - }; - "_reversi": { - /** - * リバーシ - */ - "reversi": string; - /** - * 対局の設定 - */ - "gameSettings": string; - /** - * ボードを選択 - */ - "chooseBoard": string; - /** - * 先行/後攻 - */ - "blackOrWhite": string; - /** - * {name}が黒(先行) - */ - "blackIs": ParameterizedString<"name">; - /** - * ルール - */ - "rules": string; - /** - * 対局はまもなく開始されます - */ - "thisGameIsStartedSoon": string; - /** - * 相手の準備が完了するのを待っています - */ - "waitingForOther": string; - /** - * あなたの準備が完了するのを待っています - */ - "waitingForMe": string; - /** - * 準備してください - */ - "waitingBoth": string; - /** - * 準備完了 - */ - "ready": string; - /** - * 準備を再開 - */ - "cancelReady": string; - /** - * 相手のターンです - */ - "opponentTurn": string; - /** - * あなたのターンです - */ - "myTurn": string; - /** - * {name}のターンです - */ - "turnOf": ParameterizedString<"name">; - /** - * {name}のターン - */ - "pastTurnOf": ParameterizedString<"name">; - /** - * 投了 - */ - "surrender": string; - /** - * 投了により - */ - "surrendered": string; - /** - * 時間切れ - */ - "timeout": string; - /** - * 引き分け - */ - "drawn": string; - /** - * {name}の勝ち - */ - "won": ParameterizedString<"name">; - /** - * 黒 - */ - "black": string; - /** - * 白 - */ - "white": string; - /** - * 合計 - */ - "total": string; - /** - * {count}ターン目 - */ - "turnCount": ParameterizedString<"count">; - /** - * 自分の対局 - */ - "myGames": string; - /** - * みんなの対局 - */ - "allGames": string; - /** - * 終了 - */ - "ended": string; - /** - * 対局中 - */ - "playing": string; - /** - * 石の少ない方が勝ち(ロセオ) - */ - "isLlotheo": string; - /** - * ループマップ - */ - "loopedMap": string; - /** - * どこでも置けるモード - */ - "canPutEverywhere": string; - /** - * 1ターンの時間制限 - */ - "timeLimitForEachTurn": string; - /** - * フリーマッチ - */ - "freeMatch": string; - /** - * 対戦相手を探しています - */ - "lookingForPlayer": string; - /** - * 対局がキャンセルされました - */ - "gameCanceled": string; - /** - * 開始時に対局をタイムラインに投稿 - */ - "shareToTlTheGameWhenStart": string; - /** - * 対局を開始しました! #MisskeyReversi - */ - "iStartedAGame": string; - /** - * 相手が設定を変更しました - */ - "opponentHasSettingsChanged": string; - /** - * 変則許可 (完全フリー) - */ - "allowIrregularRules": string; - /** - * 変則なし - */ - "disallowIrregularRules": string; - /** - * 盤面に行・列番号を表示 - */ - "showBoardLabels": string; - /** - * 石をアイコンにする - */ - "useAvatarAsStone": string; - }; - "_offlineScreen": { - /** - * オフライン - サーバーに接続できません - */ - "title": string; - /** - * サーバーに接続できません - */ - "header": string; - }; - "_urlPreviewSetting": { - /** - * URLプレビューの設定 - */ - "title": string; - /** - * URLプレビューを有効にする - */ - "enable": string; - /** - * プレビュー先のリダイレクトを許可 - */ - "allowRedirect": string; - /** - * 入力されたURLがリダイレクトされる場合に、そのリダイレクト先をたどってプレビューを表示するかどうかを設定します。無効にするとサーバーリソースの節約になりますが、リダイレクト先の内容は表示されなくなります。 - */ - "allowRedirectDescription": string; - /** - * プレビュー取得時のタイムアウト(ms) - */ - "timeout": string; - /** - * プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。 - */ - "timeoutDescription": string; - /** - * Content-Lengthの最大値(byte) - */ - "maximumContentLength": string; - /** - * Content-Lengthがこの値を超えた場合、プレビューは生成されません。 - */ - "maximumContentLengthDescription": string; - /** - * Content-Lengthが取得できた場合のみプレビューを生成 - */ - "requireContentLength": string; - /** - * 相手サーバがContent-Lengthを返さない場合、プレビューは生成されません。 - */ - "requireContentLengthDescription": string; - /** - * User-Agent - */ - "userAgent": string; - /** - * プレビュー取得時に使用されるUser-Agentを設定します。空欄の場合、デフォルトのUser-Agentが使用されます。 - */ - "userAgentDescription": string; - /** - * プレビューを生成するプロキシのエンドポイント - */ - "summaryProxy": string; - /** - * Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。 - */ - "summaryProxyDescription": string; - /** - * プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。 - */ - "summaryProxyDescription2": string; - }; - "_mediaControls": { - /** - * ピクチャインピクチャ - */ - "pip": string; - /** - * 再生速度 - */ - "playbackRate": string; - /** - * ループ再生 - */ - "loop": string; - }; - "_contextMenu": { - /** - * コンテキストメニュー - */ - "title": string; - /** - * アプリケーション - */ - "app": string; - /** - * Shiftキーでアプリケーション - */ - "appWithShift": string; - /** - * ブラウザのUI - */ - "native": string; - }; - "_gridComponent": { - "_error": { - /** - * この値は必須項目です - */ - "requiredValue": string; - /** - * 正規表現によるバリデーションはtype:textのカラムのみサポートします。 - */ - "columnTypeNotSupport": string; - /** - * この値は{pattern}のパターンに一致しません - */ - "patternNotMatch": ParameterizedString<"pattern">; - /** - * この値は一意である必要があります - */ - "notUnique": string; - }; - }; - "_roleSelectDialog": { - /** - * 選択されていません - */ - "notSelected": string; - }; - "_customEmojisManager": { - "_gridCommon": { - /** - * 選択行をコピー - */ - "copySelectionRows": string; - /** - * 選択範囲をコピー - */ - "copySelectionRanges": string; - /** - * 選択行を削除 - */ - "deleteSelectionRows": string; - /** - * 選択範囲の値をクリア - */ - "deleteSelectionRanges": string; - /** - * 検索設定 - */ - "searchSettings": string; - /** - * 検索条件を詳細に設定します。 - */ - "searchSettingCaption": string; - /** - * 表示件数 - */ - "searchLimit": string; - /** - * 並び順 - */ - "sortOrder": string; - /** - * 登録ログ - */ - "registrationLogs": string; - /** - * 絵文字更新・削除時のログが表示されます。更新・削除操作を行ったり、ページを遷移・リロードすると消えます。 - */ - "registrationLogsCaption": string; - /** - * 絵文字の更新・削除に失敗しました。詳細は登録ログをご確認ください。 - */ - "alertEmojisRegisterFailedDescription": string; - }; - "_logs": { - /** - * 成功ログを表示 - */ - "showSuccessLogSwitch": string; - /** - * 失敗ログはありません。 - */ - "failureLogNothing": string; - /** - * ログはありません。 - */ - "logNothing": string; - }; - "_remote": { - /** - * 選択行の詳細 - */ - "selectionRowDetail": string; - /** - * 選択行をインポート - */ - "importSelectionRows": string; - /** - * 選択範囲の行をインポート - */ - "importSelectionRangesRows": string; - /** - * チェックされた絵文字をインポート - */ - "importEmojisButton": string; - /** - * 絵文字のインポート - */ - "confirmImportEmojisTitle": string; - /** - * リモートから受信した{count}個の絵文字のインポートを行います。絵文字のライセンスに十分な注意を払ってください。実行しますか? - */ - "confirmImportEmojisDescription": ParameterizedString<"count">; - }; - "_local": { - /** - * 登録済み絵文字一覧 - */ - "tabTitleList": string; - /** - * 絵文字の登録 - */ - "tabTitleRegister": string; - "_list": { - /** - * 登録された絵文字はありません。 - */ - "emojisNothing": string; - /** - * 選択行を削除対象にする - */ - "markAsDeleteTargetRows": string; - /** - * 選択範囲の行を削除対象にする - */ - "markAsDeleteTargetRanges": string; - /** - * 変更された絵文字はありません。 - */ - "alertUpdateEmojisNothingDescription": string; - /** - * 削除対象の絵文字はありません。 - */ - "alertDeleteEmojisNothingDescription": string; - /** - * ページを移動しますか? - */ - "confirmMovePage": string; - /** - * 表示を変更しますか? - */ - "confirmChangeView": string; - /** - * {count}個の絵文字を更新します。実行しますか? - */ - "confirmUpdateEmojisDescription": ParameterizedString<"count">; - /** - * チェックがつけられた{count}個の絵文字を削除します。実行しますか? - */ - "confirmDeleteEmojisDescription": ParameterizedString<"count">; - /** - * 今までに加えた変更がすべてリセットされます。 - */ - "confirmResetDescription": string; - /** - * このページの絵文字に変更が加えられています。 - * 保存せずにこのままページを移動すると、このページで加えた変更はすべて破棄されます。 - */ - "confirmMovePageDesciption": string; - /** - * 絵文字に設定されたロールで検索 - */ - "dialogSelectRoleTitle": string; - }; - "_register": { - /** - * アップロード設定 - */ - "uploadSettingTitle": string; - /** - * この画面で絵文字アップロードを行う際の動作を設定できます。 - */ - "uploadSettingDescription": string; - /** - * ディレクトリ名を"category"に入力する - */ - "directoryToCategoryLabel": string; - /** - * ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を"category"に入力します。 - */ - "directoryToCategoryCaption": string; - /** - * リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです) - */ - "confirmRegisterEmojisDescription": ParameterizedString<"count">; - /** - * 編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか? - */ - "confirmClearEmojisDescription": string; - /** - * ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードします。実行しますか? - */ - "confirmUploadEmojisDescription": ParameterizedString<"count">; - }; - }; - }; - "_embedCodeGen": { - /** - * 埋め込みコードをカスタマイズ - */ - "title": string; - /** - * ヘッダーを表示 - */ - "header": string; - /** - * 自動で続きを読み込む(非推奨) - */ - "autoload": string; - /** - * 高さの最大値 - */ - "maxHeight": string; - /** - * 0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。 - */ - "maxHeightDescription": string; - /** - * 高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。 - */ - "maxHeightWarn": string; - /** - * プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。 - */ - "previewIsNotActual": string; - /** - * 角丸にする - */ - "rounded": string; - /** - * 外枠に枠線をつける - */ - "border": string; - /** - * プレビューに反映 - */ - "applyToPreview": string; - /** - * 埋め込みコードを作成 - */ - "generateCode": string; - /** - * コードが生成されました - */ - "codeGenerated": string; - /** - * 生成されたコードをウェブサイトに貼り付けてご利用ください。 - */ - "codeGeneratedDescription": string; - }; - "_selfXssPrevention": { - /** - * 警告 - */ - "warning": string; - /** - * 「この画面に何か貼り付けろ」はすべて詐欺です。 - */ - "title": string; - /** - * ここに何かを貼り付けると、悪意のあるユーザーにアカウントを乗っ取られたり、個人情報を盗まれたりする可能性があります。 - */ - "description1": string; - /** - * 貼り付けようとしているものが何なのかを正確に理解していない場合は、%c今すぐ作業を中止してこのウィンドウを閉じてください。 - */ - "description2": string; - /** - * 詳しくはこちらをご確認ください。 {link} - */ - "description3": ParameterizedString<"link">; - }; - "_followRequest": { - /** - * 受け取った申請 - */ - "recieved": string; - /** - * 送った申請 - */ - "sent": string; - }; - "_remoteLookupErrors": { - "_federationNotAllowed": { - /** - * このサーバーとは通信できません - */ - "title": string; - /** - * このサーバーとの通信が無効化されているか、このサーバーをブロックしている・ブロックされている可能性があります。 - * サーバー管理者にお問い合わせください。 - */ - "description": string; - }; - "_uriInvalid": { - /** - * URIが不正です - */ - "title": string; - /** - * 入力されたURIに問題があります。URIに使用できない文字を入力していないか確認してください。 - */ - "description": string; - }; - "_requestFailed": { - /** - * リクエストに失敗しました - */ - "title": string; - /** - * このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。 - */ - "description": string; - }; - "_responseInvalid": { - /** - * レスポンスが不正です - */ - "title": string; - /** - * このサーバーと通信することはできましたが、得られたデータが不正なものでした。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。 - */ - "description": string; - }; - "_noSuchObject": { - /** - * 見つかりません - */ - "title": string; - /** - * 要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。 - */ - "description": string; - }; - }; - "_captcha": { - /** - * CAPTCHAを通過してください - */ - "verify": string; - /** - * サイトキーとシークレットキーにテスト用の値を入力することでプレビューを確認できます。 - * 詳細は下記ページをご確認ください。 - */ - "testSiteKeyMessage": string; - "_error": { - "_requestFailed": { - /** - * CAPTCHAのリクエストに失敗しました - */ - "title": string; - /** - * しばらく後に実行するか、設定をもう一度ご確認ください。 - */ - "text": string; - }; - "_verificationFailed": { - /** - * CAPTCHAの検証に失敗しました - */ - "title": string; - /** - * 設定が正しいかどうかもう一度確認ください。 - */ - "text": string; - }; - "_unknown": { - /** - * CAPTCHAエラー - */ - "title": string; - /** - * 想定外のエラーが発生しました。 - */ - "text": string; - }; - }; - }; - "_bootErrors": { - /** - * 読み込みに失敗しました - */ - "title": string; - /** - * 少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。 - */ - "serverError": string; - /** - * 以下を行うと解決する可能性があります。 - */ - "solution": string; - /** - * ブラウザおよびOSを最新バージョンに更新する - */ - "solution1": string; - /** - * アドブロッカーを無効にする - */ - "solution2": string; - /** - * ブラウザのキャッシュをクリアする - */ - "solution3": string; - /** - * (Tor Browser) dom.webaudio.enabledをtrueに設定する - */ - "solution4": string; - /** - * その他のオプション - */ - "otherOption": string; - /** - * クライアント設定とキャッシュを削除 - */ - "otherOption1": string; - /** - * 簡易クライアントを起動 - */ - "otherOption2": string; - /** - * 修復ツールを起動 - */ - "otherOption3": string; - }; - "_search": { - /** - * 全て - */ - "searchScopeAll": string; - /** - * ローカル - */ - "searchScopeLocal": string; - /** - * サーバー指定 - */ - "searchScopeServer": string; - /** - * ユーザー指定 - */ - "searchScopeUser": string; - /** - * サーバーのホストを入力してください - */ - "pleaseEnterServerHost": string; - /** - * ユーザーを選択してください - */ - "pleaseSelectUser": string; - /** - * 例: misskey.example.com - */ - "serverHostPlaceholder": string; - }; - "_serverSetupWizard": { - /** - * Misskeyのインストールが完了しました! - */ - "installCompleted": string; - /** - * まずは、管理者アカウントを作成しましょう。 - */ - "firstCreateAccount": string; - /** - * 管理者アカウントが作成されました! - */ - "accountCreated": string; - /** - * サーバーの設定 - */ - "serverSetting": string; - /** - * このウィザードで簡単に最適なサーバーの設定が行えます。 - */ - "youCanEasilyConfigureOptimalServerSettingsWithThisWizard": string; - /** - * ここでの設定は、あとからでも変更できます。 - */ - "settingsYouMakeHereCanBeChangedLater": string; - /** - * Misskeyをどのように使いますか? - */ - "howWillYouUseMisskey": string; - "_use": { - /** - * お一人様サーバー - */ - "single": string; - /** - * 自分専用のサーバーとして、一人で使う - */ - "single_description": string; - /** - * お一人様サーバーとして運用する場合でも、アカウントは必要に応じて複数作成可能です。 - */ - "single_youCanCreateMultipleAccounts": string; - /** - * グループサーバー - */ - "group": string; - /** - * 信頼できる他の利用者を招待して、複数人で使う - */ - "group_description": string; - /** - * オープンサーバー - */ - "open": string; - /** - * 不特定多数の利用者を受け入れる運営を行う - */ - "open_description": string; - }; - /** - * 不特定多数の利用者を受け入れることはリスクが伴います。トラブルに対処できるよう、確実なモデレーション体制で運営することを推奨します。 - */ - "openServerAdvice": string; - /** - * 自サーバーがスパムの踏み台にならないように、reCAPTCHAといったアンチボット機能を有効にするなど、セキュリティについても細心の注意が必要です。 - */ - "openServerAntiSpamAdvice": string; - /** - * どれくらいの人数を想定していますか? - */ - "howManyUsersDoYouExpect": string; - "_scale": { - /** - * 100人以下 (小規模) - */ - "small": string; - /** - * 100人以上1000人以下 (中規模) - */ - "medium": string; - /** - * 1000人以上 (大規模) - */ - "large": string; - }; - /** - * 大規模なサーバーでは、ロードバランシングやデータベースのレプリケーションなど、高度なインフラストラクチャーの知識が必要になる場合があります。 - */ - "largeScaleServerAdvice": string; - /** - * Fediverseと接続しますか? - */ - "doYouConnectToFediverse": string; - /** - * 分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。 - */ - "doYouConnectToFediverse_description1": string; - /** - * Fediverseと接続することは「連合」とも呼ばれます。 - */ - "doYouConnectToFediverse_description2": string; - /** - * 連合可能なサーバーの指定など、高度な設定も後ほど可能です。 - */ - "youCanConfigureMoreFederationSettingsLater": string; - /** - * 管理者情報 - */ - "adminInfo": string; - /** - * 問い合わせを受け付けるために使用される管理者情報を設定します。 - */ - "adminInfo_description": string; - /** - * オープンサーバー、または連合がオンの場合は必ず入力が必要です。 - */ - "adminInfo_mustBeFilled": string; - /** - * 以下の設定が推奨されます - */ - "followingSettingsAreRecommended": string; - /** - * この設定を適用 - */ - "applyTheseSettings": string; - /** - * 設定をスキップ - */ - "skipSettings": string; - /** - * 設定が完了しました! - */ - "settingsCompleted": string; - /** - * お疲れ様でした。準備が整ったので、さっそくサーバーの使用を開始できます。 - */ - "settingsCompleted_description": string; - /** - * 詳細なサーバー設定は、「コントロールパネル」から行えます。 - */ - "settingsCompleted_description2": string; - /** - * 寄付のお願い - */ - "donationRequest": string; - "_donationRequest": { - /** - * Misskeyは有志によって開発されている無料のソフトウェアです。 - */ - "text1": string; - /** - * 今後も開発を続けられるように、よろしければぜひカンパをお願いいたします。 - */ - "text2": string; - /** - * 支援者向け特典もあります! - */ - "text3": string; - }; - }; - "_uploader": { - /** - * {x}に圧縮 - */ - "compressedToX": ParameterizedString<"x">; - /** - * {x}%節約 - */ - "savedXPercent": ParameterizedString<"x">; - /** - * アップロードされていないファイルがありますが、中止しますか? - */ - "abortConfirm": string; - /** - * アップロードされていないファイルがありますが、完了しますか? - */ - "doneConfirm": string; - /** - * アップロード可能な最大ファイルサイズは{x}です。 - */ - "maxFileSizeIsX": ParameterizedString<"x">; - /** - * アップロード可能なファイル種別 - */ - "allowedTypes": string; - /** - * ファイルはまだアップロードされていません。このダイアログで、アップロード前の確認・リネーム・圧縮・クロッピングなどが行えます。準備が出来たら、「アップロード」ボタンを押してアップロードを開始できます。 - */ - "tip": string; - }; - "_clientPerformanceIssueTip": { - /** - * バッテリー消費が多いと感じたら - */ - "title": string; - /** - * アドブロッカーを無効にしてください - */ - "makeSureDisabledAdBlocker": string; - /** - * アドブロッカーはパフォーマンスに影響を及ぼすことがあります。OSの機能やブラウザの機能・アドオンなどでアドブロッカーが有効になっていないか確認してください。 - */ - "makeSureDisabledAdBlocker_description": string; - /** - * カスタムCSSを無効にしてください - */ - "makeSureDisabledCustomCss": string; - /** - * スタイルを上書きするとパフォーマンスに影響を及ぼすことがあります。カスタムCSSや、スタイルを上書きする拡張機能が有効になっていないか確認してください。 - */ - "makeSureDisabledCustomCss_description": string; - /** - * 拡張機能を無効にしてください - */ - "makeSureDisabledAddons": string; - /** - * 一部の拡張機能はクライアントの動作に干渉しパフォーマンスに影響を及ぼすことがあります。ブラウザの拡張機能を無効にして改善するか確認してください。 - */ - "makeSureDisabledAddons_description": string; - }; - "_clip": { - /** - * クリップは、ノートをまとめることができる機能です。 - */ - "tip": string; - }; - "_userLists": { - /** - * 任意のユーザーが含まれるリストを作成できます。作成したリストはタイムラインとして表示可能です。 - */ - "tip": string; }; } declare const locales: { [lang: string]: Locale; }; -export function build(): Locale; export default locales; diff --git a/locales/index.js b/locales/index.js index 091d216dee..7801f1275b 100644 --- a/locales/index.js +++ b/locales/index.js @@ -15,7 +15,6 @@ const merge = (...args) => args.reduce((a, c) => ({ const languages = [ 'ar-SA', - 'ca-ES', 'cs-CZ', 'da-DK', 'de-DE', @@ -52,41 +51,20 @@ const primaries = { // 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); -export function build() { - // vitestの挙動を調整するため、一度ローカル変数化する必要がある - // https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577 - // https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785 - const metaUrl = import.meta.url; - const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {}); +const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); - // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す - const removeEmpty = (obj) => { - for (const [k, v] of Object.entries(obj)) { - if (v === '') { - delete obj[k]; - } else if (typeof v === 'object') { - removeEmpty(v); - } +export default Object.entries(locales) + .reduce((a, [k ,v]) => (a[k] = (() => { + const [lang] = k.split('-'); + switch (k) { + case 'ja-JP': return v; + case 'ja-KS': + case 'en-US': return merge(locales['ja-JP'], v); + default: return merge( + locales['ja-JP'], + locales['en-US'], + locales[`${lang}-${primaries[lang]}`] || {}, + v + ); } - return obj; - }; - removeEmpty(locales); - - return Object.entries(locales) - .reduce((a, [k, v]) => (a[k] = (() => { - const [lang] = k.split('-'); - switch (k) { - case 'ja-JP': return v; - case 'ja-KS': - case 'en-US': return merge(locales['ja-JP'], v); - default: return merge( - locales['ja-JP'], - locales['en-US'], - locales[`${lang}-${primaries[lang]}`] ?? {}, - v - ); - } - })(), a), {}); -} - -export default build(); + })(), a), {}); diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 8bcbe45655..51c8e28d63 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1,17 +1,13 @@ --- _lang_: "Italiano" -headlineMisskey: "Rete collegata tramite Note" +headlineMisskey: "Rete collegata tramite note" introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n\n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n\n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n\n🚀 Esplora un nuovo mondo insieme a noi!" poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source Misskey." monthAndDay: "{day}/{month}" search: "Cerca" -reset: "Ripristinare" notifications: "Notifiche" username: "Nome utente" password: "Password" -initialPasswordForSetup: "Password iniziale, per avviare le impostazioni" -initialPasswordIsIncorrect: "Password iniziale, sbagliata." -initialPasswordForSetupDescription: "Se hai installato Misskey di persona, usa la password che hai indicato nel file di configurazione.\nSe stai utilizzando un servizio di hosting Misskey, usa la password fornita dal gestore.\nSe non hai una password preimpostata, lascia il campo vuoto e continua." forgotPassword: "Hai dimenticato la password?" fetchingAsApObject: "Recuperando dal Fediverso..." ok: "OK" @@ -19,13 +15,13 @@ gotIt: "ok!" cancel: "Annulla" noThankYou: "No grazie" enterUsername: "Inserisci un nome utente" -renotedBy: "Rinotata da {user}" +renotedBy: "Rinotato da {user}" noNotes: "Nessuna nota!" noNotifications: "Nessuna notifica" instance: "Istanza" settings: "Impostazioni" notificationSettings: "Preferenze di notifica" -basicSettings: "Impostazioni base" +basicSettings: "Impostazioni generali" otherSettings: "Altre impostazioni" openInWindow: "Apri in una finestra" profile: "Profilo" @@ -49,30 +45,23 @@ pin: "Fissa sul profilo" unpin: "Non fissare sul profilo" copyContent: "Copia il contenuto" copyLink: "Copia il link" -copyRemoteLink: "Copia link remoto" -copyLinkRenote: "Copia collegamento alla Rinota" delete: "Elimina" deleteAndEdit: "Elimina e modifica" deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verranno eliminate anche tutte le reazioni, rinote e risposte collegate." addToList: "Aggiungi alla lista" -addToAntenna: "Aggiungi all'antenna" sendMessage: "Invia messaggio" copyRSS: "Copia RSS" -copyUsername: "Copia indirizzo del profilo" +copyUsername: "Copia nome utente" copyUserId: "Copia ID del profilo" copyNoteId: "Copia ID della Nota" -copyFileId: "Copia ID del file" -copyFolderId: "Copia ID della cartella" -copyProfileUrl: "Copia URL del profilo" searchUser: "Cerca profilo" -searchThisUsersNotes: "Cerca le sue Note" reply: "Rispondi" loadMore: "Mostra di più" showMore: "Espandi" showLess: "Comprimi" -youGotNewFollower: "Hai un nuovo Follower" +youGotNewFollower: "Ha iniziato a seguirti" receiveFollowRequest: "Hai ricevuto una richiesta di follow" -followRequestAccepted: "Ha accettato la tua richiesta di follow" +followRequestAccepted: "Richiesta di follow accettata" mention: "Menzioni" mentions: "Menzioni" directNotes: "Note dirette" @@ -81,17 +70,17 @@ import: "Importa" export: "Esporta" files: "Allegati" download: "Scarica" -driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?" -unfollowConfirm: "Vuoi davvero togliere il Following a {name}?" +driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\"? Anche gli allegati verranno eliminati." +unfollowConfirm: "Vuoi smettere di seguire {name}?" exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive." -importRequested: "Hai richiesto un'importazione. Potrebbe richiedere un po' di tempo." +importRequested: "Hai richiesto un'importazione. Può volerci tempo. " lists: "Liste" noLists: "Nessuna lista" note: "Nota" notes: "Note" -following: "Following" +following: "Follow" followers: "Follower" -followsYou: "Follower" +followsYou: "Ti segue" createList: "Aggiungi una nuova lista" manageLists: "Gestisci liste" error: "Errore" @@ -107,64 +96,52 @@ makeFollowManuallyApprove: "Approva i follower manualmente" defaultNoteVisibility: "Privacy predefinita delle note" follow: "Segui" followRequest: "Richiesta di follow" -followRequests: "Relazioni" -unfollow: "Togli Following" +followRequests: "Richieste di follow" +unfollow: "Non seguire" followRequestPending: "Richiesta in approvazione" enterEmoji: "Inserisci emoji" renote: "Rinota" -unrenote: "Elimina la Rinota" -renoted: "Rinotata!" -renotedToX: "Rinota da {name}." +unrenote: "Annulla rinota" +renoted: "Rinotato!" cantRenote: "È impossibile rinotare questa nota." cantReRenote: "È impossibile rinotare una Rinota." -quote: "Citazione" +quote: "Cita" inChannelRenote: "Rinota nel canale" inChannelQuote: "Cita nel canale" -renoteToChannel: "Rinota al canale" -renoteToOtherChannel: "Rinota a un altro canale" -pinnedNote: "Nota in primo piano" +pinnedNote: "Nota fissata" pinned: "Fissa sul profilo" you: "Tu" -clickToShow: "Contenuto occultato, cliccare solo se si intende vedere" -sensitive: "Esplicito" +clickToShow: "Clicca per visualizzare" +sensitive: "Contenuto sensibile" add: "Aggiungi" reaction: "Reazioni" reactions: "Reazioni" -emojiPicker: "Selettore emoji" -pinnedEmojisForReactionSettingDescription: "Scegli quale sia l'emoji in cima, quando reagisci" -pinnedEmojisSettingDescription: "Scegli quale sia l'emoji in cima, quando reagisci" -emojiPickerDisplay: "Visualizza selettore" -overwriteFromPinnedEmojisForReaction: "Sovrascrivi con le impostazioni reazioni" -overwriteFromPinnedEmojis: "Sovrascrivi con le impostazioni globali" +reactionSetting: "Reazioni visualizzate sul pannello" reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere." rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note" attachCancel: "Rimuovi allegato" -deleteFile: "File da Drive eliminato" -markAsSensitive: "Segna come esplicito" -unmarkAsSensitive: "Non segnare come esplicito " +markAsSensitive: "Segna come sensibile" +unmarkAsSensitive: "Segna come non sensibile" enterFileName: "Nome del file" -mute: "Silenziare" +mute: "Silenzia" unmute: "Riattiva l'audio" -renoteMute: "Silenziare le Rinota" -renoteUnmute: "Non silenziare le Rinota" -block: "Bloccare" -unblock: "Sbloccare" -suspend: "Sospensione" +renoteMute: "Silenzia i Rinota" +renoteUnmute: "Non silenziare i Rinota" +block: "Blocca" +unblock: "Sblocca" +suspend: "Sospendi" unsuspend: "Revoca la sospensione" blockConfirm: "Vuoi davvero bloccare il profilo?" unblockConfirm: "Vuoi davvero sbloccare il profilo?" -suspendConfirm: "Vuoi davvero sospendere questo profilo?" +suspendConfirm: "Vuoi sospendere questo profilo?" unsuspendConfirm: "Vuoi revocare la sospensione si questo profilo?" selectList: "Seleziona una lista" -editList: "Modifica Lista" selectChannel: "Seleziona canale" selectAntenna: "Scegli un'antenna" -editAntenna: "Modifica Antenna" -createAntenna: "Crea Antenna" selectWidget: "Seleziona il riquadro" editWidgets: "Modifica i riquadri" editWidgetsExit: "Conferma le modifiche" -customEmojis: "Emoji personalizzate" +customEmojis: "Emoji personalizzati" emoji: "Emoji" emojis: "Emoji" emojiName: "Nome dell'emoji" @@ -173,41 +150,33 @@ addEmoji: "Aggiungi un emoji" settingGuide: "Configurazione suggerita" cacheRemoteFiles: "Memorizza i file remoti nella cache" cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime." -youCanCleanRemoteFilesCache: "Puoi svuotare tutta la cache cliccando il bottone 🗑️ nella gestione file" -cacheRemoteSensitiveFiles: "Copia nella cache locale i file espliciti remoti" -cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file espliciti verranno richiesti direttamente all'istanza remota senza essere salvati nel server locale." flagAsBot: "Io sono un robot" flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene d’interazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot." -flagAsCat: "MIIaaaoo!!! (Io sono un gatto è un romanzo del 1905, il primo dello scrittore giapponese Natsume Sōseki)" -flagAsCatDescription: "Miaoo mia miao mi miao?" +flagAsCat: "Sono un gatto" +flagAsCatDescription: "La modalità \"sono un gatto\" aggiunge le orecchie al tuo profilo" flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline." flagShowTimelineRepliesDescription: "Attivando, la timeline mostra le Note del profilo ed anche le risposte ad altre Note" -autoAcceptFollowed: "Accetta automaticamente le richieste di follow da profili che già segui" +autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui" addAccount: "Aggiungi profilo" reloadAccountsList: "Ricarica l'elenco dei profili" loginFailed: "Accesso non riuscito" showOnRemote: "Leggi sull'istanza remota" -continueOnRemote: "Continua da remoto" -chooseServerOnMisskeyHub: "Scegli l'istanza sul sito Misskey Hub" -specifyServerHost: "Indica l'indirizzo dell'istanza" -inputHostName: "Digita il nome del dominio " general: "Generali" wallpaper: "Sfondo" setWallpaper: "Imposta sfondo" removeWallpaper: "Elimina lo sfondo" searchWith: "Cerca: {q}" youHaveNoLists: "Non hai ancora creato nessuna lista" -followConfirm: "Confermi il Following a {name}?" +followConfirm: "Vuoi seguire {name}?" proxyAccount: "Profilo proxy" proxyAccountDescription: "Un profilo proxy funziona come follower per i profili remoti, sotto certe condizioni. Ad esempio, quando un profilo locale ne inserisce uno remoto in una lista (senza seguirlo), se nessun altro segue quel profilo remoto, le attività non possono essere distribuite. Dunque, il profilo proxy le seguirà per tutti." -host: "Host" -selectSelf: "Segli me" +host: "Server remoto" selectUser: "Seleziona profilo" recipient: "Destinatario" annotation: "Annotazione preventiva" federation: "Federazione" instances: "Istanza" -registeredAt: "Prima federazione" +registeredAt: "Registrato presso" latestRequestReceivedAt: "Ultima richiesta ricevuta" latestStatus: "Ultimo stato" storageUsage: "Capienza dei dischi" @@ -215,12 +184,9 @@ charts: "Grafici" perHour: "orario" perDay: "giornaliero" stopActivityDelivery: "Interrompi la distribuzione di attività" -blockThisInstance: "Bloccare l'istanza" -silenceThisInstance: "Silenziare l'istanza" -mediaSilenceThisInstance: "Silenzia i media dell'istanza" +blockThisInstance: "Blocca questa istanza" operations: "Operazioni" software: "Software" -softwareName: "Nome del software" version: "Versione" metadata: "Metadato" withNFiles: "{n} file in allegato" @@ -229,7 +195,7 @@ jobQueue: "Coda di lavoro" cpuAndMemory: "CPU e Memoria" network: "Rete" disk: "Disco" -instanceInfo: "Informazioni sul server" +instanceInfo: "Informazioni sull'istanza" statistics: "Statistiche" clearQueue: "Svuota coda" clearQueueConfirmTitle: "Vuoi davvero svuotare la coda?" @@ -238,19 +204,14 @@ clearCachedFiles: "Svuota cache" clearCachedFilesConfirm: "Vuoi davvero svuotare la cache da tutti i file remoti?" blockedInstances: "Istanze bloccate" blockedInstancesDescription: "Elenca le istanze che vuoi bloccare, una per riga. Esse non potranno più interagire con la tua istanza." -silencedInstances: "Istanze silenziate" -silencedInstancesDescription: "Elenca i nomi host delle istanze che vuoi silenziare. Tutti i profili nelle istanze silenziate vengono trattati come tali. Possono solo inviare richieste di follow e menzionare soltanto i profili locali che seguono. Le istanze bloccate non sono interessate." -mediaSilencedInstances: "Istanze coi media silenziati" -mediaSilencedInstancesDescription: "Elenca i nomi host delle istanze di cui vuoi silenziare i media, uno per riga. Tutti gli allegati dei profili nelle istanze silenziate per via degli allegati espliciti, verranno impostati come tali, le emoji personalizzate non saranno disponibili. Le istanze bloccate sono escluse." -federationAllowedHosts: "Server a cui consentire la federazione" -federationAllowedHostsDescription: "Indica gli host dei server a cui è consentita la federazione, uno per ogni linea." -muteAndBlock: "Silenziare e bloccare" +muteAndBlock: "Silenziati / Bloccati" mutedUsers: "Profili silenziati" blockedUsers: "Profili bloccati" noUsers: "Non ci sono profili" editProfile: "Modifica profilo" noteDeleteConfirm: "Vuoi davvero eliminare questa Nota?" pinLimitExceeded: "Non puoi fissare altre note " +intro: "L'installazione di Misskey è terminata! Si prega di creare il profilo amministratore." done: "Fine" processing: "In elaborazione" preview: "Anteprima" @@ -265,9 +226,9 @@ all: "Tutte" subscribing: "Iscrizione" publishing: "Pubblicazione" notResponding: "Nessuna risposta" -instanceFollowing: "Istanza Following" +instanceFollowing: "Seguiti dall'istanza" instanceFollowers: "Follower dell'istanza" -instanceUsers: "Profili nell'istanza" +instanceUsers: "Utenti dell'istanza" changePassword: "Aggiorna Password" security: "Sicurezza" retypedNotMatch: "Le password non corrispondono." @@ -276,7 +237,7 @@ newPassword: "Nuova Password" newPasswordRetype: "Conferma password" attachFile: "Allega file" more: "Di più!" -featured: "In evidenza" +featured: "Tendenze" usernameOrUserId: "Nome utente o ID" noSuchUser: "Profilo non trovato" lookup: "Ricerca remota" @@ -285,10 +246,10 @@ imageUrl: "URL dell'immagine" remove: "Elimina" removed: "Eliminato con successo" removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?" -deleteAreYouSure: "Vuoi davvero eliminare \"{x}\"?" +deleteAreYouSure: "Eliminare \"{x}\"?" resetAreYouSure: "Ripristinare?" -areYouSure: "Confermi?" saved: "Salvato" +messaging: "Messaggi" upload: "Carica" keepOriginalUploading: "Conservare l'immagine originale." keepOriginalUploadingDescription: "Conserva la versione originale quando si caricano le immagini. Se è disattivato, il browser genera l'immagine per la pubblicazione sul Web durante il caricamento." @@ -298,17 +259,16 @@ uploadFromUrl: "Incolla URL immagine" uploadFromUrlDescription: "URL del file che vuoi caricare" uploadFromUrlRequested: "Caricamento richiesto" uploadFromUrlMayTakeTime: "Il caricamento del file può richiedere tempo." -uploadNFiles: "Caricare {n} file singolarmente" explore: "Esplora" messageRead: "Visualizzato" noMoreHistory: "Non c'è più cronologia da visualizzare" -startChat: "Inizia a chattare" +startMessaging: "Nuovo messaggio" nUsersRead: "Letto da {n} persone" agreeTo: "Sono d'accordo con {0}" -agree: "Accetto" +agree: "D'accordo" agreeBelow: "Accetto quanto riportato sotto" basicNotesBeforeCreateAccount: "Note importanti" -termsOfService: "Condizioni d'uso del servizio" +termsOfService: "Informativa Privacy" start: "Inizia!" home: "Home" remoteUserCaution: "Le informazioni potrebbero essere incomplete poiché questo profilo remoto potrebbe non essere completamente federato." @@ -317,7 +277,7 @@ images: "Immagini" image: "Immagini" birthday: "Compleanno" yearsOld: "{age} anni" -registeredDate: "Data iscrizione" +registeredDate: "Iscrizione a.." location: "Posizione" theme: "Tema" themeForLightMode: "Tema da utilizzare per il modo chiaro" @@ -333,15 +293,12 @@ selectFile: "Scelta allegato" selectFiles: "Scelta allegato" selectFolder: "Seleziona cartella" selectFolders: "Seleziona cartella" -fileNotSelected: "Nessun file selezionato" renameFile: "Rinomina file" folderName: "Nome della cartella" createFolder: "Nuova cartella" renameFolder: "Rinomina cartella" deleteFolder: "Elimina cartella" -folder: "Cartella" addFile: "Allega" -showFile: "Visualizza file" emptyDrive: "Il Drive è vuoto" emptyFolder: "La cartella è vuota" unableToDelete: "Eliminazione impossibile" @@ -354,11 +311,10 @@ copyUrl: "Copia URL" rename: "Modifica nome" avatar: "Foto del profilo" banner: "Intestazione" -displayOfSensitiveMedia: "Visibilità dei media espliciti" whenServerDisconnected: "Quando la connessione col server è persa" -disconnectedFromServer: "Connessione persa" +disconnectedFromServer: "Il server si è disconnesso" reload: "Ricarica" -doNothing: "Ignora" +doNothing: "Nessun'azione" reloadConfirm: "Vuoi ricaricare?" watch: "Osserva" unwatch: "Smetti di Osserva" @@ -369,7 +325,7 @@ instanceName: "Nome dell'istanza" instanceDescription: "Descrizione dell'istanza" maintainerName: "Nome dell'amministratore" maintainerEmail: "Indirizzo e-mail dell'amministratore" -tosUrl: "URL delle condizioni d'uso" +tosUrl: "URL dei termini del servizio e della privacy" thisYear: "Anno" thisMonth: "Mese" today: "Oggi" @@ -383,29 +339,26 @@ disconnectService: "Disconnetti" enableLocalTimeline: "Abilita la timeline locale" enableGlobalTimeline: "Abilita la timeline federata" disablingTimelinesInfo: "Anche disabilitandole, gli Amministratori e i Moderatori potranno comunque accedervi." -registration: "Registrazione" +registration: "Iscriviti" +enableRegistration: "Consenti a chiunque di registrarsi" invite: "Invita" driveCapacityPerLocalAccount: "Capienza del Drive per profilo locale" driveCapacityPerRemoteAccount: "Capienza del Drive per profilo remoto" inMb: "in Megabytes" +iconUrl: "URL di icona (favicon, ecc.)" bannerUrl: "URL dell'immagine d'intestazione" backgroundImageUrl: "URL dello sfondo" basicInfo: "Informazioni fondamentali" -pinnedUsers: "Profili in evidenza" -pinnedUsersDescription: "Elenca i profili delle persone che vuoi fissare nella pagina \"Esplora\"." +pinnedUsers: "Utenti in evidenza" +pinnedUsersDescription: "Elenca gli/le utenti che vuoi fissare in cima alla pagina \"Esplora\", un@ per riga." pinnedPages: "Pagine in evidenza" pinnedPagesDescription: "Specifica il percorso delle pagine che vuoi fissare in cima alla pagina dell'istanza. Una pagina per riga." pinnedClipId: "ID della Clip in evidenza" -pinnedNotes: "Note in primo piano" +pinnedNotes: "Nota fissata" hcaptcha: "hCaptcha" enableHcaptcha: "Abilita hCaptcha" hcaptchaSiteKey: "Chiave del sito" hcaptchaSecretKey: "Chiave segreta" -mcaptcha: "mCaptcha" -enableMcaptcha: "Abilita hCaptcha" -mcaptchaSiteKey: "Chiave del sito" -mcaptchaSecretKey: "Chiave segreta" -mcaptchaInstanceUrl: "URL della istanza mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Abilita reCAPTCHA" recaptchaSiteKey: "Chiave del sito" @@ -421,50 +374,43 @@ name: "Nome" antennaSource: "Fonte dell'antenna" antennaKeywords: "Parole chiavi da ricevere" antennaExcludeKeywords: "Parole chiavi da escludere" -antennaExcludeBots: "Escludere i Bot" -antennaKeywordsDescription: "Sparando con uno spazio indichi la condizione E (and). Separando con un a capo, indichi la condizione O (or)." +antennaKeywordsDescription: "Separare con uno spazio indica la condizione \"E\". Separare con un'interruzzione riga indica la condizione \"O\"." notifyAntenna: "Invia notifiche delle nuove note" withFileAntenna: "Solo note con file in allegato" -excludeNotesInSensitiveChannel: "Escludere le Note dai canali espliciti" enableServiceworker: "Abilita ServiceWorker" -antennaUsersDescription: "Elenca un nome utente per riga" +antennaUsersDescription: "Inserisci solo un nome utente per riga" caseSensitive: "Sensibile alla distinzione tra maiuscole e minuscole" withReplies: "Includere le risposte" connectedTo: "Connessione ai seguenti profili:" notesAndReplies: "Note e risposte" -withFiles: "Con allegati" -silence: "Silenziare" -silenceConfirm: "Vuoi davvero silenziare questo profilo?" +withFiles: "Con file in allegato" +silence: "Silenzia" +silenceConfirm: "Vuoi davvero silenziare l'utente?" unsilence: "Riattiva" -unsilenceConfirm: "Vuoi davvero riattivare questo profilo?" -popularUsers: "Profili popolari" +unsilenceConfirm: "Vuoi davvero riattivare l'utente?" +popularUsers: "Utenti popolari" recentlyUpdatedUsers: "Utenti attivi di recente" -recentlyRegisteredUsers: "Profili iscritti di recente" -recentlyDiscoveredUsers: "Profili scoperti di recente" -exploreUsersCount: "Ci sono {count} profili" +recentlyRegisteredUsers: "Utenti registrati di recente" +recentlyDiscoveredUsers: "Utenti scoperti di recente" +exploreUsersCount: "Ci sono {count} utenti" exploreFediverse: "Esplora il Fediverso" -popularTags: "Hashtag popolari" +popularTags: "Tag di tendenza" userList: "Liste" about: "Informazioni" -aboutMisskey: "A proposito di Misskey" +aboutMisskey: "Informazioni di Misskey" administrator: "Amministratore" token: "Token" 2fa: "Autenticazione a due fattori" -setupOf2fa: "Impostare l'autenticazione a due fattori" -totp: "App di autenticazione a due fattori (2FA/MFA)" -totpDescription: "Puoi autenticarti inserendo un codice OTP tramite la tua App di autenticazione a due fattori (2FA/MFA)" +totp: "App di autenticazione" +totpDescription: "Inserisci un codice OTP tramite un'app di autenticazione" moderator: "Moderatore" moderation: "moderazione" -moderationNote: "Promemoria di moderazione" -moderationNoteDescription: "Puoi scrivere promemoria condivisi solo tra moderatori." -addModerationNote: "Aggiungi promemoria di moderazione" -moderationLogs: "Cronologia di moderazione" -nUsersMentioned: "{n} profili ne parlano" +nUsersMentioned: "{n} profili menzionati" securityKeyAndPasskey: "Chiave di sicurezza e accesso" securityKey: "Chiave di sicurezza" lastUsed: "Ultima attività" lastUsedAt: "Uso più recente: {t}" -unregister: "Rimuovi autenticazione a due fattori (2FA/MFA)" +unregister: "Annulla l'iscrizione" passwordLessLogin: "Accedi senza password" passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza" resetPassword: "Ripristina la password" @@ -474,7 +420,8 @@ share: "Condividi" notFound: "Non trovato" notFoundDescription: "Nessuna pagina corrisponde all'URL indicata." uploadFolder: "Destinazione caricamento predefinita" -markAsReadAllNotifications: "Segnare tutte le notifiche come lette" +cacheClear: "Svuota cache" +markAsReadAllNotifications: "Segna tutte le notifiche come lette" markAsReadAllUnreadNotes: "Segna tutte le note come lette" markAsReadAllTalkMessages: "Segna tutte le chat come lette" help: "Guida" @@ -491,16 +438,16 @@ retype: "Conferma" noteOf: "Note di {user}" quoteAttached: "Citazione allegata" quoteQuestion: "Vuoi aggiungere una citazione?" -attachAsFileQuestion: "Il testo copiato eccede le dimensioni, vuoi allegarlo?" +noMessagesYet: "Ancora nessuna chat" +newMessageExists: "Hai ricevuto un nuovo messaggio" onlyOneFileCanBeAttached: "È possibile allegare al messaggio soltanto uno file" signinRequired: "Occorre avere un profilo registrato su questa istanza" -signinOrContinueOnRemote: "Per continuare, devi accedere alla tua istanza o registrarti su questa e poi accedere" invitations: "Invita" invitationCode: "Codice di invito" checking: "Confermando" available: "Disponibile" -unavailable: "Non puoi usarlo" -usernameInvalidFormat: "Il nome utente deve avere solo caratteri alfanumerici e trattino basso '_'" +unavailable: "Il nome utente è già in uso" +usernameInvalidFormat: "Il nome utente può contenere solo lettere, numeri e '_'" tooShort: "Troppo breve" tooLong: "Troppo lungo" weakPassword: "Password debole" @@ -516,15 +463,11 @@ uiLanguage: "Lingua di visualizzazione dell'interfaccia" aboutX: "Informazioni su {x}" emojiStyle: "Stile emoji" native: "Nativo" -menuStyle: "Stile menu" -style: "Stile" -drawer: "Drawer" -popup: "Popup" +disableDrawer: "Non mostrare il menù sul drawer" showNoteActionsOnlyHover: "Mostra le azioni delle Note solo al passaggio del mouse" -showReactionsCount: "Visualizza il numero di reazioni su una nota" noHistory: "Nessuna cronologia" signinHistory: "Storico degli accessi al profilo" -enableAdvancedMfm: "Attivare i Misskey Flavoured Markdown (MFM) avanzati" +enableAdvancedMfm: "Attiva MFM avanzati" enableAnimatedMfm: "Attiva MFM animati" doing: "In corso..." category: "Categoria" @@ -536,11 +479,11 @@ regenerate: "Generare di nuovo" fontSize: "Dimensione carattere" mediaListWithOneImageAppearance: "Altezza dell'elenco media con una sola immagine " limitTo: "Limita a {x}" -noFollowRequests: "Non ci sono richieste di relazione" +noFollowRequests: "Non hai alcuna richiesta di follow" openImageInNewTab: "Apri le immagini in un nuovo tab" dashboard: "Pannello di controllo" local: "Locale" -remote: "Remota" +remote: "Remoto" total: "Totale" weekOverWeekChanges: "Settimanale" dayOverDayChanges: "Giornaliero" @@ -552,8 +495,8 @@ promote: "Pubblicizza" numberOfDays: "Numero di giorni" hideThisNote: "Nasconda la nota" showFeaturedNotesInTimeline: "Mostrare le note di tendenza nella tua timeline" -objectStorage: "Storage S3" -useObjectStorage: "Utilizza lo storage S3 in cloud" +objectStorage: "Stoccaggio oggetti" +useObjectStorage: "Utilizza stoccaggio oggetti" objectStorageBaseUrl: "Base URL" objectStorageBaseUrlDesc: "URL di riferimento. In caso di utilizzo di proxy o CDN l'URL è 'https://.s3.amazonaws.com' per S3, 'https://storage.googleapis.com/' per GCS eccetera. " objectStorageBucket: "Bucket" @@ -574,22 +517,16 @@ serverLogs: "Log del server" deleteAll: "Cancella cronologia" showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline" showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline" -withRepliesByDefaultForNewlyFollowed: "Quando segui nuovi profili, includi le risposte in TL come impostazione predefinita" -newNoteRecived: "Nuove Note da leggere" -newNote: "Nuova Nota" +newNoteRecived: "Vedi le nuove note" sounds: "Impostazioni suoni" -sound: "Suono" -notificationSoundSettings: "Preferenze di notifica" +sound: "Impostazioni suoni" listen: "Ascolta" none: "Nessuno" showInPage: "Visualizza in pagina" popout: "Finestra pop-out" volume: "Volume" masterVolume: "Volume principale" -notUseSound: "Non emettere suoni" -useSoundOnlyWhenActive: "Emetti suoni solo quando Misskey è in attività" details: "Dettagli" -renoteDetails: "Dettagli della Rinota" chooseEmoji: "Scegli emoji" unableToProcess: "Impossibile compiere l'operazione" recentUsed: "Usato di recente" @@ -601,26 +538,20 @@ installedDate: "Data installazione" lastUsedDate: "Data di ultimo uso" state: "Stato" sort: "Ordina per" -ascendingOrder: "Aumenta" -descendingOrder: "Diminuisce" +ascendingOrder: "Ascendente" +descendingOrder: "Discendente" scratchpad: "ScratchPad" scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScript. È possibile scrivere, eseguire e confermare i risultati dell'interazione del codice con Misskey." -uiInspector: "UI Inspector" -uiInspectorDescription: "Puoi visualizzare un elenco di elementi UI presenti in memoria. I componenti dell'interfaccia utente vengono generati dalle funzioni Ui:C:." -output: "Output" +output: "Uscita" script: "Script" -disablePagesScript: "Disabilitare AiScript nelle pagine" -updateRemoteUser: "Aggiorna dati dal profilo remoto" -unsetUserAvatar: "Rimozione foto profilo" -unsetUserAvatarConfirm: "Vuoi davvero rimuovere la foto profilo?" -unsetUserBanner: "Rimuovi intestazione profilo" -unsetUserBannerConfirm: "Vuoi davvero rimuovere l'intestazione dal profilo?" +disablePagesScript: "Disabilita AiScript nelle pagine" +updateRemoteUser: "Aggiornare le informazioni di utente remot@" deleteAllFiles: "Elimina tutti i file" deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?" -removeAllFollowing: "Annulla tutti i follow" -removeAllFollowingDescription: "Togli il Following a tutti i profili su {host}. Utile, ad esempio, quando l'istanza non esiste più." +removeAllFollowing: "Cancella tutti i follows" +removeAllFollowingDescription: "Cancella tutti i follows del server {host}. Per favore, esegui se, ad esempio, l'istanza non esiste più." userSuspended: "L'utente è in sospensione" -userSilenced: "Profilo silenziato" +userSilenced: "L'utente è silenziat@." yourAccountSuspendedTitle: "Questo profilo è sospeso" yourAccountSuspendedDescription: "Questo profilo è stato sospeso a causa di una violazione del regolamento. Per informazioni, contattare l'amministrazione. Si prega di non creare un nuovo account." tokenRevoked: "Il token non è valido" @@ -641,7 +572,7 @@ invisibleNote: "Nota invisibile" enableInfiniteScroll: "Abilita scorrimento infinito" visibility: "Visibilità" poll: "Sondaggio" -useCw: "Contenuto esplicito" +useCw: "Content Warning" enablePlayer: "Visualizza" disablePlayer: "Chiudi" expandTweet: "Espandi tweet" @@ -665,9 +596,8 @@ medium: "Medio" small: "Piccolo" generateAccessToken: "Genera token di accesso" permission: "Autorizzazioni " -adminPermission: "Privilegi amministrativi" enableAll: "Abilita tutto" -disableAll: "Disabilitare tutto" +disableAll: "Disabilita tutto" tokenRequested: "Autorizza accesso al profilo" pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui." notificationType: "Tipo di notifiche" @@ -678,28 +608,22 @@ emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronic email: "Email" emailAddress: "Indirizzo di posta elettronica" smtpConfig: "Impostazioni del server SMTP" -smtpHost: "Host SMTP" +smtpHost: "Server remoto" smtpPort: "Porta" smtpUser: "Nome utente" smtpPass: "Password" -emptyToDisableSmtpAuth: "Lasciare i campi vuoti se non c'è autenticazione SMTP" -smtpSecure: "Usare SSL/TLS implicito per le connessioni SMTP" +emptyToDisableSmtpAuth: "Lasciare il nome utente e la password vuoti per disabilitare la verifica SMTP" +smtpSecure: "Usare la porta SSL/TLS implicito per le connessioni SMTP" smtpSecureInfo: "Disabilitare quando è attivo STARTTLS." -testEmail: "Verifica il funzionamento" +testEmail: "Testa la consegna di posta elettronica" wordMute: "Filtri parole" -wordMuteDescription: "Contrae le Note con la parola o la frase specificata. Permette di espandere le Note, cliccandole." -hardWordMute: "Filtro parole forte" -showMutedWord: "Elenca le parole silenziate" -hardWordMuteDescription: "Nasconde le Note con la parola o la frase specificata. A differenza delle parole silenziate, la Nota non verrà federata." regexpError: "errore regex" regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:" -instanceMute: "Silenziare l'istanza" +instanceMute: "Silenzia l'istanza" userSaysSomething: "{name} ha detto qualcosa" -userSaysSomethingAbout: "{name} ha Notato a riguardo di \"{word}\"" makeActive: "Attiva" display: "Visualizza" copy: "Copia" -copiedToClipboard: "Copiato negli appunti" metrics: "Statistiche" overview: "Anteprima" logs: "Log" @@ -708,30 +632,31 @@ database: "Base dati" channel: "Canale" create: "Crea" notificationSetting: "Impostazioni notifiche" -notificationSettingDesc: "Scegli quali notifiche mostrare." +notificationSettingDesc: "Seleziona il tipo di notifiche da visualizzare." useGlobalSetting: "Usa impostazioni generali" useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifiche del profilo. Altrimenti si possono segliere impostazioni personalizzate." -other: "Eccetera" +other: "Avanzate" regenerateLoginToken: "Genera di nuovo un token di connessione" regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi." -theKeywordWhenSearchingForCustomEmoji: "Questa sarà la parola chiave durante la ricerca di emoji personalizzate" setMultipleBySeparatingWithSpace: "È possibile creare multiple voci separate da spazi." fileIdOrUrl: "ID o URL del file" behavior: "Comportamento" sample: "Esempio" abuseReports: "Segnalazioni" -reportAbuse: "Segnalare" -reportAbuseRenote: "Segnalare la Rinota" -reportAbuseOf: "Segnalare {name}" -fillAbuseReportDescription: "Per favore, spiegaci il motivo della segnalazione. Se riguarda una Nota precisa, indica anche l'indirizzo URL." +reportAbuse: "Segnalazioni" +reportAbuseOf: "Segnala {name}" +fillAbuseReportDescription: "Si prega di spiegare il motivo della segnalazione. Se riguarda una nota precisa, si prega di collegare anche l'URL della nota." abuseReported: "La segnalazione è stata inviata. Grazie." reporter: "il corrispondente" -reporteeOrigin: "Segnalazione a" -reporterOrigin: "Segnalazione da" +reporteeOrigin: "Origine del segnalato" +reporterOrigin: "Origine del segnalatore" +forwardReport: "Inoltro di un report a un'istanza remota." +forwardReportIsAnonymous: "L'istanza remota non vedrà le tue informazioni, apparirai come profilo di sistema, anonimo." send: "Inviare" +abuseMarkAsResolved: "Contrassegna la segnalazione come risolta" openInNewTab: "Apri in una nuova scheda" openInSideView: "Apri in vista laterale" -defaultNavigationBehaviour: "Tipo di navigazione predefinita" +defaultNavigationBehaviour: "Navigazione preimpostata" editTheseSettingsMayBreakAccount: "Modificare queste impostazioni può danneggiare il profilo" instanceTicker: "Informazioni sull'istanza da cui vengono le note" waitingFor: "Aspettando {x}" @@ -746,7 +671,6 @@ createNewClip: "Crea una Clip" unclip: "Togli Nota dalla Clip" confirmToUnclipAlreadyClippedNote: "Questa nota è già inclusa in \"{name}\". Si desidera escludere la nota?" public: "Pubblica" -private: "Privato" i18nInfo: "Misskey è tradotto in diverse lingue da volontari. Anche tu puoi contribuire su {link}." manageAccessTokens: "Gestisci token di accesso" accountInfo: "Informazioni profilo" @@ -755,7 +679,7 @@ repliesCount: "Numero di risposte inviate" renotesCount: "Numero di note che hai ricondiviso" repliedCount: "Numero di risposte ricevute" renotedCount: "Numero delle tue note ricondivise" -followingCount: "Numero di Following" +followingCount: "Numero di profili seguiti" followersCount: "Numero di profili che ti seguono" sentReactionsCount: "Numero di reazioni inviate" receivedReactionsCount: "Numero di reazioni ricevute" @@ -768,10 +692,9 @@ driveUsage: "Utilizzazione del Drive" noCrawle: "Rifiuta l'indicizzazione dai robot." noCrawleDescription: "Richiedi che i motori di ricerca non indicizzino la tua pagina di profilo, le tue note, pagine, ecc." lockedAccountInfo: "A meno che non imposti la visibilità delle tue note su \"Solo ai follower\", le tue note sono visibili da tutti, anche se hai configurato l'account per confermare manualmente le richieste di follow." -alwaysMarkSensitive: "Segnare automaticamente come espliciti gli allegati" +alwaysMarkSensitive: "Segnare i media come sensibili per impostazione predefinita" loadRawImages: "Visualizza le intere immagini allegate invece delle miniature." -disableShowingAnimatedImages: "Disabilitare le immagini animate" -highlightSensitiveMedia: "Evidenzia i media espliciti" +disableShowingAnimatedImages: "Disabilita le immagini animate" verificationEmailSent: "Una mail di verifica è stata inviata. Si prega di accedere al collegamento per compiere la verifica." notSet: "Non impostato" emailVerified: "Il tuo indirizzo email è stato verificato" @@ -787,6 +710,7 @@ thisIsExperimentalFeature: "Questa è una funzionalità sperimentale. Potrebbe e developer: "Sviluppatore" makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\"" makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"." +showGapBetweenNotesInTimeline: "Mostrare un intervallo tra le note sulla timeline" duplicate: "Duplica" left: "Sinistra" center: "Centro" @@ -794,11 +718,10 @@ wide: "Largo" narrow: "Stretto" reloadToApplySetting: "Le tue preferenze verranno impostate dopo il ricaricamento della pagina. Vuoi ricaricare adesso?" needReloadToApply: "È necessario riavviare per rendere effettive le modifiche." -needToRestartServerToApply: "Per attivare le modifiche, occorre riavviare il server." showTitlebar: "Visualizza la barra del titolo" clearCache: "Svuota la cache" -onlineUsersCount: "{n} persone attive adesso" -nUsers: "{n} profili" +onlineUsersCount: "{n} utenti online" +nUsers: "{n} utenti" nNotes: "{n}Note" sendErrorReports: "Invia segnalazioni di errori" sendErrorReportsDescription: "Quando abilitato, se si verifica un problema, informazioni dettagliate sugli errori verranno condivise con Misskey in modo da aiutare a migliorare la qualità del software.\nCiò include informazioni come la versione del sistema operativo, il tipo di navigatore web che usi, la cronologia delle attività, ecc." @@ -828,7 +751,7 @@ editCode: "Modifica codice" apply: "Applica" receiveAnnouncementFromInstance: "Ricevi i messaggi informativi dall'istanza" emailNotification: "Eventi per notifiche via mail" -publish: "Pubblicare" +publish: "Pubblico" inChannelSearch: "Cerca in canale" useReactionPickerForContextMenu: "Cliccare sul tasto destro per aprire il pannello di reazioni" typingUsers: "{users} sta(nno) scrivendo" @@ -844,13 +767,13 @@ addDescription: "Aggiungi descrizione" userPagePinTip: "Qui puoi appuntare note, premendo \"Fissa sul profilo\" nel menù delle singole note." notSpecifiedMentionWarning: "Sono stati menzionati profili non inclusi fra i destinatari" info: "Informazioni" -userInfo: "Informazioni sul profilo" +userInfo: "Informazioni utente" unknown: "Sconosciuto" onlineStatus: "Stato di connessione" -hideOnlineStatus: "Modalità invisibile" -hideOnlineStatusDescription: "Attivando questa opzione potresti ridurre l'usabilità di alcune funzioni, come la ricerca." +hideOnlineStatus: "Stato invisibile" +hideOnlineStatusDescription: "Abilitare l'opzione di stato invisibile può guastare la praticità di singole funzioni, come la ricerca." online: "Online" -active: "Attivo" +active: "Attiv@" offline: "Offline" notRecommended: "Sconsigliato" botProtection: "Protezione contro i bot" @@ -864,14 +787,13 @@ user: "Profilo" administration: "Gestione" accounts: "Profilo" switch: "Cambia" -noMaintainerInformationWarning: "Mancano le informazioni sull'amministratore." -noInquiryUrlWarning: "Non è stata impostata la URL di contatto" -noBotProtectionWarning: "Non è stata impostata alcuna protezione dai Bot" +noMaintainerInformationWarning: "Le informazioni amministratore non sono impostate." +noBotProtectionWarning: "Nessuna protezione impostata contro i bot." configure: "Imposta" postToGallery: "Pubblicare nella galleria" postToHashtag: "Pubblica a questo hashtag" -gallery: "Gallerie" -recentPosts: "Pubblicazioni recenti" +gallery: "Galleria" +recentPosts: "Le più recenti" popularPosts: "Le più visualizzate" shareWithNote: "Condividere in nota" ads: "Banner" @@ -888,34 +810,34 @@ previewNoteText: "Anteprima del testo" customCss: "CSS personalizzato" customCssWarn: "Questa impostazione deve essere eseguita da una persona esperta. Una configurazione errata può impedire al client di utilizzare correttamente il sistema." global: "Federata" -squareAvatars: "Foto profilo squadrate" -sent: "Inviato" +squareAvatars: "Mostra l'immagine del profilo come quadrato" +sent: "Inviare" received: "Ricevuto" searchResult: "Risultati della Ricerca" hashtags: "Hashtag" troubleshooting: "Risoluzione problemi" useBlurEffect: "Utilizza effetto sfocatura" -learnMore: "Per saperne di più" +learnMore: "Più dettagli" misskeyUpdated: "Misskey è stato aggiornato!" -whatIsNew: "Informazioni sull'aggiornamento" -translate: "Traduci" -translatedFrom: "Traduzione da {x}" +whatIsNew: "Visualizza le informazioni sull'aggiornamento" +translate: "Traduzione" +translatedFrom: "Tradotto da {x}" accountDeletionInProgress: "È in corso l'eliminazione del profilo" usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito." aiChanMode: "Modalità Ai" -devMode: "Modalità sviluppo" -keepCw: "Mostra i contenuti espliciti" +devMode: "Modalità sviluppatori" +keepCw: "Mantieni il Content Warning" pubSub: "Publish/Subscribe del profilo" lastCommunication: "La comunicazione più recente" resolved: "Risolto" unresolved: "Non risolto" -breakFollow: "Rimuovi Follower" -breakFollowConfirm: "Vuoi davvero togliere questo Follower?" +breakFollow: "Non seguire" +breakFollowConfirm: "Vuoi davvero togliere follower?" itsOn: "Abilitato" itsOff: "Disabilitato" on: "Acceso" off: "Spento" -emailRequiredForSignup: "L'indirizzo e-mail è obbligatorio per registrarsi" +emailRequiredForSignup: "L'ndirizzo e-mail è obbligatorio per registrarsi" unread: "Non lette" filter: "Filtri" controlPanel: "Pannello di controllo" @@ -923,14 +845,13 @@ manageAccounts: "Gestisci i profili" makeReactionsPublic: "Pubblicare la lista delle reazioni." makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a disposizione di tutti." classic: "Classico" -muteThread: "Silenziare conversazione" +muteThread: "Silenzia la conversazione" unmuteThread: "Riattiva la conversazione" -followingVisibility: "Visibilità dei Following" -followersVisibility: "Visibilità dei profili che ti seguono" -continueThread: "Altre conversazioni" +ffVisibility: "Ambito pubblico del collegamento" +ffVisibilityDescription: "È possibile impostare la portata pubblica delle informazioni sui propri follower/seguaci." +continueThread: "Altri thread." deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" incorrectPassword: "La password è errata." -incorrectTotp: "Il codice OTP è sbagliato, oppure scaduto." voteConfirm: "Votare per「{choice}」?" hide: "Nascondere" useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile" @@ -955,9 +876,6 @@ oneHour: "1 ora" oneDay: "1 giorno" oneWeek: "1 settimana" oneMonth: "Un mese" -threeMonths: "3 mesi" -oneYear: "1 anno" -threeDays: "3 giorni" reflectMayTakeTime: "Potrebbe essere necessario un po' di tempo perché ciò abbia effetto." failedToFetchAccountInformation: "Impossibile recuperare le informazioni sul profilo" rateLimitExceeded: "Superato il limite di richieste." @@ -972,17 +890,16 @@ noEmailServerWarning: "Il server di posta non è configurato." thereIsUnresolvedAbuseReportWarning: "Ci sono report non evasi." recommended: "Consigliato" check: "Verifica" -driveCapOverrideLabel: "Modificare la capienza del Drive per questo profilo" +driveCapOverrideLabel: "Modificare il limite di spazio per questo utente" driveCapOverrideCaption: "Se viene specificato meno di 0, viene annullato." requireAdminForView: "Per visualizzarli, è necessario aver effettuato l'accesso con un profilo amministratore." -isSystemAccount: "Si tratta di un profilo creato e gestito automaticamente dal sistema." -typeToConfirm: "Digita {x} per continuare" +isSystemAccount: "Questi profili vengono creati e gestiti automaticamente dal sistema" +typeToConfirm: "Per eseguire questa operazione, digitare {x}" deleteAccount: "Eliminazione profilo" -document: "Documentazione" +document: "Documento" numberOfPageCache: "Numero di pagine cache" numberOfPageCacheDescription: "Aumenta l'usabilità, ma aumenta anche il carico e l'utilizzo della memoria." logoutConfirm: "Vuoi davvero uscire da Misskey? " -logoutWillClearClientData: "All'uscita, la configurazione del client viene rimossa dal browser. Per ripristinarla quando si effettua nuovamente l'accesso, abilitare il backup automatico." lastActiveDate: "Data dell'ultimo utilizzo" statusbar: "Barra di stato" pleaseSelect: "Scegli un'opzione" @@ -994,14 +911,13 @@ type: "Tipo" speed: "Velocità" slow: "Lento" fast: "Veloce" -sensitiveMediaDetection: "Rilevamento dei contenuti espliciti" +sensitiveMediaDetection: "Rilevamento dei contenuti sensibili." localOnly: "Soltanto locale" remoteOnly: "Solo remoto" failedToUpload: "errore di caricamento" cannotUploadBecauseInappropriate: "Non è possibile caricarlo perché è stato stabilito che potrebbe contenere contenuti inappropriati." cannotUploadBecauseNoFreeSpace: "Impossibile caricare a causa della mancanza di spazio libero sul drive." cannotUploadBecauseExceedsFileSizeLimit: "Il file non può essere caricato perché eccede le dimensioni consentite." -cannotUploadBecauseUnallowedFileType: "Impossibile caricare a causa di un tipo file non autorizzato." beta: "Versione beta" enableAutoSensitive: "Determinazione automatica del NSFW" enableAutoSensitiveDescription: "Se disponibile, il flag NSFW viene impostato automaticamente sui media utilizzando l'apprendimento automatico. Anche se questa funzione è disattivata, in alcuni casi può essere impostata automaticamente." @@ -1011,11 +927,11 @@ shuffle: "Casuale" account: "Account" move: "Sposta" pushNotification: "Notifiche Push" -subscribePushNotification: "Attivare le notifiche push" -unsubscribePushNotification: "Disattivare le notifiche push" +subscribePushNotification: "Attiva le notifiche push" +unsubscribePushNotification: "Disattiva le notifiche push" pushNotificationAlreadySubscribed: "Le notifiche push sono già attivate" pushNotificationNotSupported: "Il client o il server non supporta le notifiche push" -sendPushNotificationReadMessage: "Eliminare le notifiche push dopo la relativa lettura" +sendPushNotificationReadMessage: "Elimina le notifiche push dopo la relativa lettura" sendPushNotificationReadMessageCaption: "Se possibile, verrà mostrata brevemente una notifica con il testo \"{emptyPushNotificationMessage}\". Potrebbe influire negativamente sulla durata della batteria." windowMaximize: "Ingrandisci" windowMinimize: "Contrai finestra" @@ -1033,7 +949,6 @@ neverShow: "Non mostrare più" remindMeLater: "Rimanda" didYouLikeMisskey: "Ti piace Misskey?" pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!" -correspondingSourceIsAvailable: "Il codice sorgente corrispondente è disponibile su {anchor}." roles: "Ruoli" role: "Ruolo" noRole: "Ruolo non trovato" @@ -1043,7 +958,6 @@ assign: "Assegna" unassign: "Disassegna" color: "Colore" manageCustomEmojis: "Gestisci le emoji personalizzate" -manageAvatarDecorations: "Gestire le decorazioni di foto del profilo" youCannotCreateAnymore: "Non puoi creare, hai raggiunto il limite." cannotPerformTemporary: "Indisponibilità temporanea" cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché si è raggiunto il limite di esecuzioni possibili. Per favore, riprova più tardi." @@ -1053,15 +967,14 @@ permissionDeniedError: "Errore, attività non autorizzata" permissionDeniedErrorDescription: "Non si dispone dell'autorizzazione per eseguire questa operazione." preset: "Preimpostato" selectFromPresets: "Seleziona preimpostato" -achievements: "Conquiste" +achievements: "Obiettivi raggiunti" gotInvalidResponseError: "Risposta del server non valida" gotInvalidResponseErrorDescription: "Il server potrebbe essere irraggiungibile o in manutenzione. Riprova più tardi." thisPostMayBeAnnoying: "Questa nota potrebbe essere offensiva" thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale" thisPostMayBeAnnoyingCancel: "Annulla" thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso" -collapseRenotes: "Comprimi le Rinota già viste" -collapseRenotesDescription: "Comprimi le Note con cui hai già interagito." +collapseRenotes: "Comprimi i Rinota già letti" internalServerError: "Errore interno del server" internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server" copyErrorInfo: "Copia le informazioni sull'errore" @@ -1078,37 +991,28 @@ cannotBeChangedLater: "Non sarà più modificabile" reactionAcceptance: "Reazioni consentite" likeOnly: "Solo i Like" likeOnlyForRemote: "Solo Like remoti" -nonSensitiveOnly: "Soltanto non espliciti" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Soltanto non espliciti (reazioni remote)" +nonSensitiveOnly: "Solamente non sensibili" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "Solamente non sensibili (solo Mi piace remoti)" rolesAssignedToMe: "I miei ruoli" resetPasswordConfirm: "Vuoi davvero ripristinare la password?" -sensitiveWords: "Parole esplicite" +sensitiveWords: "Parole sensibili" sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga." sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare." -prohibitedWords: "Parole proibite" -prohibitedWordsDescription: "Verrà impedito di pubblicare Note che abbiano le parole indicate. Puoi impostare più parole, separatamente, su ogni riga." -prohibitedWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare." -hiddenTags: "Hashtag nascosti" -hiddenTagsDescription: "Impedire la visualizzazione del tag impostato nei trend. Puoi impostare più valori, uno per riga." notesSearchNotAvailable: "Non è possibile cercare tra le Note." license: "Licenza" unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?" myClips: "Le mie Clip" -drivecleaner: "Pulizia del Drive" +drivecleaner: "Drive cleaner" retryAllQueuesNow: "Ritenta di consumare tutte le code" retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?" retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente." enableChartsForRemoteUser: "Abilita i grafici per i profili remoti" enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate" -enableStatsForFederatedInstances: "Informazioni statistiche sui server federati" showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note" -reactionsDisplaySize: "Grandezza delle reazioni" -limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale" +largeNoteReactions: "Ingrandisci le reazioni" noteIdOrUrl: "ID della Nota o URL" video: "Video" videos: "Video" -audio: "Audio" -audioFiles: "Audio" dataSaver: "Risparmia dati" accountMigration: "Migrazione del profilo" accountMoved: "Questo profilo ha migrato altrove:" @@ -1116,9 +1020,9 @@ accountMovedShort: "Questo profilo è stato migrato" operationForbidden: "Operazione non consentita" forceShowAds: "Mostra sempre i banner" addMemo: "Aggiungi Memo" -editMemo: "Modifica il promemoria" -reactionsList: "Chi ha reagito?" -renotesList: "Chi ha Rinotato?" +editMemo: "Modifica Memo" +reactionsList: "Elenco delle reazioni" +renotesList: "Elenco di Rinota" notificationDisplay: "Stile delle notifiche" leftTop: "In alto a sinistra" rightTop: "In alto a destra" @@ -1129,28 +1033,23 @@ vertical: "Verticale" horizontal: "Laterale" position: "Posizione" serverRules: "Regolamento" -pleaseConfirmBelowBeforeSignup: "Per iscriversi, occorre essere d'accordo con le seguenti condizioni." -pleaseAgreeAllToContinue: "Occorre accettare tutte le condizioni prima di continuare." +pleaseConfirmBelowBeforeSignup: "Ai sensi del regolamento EU 679/2016 GDPR, autorizzo il trattamento dati personali come descritto nella informativa Privacy." +pleaseAgreeAllToContinue: "Per continuare, occorre selezionare ed essere d'accordo su tutto." continue: "Continua" preservedUsernames: "Nomi utente riservati" preservedUsernamesDescription: "Elenca, uno per linea, i nomi utente che non possono essere registrati durante la creazione del profilo. La restrizione non si applica agli amministratori. Inoltre, i profili già registrati sono esenti." createNoteFromTheFile: "Crea Nota da questo file" archive: "Archivio" -archived: "Archiviato" -unarchive: "Annulla archiviazione" channelArchiveConfirmTitle: "Vuoi davvero archiviare {name}?" channelArchiveConfirmDescription: "Un canale archiviato non compare nell'elenco canali, nemmeno nei risultati di ricerca. Non può ricevere nemmeno nuove Note." thisChannelArchived: "Questo canale è stato archiviato." displayOfNote: "Visualizzazione delle Note" initialAccountSetting: "Impostazioni iniziali del profilo" -youFollowing: "Following" +youFollowing: "Seguiti" preventAiLearning: "Impedisci l'apprendimento della IA" preventAiLearningDescription: "Aggiungendo il campo \"noai\" alla risposta HTML, si indica ai Robot esterni di non usare testi e allegati per addestrare sistemi di Machine Learning (IA predittiva/generativa). Anche se è impossibile sapere se la richiesta venga onorata o semplicemente ignorata." options: "Opzioni del ruolo" specifyUser: "Profilo specifico" -lookupConfirm: "Vuoi davvero richiedere informazioni?" -openTagPageConfirm: "Vuoi davvero aprire la pagina dell'hashtag?" -specifyHost: "Host specifici" failedToPreviewUrl: "Anteprima non disponibile" update: "Aggiorna" rolesThatCanBeUsedThisEmojiAsReaction: "Ruoli che possono usare questa emoji come reazione" @@ -1165,365 +1064,6 @@ installed: "Installazione avvenuta" branding: "Branding" enableServerMachineStats: "Pubblicare le informazioni sul server" enableIdenticonGeneration: "Generazione automatica delle Identicon" -turnOffToImprovePerformance: "Disattiva, per migliorare le prestazioni" -createInviteCode: "Genera codice di invito" -createWithOptions: "Genera con opzioni" -createCount: "Conteggio inviti" -inviteCodeCreated: "Inviti generati" -inviteLimitExceeded: "Hai raggiunto il numero massimo di codici invito generabili." -createLimitRemaining: "Inviti generabili: {limit} rimanenti" -inviteLimitResetCycle: "Alle {time}, il limite verrà ripristinato a {limit}" -expirationDate: "Scadenza" -noExpirationDate: "Senza scadenza" -inviteCodeUsedAt: "Codice di invito usato alle" -registeredUserUsingInviteCode: "Codice di invito usato da" -waitingForMailAuth: "In attesa della verifica email" -inviteCodeCreator: "Codice di invito creato da" -usedAt: "Usato alle" -unused: "Inutilizzato" -used: "Utilizzato" -expired: "Scaduto" -doYouAgree: "Accetti le condizioni?" -beSureToReadThisAsItIsImportant: "Si prega di leggere attentamente perché è importante." -iHaveReadXCarefullyAndAgree: "Dichiaro di aver letto attentamente \"{x}\" e accettarne le condizioni." -dialog: "Dialogo" -icon: "Ritratto" -forYou: "Per te" -currentAnnouncements: "Annunci attuali" -pastAnnouncements: "Annunci precedenti" -youHaveUnreadAnnouncements: "Ci sono Annunci non letti" -useSecurityKey: "Per utilizzare la chiave di sicurezza o la passkey, segui le indicazioni del dispositivo" -replies: "Risposte" -renotes: "Rinota" -loadReplies: "Leggi le risposte" -loadConversation: "Leggi la conversazione" -pinnedList: "Elenco in primo piano" -keepScreenOn: "Mantenere lo schermo acceso" -verifiedLink: "Abbiamo confermato la validità di questo collegamento" -notifyNotes: "Notifica nuove Note" -unnotifyNotes: "Interrompi le notifiche di nuove Note" -authentication: "Autenticazione" -authenticationRequiredToContinue: "Per procedere, è richiesta l'autenticazione" -dateAndTime: "Data e Ora" -showRenotes: "Includi le Rinota" -edited: "Modificato" -notificationRecieveConfig: "Preferenze di notifica" -mutualFollow: "Follow reciproco" -followingOrFollower: "Following o Follower" -fileAttachedOnly: "Solo con allegati" -showRepliesToOthersInTimeline: "Risposte altrui nella TL" -hideRepliesToOthersInTimeline: "Nascondi Riposte altrui nella TL" -showRepliesToOthersInTimelineAll: "Mostra le risposte dei tuoi follow nella TL" -hideRepliesToOthersInTimelineAll: "Nascondi le risposte dei tuoi follow nella TL" -confirmShowRepliesAll: "Questa è una attività irreversibile. Vuoi davvero includere tutte le risposte dei following in TL?" -confirmHideRepliesAll: "Questa è una attività irreversibile. Vuoi davvero escludere tutte le risposte dei following in TL?" -externalServices: "Servizi esterni" -sourceCode: "Codice sorgente" -sourceCodeIsNotYetProvided: "" -repositoryUrl: "URL della repository" -repositoryUrlDescription: "Se esiste un repository il cui il codice sorgente è disponibile pubblicamente, inserisci il suo URL. Se stai utilizzando Misskey così com'è (senza alcuna modifica al codice sorgente), inserisci https://github.com/misskey-dev/misskey." -repositoryUrlOrTarballRequired: "Se non disponi di un repository pubblico, dovrai fornire un file tarball (tar). Vedere .config/example.yml per i dettagli." -feedback: "Feedback" -feedbackUrl: "URL di feedback" -impressum: "Dichiarazione di proprietà" -impressumUrl: "URL della dichiarazione di proprietà" -impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)." -privacyPolicy: "Informativa ai sensi del Reg. UE 2016/679 (GDPR)" -privacyPolicyUrl: "URL della informativa privacy" -tosAndPrivacyPolicy: "Condizioni d'uso e informativa privacy" -avatarDecorations: "Decorazioni foto profilo" -attach: "Applica" -detach: "Rimuovi" -detachAll: "Togli tutto" -angle: "Angolo" -flip: "Inverti" -showAvatarDecorations: "Mostra decorazione della foto profilo" -releaseToRefresh: "Rilascia per aggiornare" -refreshing: "Aggiornamento..." -pullDownToRefresh: "Trascinare per aggiornare" -useGroupedNotifications: "Mostra le notifiche raggruppate" -signupPendingError: "Si è verificato un problema durante la verifica del tuo indirizzo email. Potrebbe essere scaduto il collegamento temporaneo." -cwNotationRequired: "Devi indicare perché il contenuto è indicato come esplicito." -doReaction: "Reagisci" -code: "Codice" -reloadRequiredToApplySettings: "Per applicare le impostazioni, occorre ricaricare." -remainingN: "Rimangono: {n}" -overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?" -seasonalScreenEffect: "Abilita gli effetti speciali stagionali" -decorate: "Decora" -addMfmFunction: "Aggiungi decorazioni" -enableQuickAddMfmFunction: "Attiva il selettore di funzioni MFM" -bubbleGame: "Bubble Game" -sfx: "Effetti sonori" -soundWillBePlayed: "Con musica ed effetti sonori" -showReplay: "Vedi i replay" -replay: "Replay" -replaying: "Replay in corso" -endReplay: "Termina replay" -copyReplayData: "Copia replay" -ranking: "Classifica" -lastNDays: "Ultimi {n} giorni" -backToTitle: "Torna al titolo" -hemisphere: "Geolocalizzazione" -withSensitive: "Mostra le Note con allegati espliciti" -userSaysSomethingSensitive: "Note da {name} con allegati espliciti" -enableHorizontalSwipe: "Trascinare per invertire le colonne" -loading: "Caricamento" -surrender: "Annulla" -gameRetry: "Riprova" -notUsePleaseLeaveBlank: "Lasciare vuoto, se non in uso" -useTotp: "Usare il codice OTP" -useBackupCode: "Usare il codice usa-e-getta" -launchApp: "Esegui l'App" -useNativeUIForVideoAudioPlayer: "Riprodurre audio/video usando le funzionalità del browser" -keepOriginalFilename: "Mantieni il nome file originale" -keepOriginalFilenameDescription: "Disattivandola, i file verranno caricati usando nomi casuali." -noDescription: "Manca la descrizione" -alwaysConfirmFollow: "Richiedi conferma per i Follow" -inquiry: "Contattaci" -tryAgain: "Per favore riprova" -confirmWhenRevealingSensitiveMedia: "Richiedi conferma prima di mostrare gli allegati espliciti" -sensitiveMediaRevealConfirm: "Questo allegato è esplicito, vuoi vederlo?" -createdLists: "Liste create" -createdAntennas: "Antenne create" -fromX: "Da {x}" -genEmbedCode: "Ottieni il codice di incorporamento" -noteOfThisUser: "Elenco di Note di questo profilo" -clipNoteLimitExceeded: "Non è possibile aggiungere ulteriori Note a questa Clip." -performance: "Prestazioni" -modified: "Modificato" -discard: "Scarta" -thereAreNChanges: "Ci sono {n} cambiamenti" -signinWithPasskey: "Accedi con passkey" -unknownWebAuthnKey: "Questa è una passkey sconosciuta." -passkeyVerificationFailed: "La verifica della passkey non è riuscita." -passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato." -messageToFollower: "Messaggio ai follower" -target: "Riferimento" -testCaptchaWarning: "Questa funzione è destinata al test CAPTCHA. Da non utilizzare in ambiente di produzione." -prohibitedWordsForNameOfUser: "Parole proibite (nome utente)" -prohibitedWordsForNameOfUserDescription: "Il sistema rifiuta di rinominare un utente, se il nome contiene qualsiasi parola nell'elenco. Sono esenti i profili con privilegi di moderazione." -yourNameContainsProhibitedWords: "Il nome che hai scelto contiene una o più parole vietate" -yourNameContainsProhibitedWordsDescription: "Se desideri comunque utilizzare questo nome, contatta l''amministrazione." -thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autore richiede di iscriversi per vedere il contenuto" -lockdown: "Isolamento" -pleaseSelectAccount: "Per favore, seleziona un profilo" -availableRoles: "Ruoli disponibili" -acknowledgeNotesAndEnable: "Attivare dopo averne compreso il comportamento." -federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione." -federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server." -confirmOnReact: "Confermare le reazioni" -reactAreYouSure: "Vuoi davvero reagire con {emoji} ?" -markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?" -unmarkAsSensitiveConfirm: "Vuoi davvero indicare come non esplicito il contenuto multimediale?" -preferences: "Preferenze" -accessibility: "Accessibilità" -preferencesProfile: "Profilo preferenze" -copyPreferenceId: "Copia ID preferenze" -resetToDefaultValue: "Ripristina a predefinito" -overrideByAccount: "Sovrascrivere col profilo" -untitled: "Senza titolo" -noName: "Senza nome" -skip: "Salta" -restore: "Ripristina" -syncBetweenDevices: "Sincronizzazione tra i dispositivi" -preferenceSyncConflictTitle: "Sul server esiste già il valore impostato" -preferenceSyncConflictText: "Le impostazione sincronizzata salverà il valore sul server. Però, bada che esiste già un valore sul server. Quale vorresti sovrascrivere?" -preferenceSyncConflictChoiceServer: "Valore del server" -preferenceSyncConflictChoiceDevice: "Valore del dispositivo" -preferenceSyncConflictChoiceCancel: "Annulla la sincronizzazione" -paste: "Incolla" -emojiPalette: "Tavolozza emoji" -postForm: "Finestra di pubblicazione" -textCount: "Il numero di caratteri" -information: "Informazioni" -chat: "Chat" -migrateOldSettings: "Migrare le vecchie impostazioni" -migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali." -compress: "Comprimi" -right: "Destra" -bottom: "Sotto" -top: "Sopra" -embed: "Incorporare" -settingsMigrating: "Migrazione delle impostazioni. Attendere prego ... (Puoi anche migrare manualmente in un secondo momento, nel menu: Impostazioni → Altro → Migrazione delle impostazioni)" -readonly: "Sola lettura" -goToDeck: "Torna al Deck" -federationJobs: "Coda di federazione" -scrollToClose: "Scorri per chiudere" -advice: "Consiglio" -realtimeMode: "Modalità in tempo reale" -turnItOn: "Attivare" -turnItOff: "Disattivare" -emojiMute: "Silenzia emoji" -emojiUnmute: "De silenzia emoji" -muteX: "Silenzia {x}" -unmuteX: "De silenzia {x}" -abort: "Annulla" -tip: "Suggerimento" -redisplayAllTips: "Mostra tutti i suggerimenti" -hideAllTips: "Nascondi tutti i suggerimenti" -_chat: - noMessagesYet: "Ancora nessun messaggio" - newMessage: "Nuovo messaggio" - individualChat: "Chat individuale" - individualChat_description: "Puoi chattare con una persona specifica." - roomChat: "Stanza di chat" - roomChat_description: "Puoi chattare con più persone.\nInoltre, anche le persone che non consentono chat personalizzate possono chattare se gli altri accettano." - createRoom: "Crea stanza" - inviteUserToChat: "Invita a chattare altre persone" - yourRooms: "Le tue stanze" - joiningRooms: "Stanze a cui partecipi" - invitations: "Invita" - noInvitations: "Nessun invito" - history: "Cronologia" - noHistory: "Nessuna cronologia" - noRooms: "Nessuna stanza" - inviteUser: "Invita" - sentInvitations: "Inviti spediti" - join: "Entra" - ignore: "Ignora" - leave: "Esci" - members: "Membri" - searchMessages: "Cerca messaggi" - home: "Home" - send: "Inviare" - newline: "Nuova riga" - muteThisRoom: "Silenzia stanza" - deleteRoom: "Elimina stanza" - chatNotAvailableForThisAccountOrServer: "Questo server, o questo profilo ha disabilitato la chat." - chatIsReadOnlyForThisAccountOrServer: "Le chat, su questo server o su questo profilo, sono di sola lettura. Impossibile scrivere in chat o creare e partecipare a stanze." - chatNotAvailableInOtherAccount: "La chat non è disponibile nel profilo dell'altra persona." - cannotChatWithTheUser: "Impossibile chattare con questa persona" - cannotChatWithTheUser_description: "La chat potrebbe non essere disponibile, oppure l'altra persona potrebbe non esserlo." - youAreNotAMemberOfThisRoomButInvited: "Non partecipi a questa stanza di chat, ma hai ricevuto un invito. Per partecipare, accetta l'invito." - doYouAcceptInvitation: "Intendi accettare l'invito?" - chatWithThisUser: "Chatta con questa persona" - thisUserAllowsChatOnlyFromFollowers: "Questa persona permette di chattare soltanto i propri Follower." - thisUserAllowsChatOnlyFromFollowing: "Questa persona permette di chattare soltanto ai suoi Follow." - thisUserAllowsChatOnlyFromMutualFollowing: "Questa persona permette di chattare solo a relazioni reciproche." - thisUserNotAllowedChatAnyone: "Questa persona non permette di chattare a nessuno." - chatAllowedUsers: "Persone ammesse alla chat" - chatAllowedUsers_note: "Puoi chattare con le persone a cui hai già inviato un messaggio, indipendentemente da questa impostazione." - _chatAllowedUsers: - everyone: "Chiunque" - followers: "Solo i tuoi Follower" - following: "Solo i tuoi Follow" - mutual: "Solo relazioni reciproche" - none: "Nessuno" -_emojiPalette: - palettes: "Tavolozza" - enableSyncBetweenDevicesForPalettes: "Attiva la sincronizzazione tra dispositivi" - paletteForMain: "Tavolozza principale" - paletteForReaction: "Tavolozza per reazioni" -_settings: - driveBanner: "Permette di gestire e configurare il Drive, controllare il consumo di spazio e configurare il caricamento dei file." - pluginBanner: "Consentono di migliorare le funzionalità. Le estensioni si possono configurare e gestire singolarmente." - notificationsBanner: "Puoi impostare il tipo di notifiche da ricevere dal server e anche le notifiche push." - api: "API" - webhook: "Webhook" - serviceConnection: "Integrazione servizi" - serviceConnectionBanner: "Puoi gestire i codici di accesso e i Webhook per collegare App o servizi esterni." - accountData: "Dati del profilo" - accountDataBanner: "Puoi gestire i dati del tuo profilo, esportando e importando." - muteAndBlockBanner: "Puoi configurare la visibiltà dei contenuti e limitare le attività provenienti da profili specifici." - accessibilityBanner: "Puoi personalizzare e migliorare la lettura sul tuo dispositivo in modo che sia più chiaro e reattivo." - privacyBanner: "Puoi configurare la privacy del tuo profilo, come la visibilità delle Note, la visibilità del profilo nelle ricerche e l'approvazione delle relazioni tra profili." - securityBanner: "Puoi gestire la sicurezza del tuo account, la password, i modi di accesso, la generazione di codici OTP per accesso multi fattore (MFA/2FA) e la passkey." - preferencesBanner: "Puoi personalizzare il comportamento del tuo dispositivo." - appearanceBanner: "Puoi personalizzare l'aspetto nel dispositivo, in base alle tue preferenze." - soundsBanner: "Puoi personalizzare i suoni emessi dagli eventi sul tuo dispositivo." - timelineAndNote: "Note e Timeline" - makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile" - makeEveryTextElementsSelectable_description: "Potrebbe ridurre l'usabilità in alcune situazioni." - useStickyIcons: "Fissa le icone durante lo scorrimento" - enableHighQualityImagePlaceholders: "Mostra un segnaposto per immagini in alta qualità" - uiAnimations: "Animazione dell'interfaccia" - showNavbarSubButtons: "Mostra i pulsanti secondari nella barra di navigazione" - ifOn: "Quando attivato" - ifOff: "Quando disattivato" - enableSyncThemesBetweenDevices: "Sincronizzare il tema tra i dispositivi" - enablePullToRefresh: "Scorri e aggiorna" - enablePullToRefresh_description: "Clicca col mouse e gira la rotella." - realtimeMode_description: "Connette al server e aggiorna il contenuto in tempo reale. Potrebbe aumentare l'uso dei dati e il consumo della batteria." - contentsUpdateFrequency: "Frequenza di ricezione contenuti" - contentsUpdateFrequency_description: "Se l'impostazione è alta, verranno aggiornati più frequentemente, consumando più dati e più batteria." - contentsUpdateFrequency_description2: "Quando la modalità è in tempo reale, arriveranno a prescindere." - showUrlPreview: "Mostra anteprima dell'URL" - _chat: - showSenderName: "Mostra il nome del mittente" - sendOnEnter: "Invio spedisce" -_preferencesProfile: - profileName: "Nome del profilo" - profileNameDescription: "Impostare il nome che indentifica questo dispositivo." - profileNameDescription2: "Es: \"PC principale\" o \"Cellulare\"" - manageProfiles: "Gestione profili" -_preferencesBackup: - autoBackup: "Backup automatico" - restoreFromBackup: "Ripristinare da backup" - noBackupsFoundTitle: "Nessun backup trovato" - noBackupsFoundDescription: "Impossibile trovare un backup creato automaticamente. Se se hai salvato il file di backup manualmente, puoi importarlo e ripristinarlo." - selectBackupToRestore: "Seleziona un backup da ripristinare" - youNeedToNameYourProfileToEnableAutoBackup: "Per abilitare i backup automatici, è necessario indicare il nome del profilo." - autoPreferencesBackupIsNotEnabledForThisDevice: "Su questo dispositivo non è stato attivato il backup automatico delle preferenze." - backupFound: "Esiste il Backup delle preferenze" -_accountSettings: - requireSigninToViewContents: "Per vedere il contenuto, è necessaria l'iscrizione" - requireSigninToViewContentsDescription1: "Richiedere l'iscrizione per visualizzare tutte le Note e gli altri contenuti che hai creato. Probabilmente l'effetto è impedire la raccolta di informazioni da parte dei bot crawler." - requireSigninToViewContentsDescription2: "La visualizzazione verrà disabilitata a server che non supportano l'anteprima URL (OGP), all'incorporamento nelle pagine Web e alla citazione delle Note." - requireSigninToViewContentsDescription3: "Queste restrizioni potrebbero non applicarsi al contenuto federato su server remoti." - makeNotesFollowersOnlyBefore: "Rendi visibili solo ai Follower le Note pubblicate in precedenza" - makeNotesFollowersOnlyBeforeDescription: "Mentre questa funzione è abilitata, le Note antecedenti al momento impostato, saranno visibili solo ai profili Follower. Disabilitandola nuovamente, verrà ripristinata anche la visibilità pubblica della Nota." - makeNotesHiddenBefore: "Nascondi le Note pubblicate in precedenza" - makeNotesHiddenBeforeDescription: "Mentre questa funzione è abilitata, le Note antecedenti al momento impostato, saranno visibili soltanto a te (private). Disabilitandola nuovamente, verrà ripristinata anche la visibilità pubblica della Nota." - mayNotEffectForFederatedNotes: "Le Note già federate su server remoti potrebbero non essere modificate." - mayNotEffectSomeSituations: "Queste restrizioni sono semplificate. In alcuni casi, potrebbero anche non avvenire. Ad esempio visionando un server remoto o durante la moderazione." - notesHavePassedSpecifiedPeriod: "Note antecedenti al periodo specificato" - notesOlderThanSpecifiedDateAndTime: "Note antecedenti al momento specificato" -_abuseUserReport: - forward: "Inoltra" - forwardDescription: "Inoltra il report al server remoto, per mezzo di account di sistema, anonimo." - resolve: "Risolvi" - accept: "Approva" - reject: "Rifiuta" - resolveTutorial: "Se moderi una segnalazione legittima, scegli \"Approva\" per risolvere positivamente.\nSe la segnalazione non è legittima, seleziona \"Rifiuta\" per risolvere negativamente." -_delivery: - status: "Stato della consegna" - stop: "Sospensione" - resume: "Riprendi la consegna" - _type: - none: "Pubblicazione" - manuallySuspended: "Sospesa manualmente" - goneSuspended: "Sospensione server a causa dell'eliminazione" - autoSuspendedForNotResponding: "Sospensione del server a causa di mancata risposta" - softwareSuspended: "Attualmente non disponibile perché il software non è più distribuito" -_bubbleGame: - howToPlay: "Come giocare" - hold: "Tieni" - _score: - score: "Punteggio" - scoreYen: "Capitale" - highScore: "Punteggio migliore" - maxChain: "Miglior combo" - yen: "{yen}¥" - estimatedQty: "{qty} punti" - scoreSweets: "Onigiri {onigiriQtyWithUnit}" - _howToPlay: - section1: "Scegli la posizione e rilascia l'oggetto nel contenitore." - section2: "Se due oggetti dello stesso tipo si toccano, si trasformano in un oggetto diverso, aumentando il punteggio." - section3: "Se gli oggetti escono dal limite superiore del contenitore, il gioco finisce. Cerca di ottenere un punteggio elevato fondendo gli oggetti, evitando che escano dal contenitore!" -_announcement: - forExistingUsers: "Solo ai profili attuali" - forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." - needConfirmationToRead: "Conferma di lettura obbligatoria" - needConfirmationToReadDescription: "I profili riceveranno una finestra di dialogo che richiede di accettare obbligatoriamente per procedere. Tale richiesta è esente da \"conferma tutte\"." - end: "Archivia l'annuncio" - tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi." - readConfirmTitle: "Segnare come già letto?" - readConfirmText: "Hai già letto \"{title}˝?" - shouldNotBeUsedToPresentPermanentInfo: "Ti consigliamo di utilizzare gli annunci per pubblicare informazioni tempestive e limitate nel tempo, anziché informazioni importanti a lungo andare nel tempo, poiché potrebbero risultare difficili da ritrovare e peggiorare la fruibilità del servizio, specialmente alle nuove persone iscritte." - dialogAnnouncementUxWarn: "Ti consigliamo di usarli con cautela, poiché è molto probabile che avere più di un annuncio in stile \"finestra di dialogo\" peggiori sensibilmente la fruibilità del servizio, specialmente alle nuove persone iscritte." - silence: "Annuncio silenzioso" - silenceDescription: "Attivando questa opzione, non invierai la notifica, evitando che debba essere contrassegnata come già letta." _initialAccountSetting: accountCreated: "Il tuo profilo è stato creato!" letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo." @@ -1536,119 +1076,16 @@ _initialAccountSetting: pushNotificationDescription: "Attivare le notifiche push ti permettera di ricevere informazioni sulla attività di {name} direttamente sul tuo dispositivo." initialAccountSettingCompleted: "Hai completato la configurazione iniziale!" haveFun: "Divertiti con {name}!" - youCanContinueTutorial: "Puoi continuare con l'esercitazione su come usare {name} (Misskey), oppure interrompere, iniziando subito a usarlo." - startTutorial: "Avvia l'esercitazione" + ifYouNeedLearnMore: "Per saperne di più su come usare {name} (Misskey), visita la pagina {link}" skipAreYouSure: "Vuoi davvero saltare la configurazione iniziale?" laterAreYouSure: "Vuoi davvero rimandare la configurazione iniziale?" -_initialTutorial: - launchTutorial: "Inizia il tutorial" - title: "Tutorial" - wellDone: "Ottimo lavoro!" - skipAreYouSure: "Vuoi davvero interrompere il tutorial?" - _landing: - title: "Eccoci nel tutorial" - description: "Qui puoi verificare l'uso delle funzionalità base di Misskey." - _note: - title: "Cosa sono le Note?" - description: "Gli status su Misskey sono chiamati \"Note\". Le Note sono elencate in ordine cronologico nelle timeline e vengono aggiornate in tempo reale." - reply: "Puoi rispondere alle Note, alle altre risposte e dialogare in conversazioni." - renote: "Puoi ri-condividere le Note, ritorneranno sulla Timeline. Aggiungendo del testo, scriverai una Citazione." - reaction: "Puoi aggiungere una reazione. Nella pagina successiva ti spiego come." - menu: "Per altre attività, ad esempio, vedere i dettagli delle Note o copiare i collegamenti." - _reaction: - title: "Cosa sono le Reazioni?" - description: "Reazioni alle Note. Le sensazioni che non si possono descrivere con \"Mi piace\" si esprimono facilmente con le reazioni." - letsTryReacting: "Puoi aggiungere una Reazione cliccando il bottone \"+\" (più) della relativa Nota. Prova ad aggiungerne una a questa Nota di esempio!" - reactToContinue: "Aggiungere la Reazione ti consentirà di procedere col tutorial." - reactNotification: "Quando qualcuno reagisce alle tue Note, ricevi una notifica in tempo reale." - reactDone: "Annulla la tua Reazione premendo il bottone \"ー\" (meno)" - _timeline: - title: "Come funziona la Timeline" - description1: "Misskey fornisce alcune Timeline (sequenze cronologiche di Note). Una di queste potrebbe essere stata disattivata dagli amministratori." - home: "le Note provenienti dai profili che segui (Following)." - local: "tutte le Note pubblicate dai profili di questa istanza." - social: "sia le Note della Timeline Home che quelle della Timeline Locale, insieme!" - global: "le Note da pubblicate da tutte le altre istanze federate con la nostra." - description2: "Nella parte superiore dello schermo, puoi scegliere una Timeline o l'altra in qualsiasi momento." - description3: "Ci sono anche sequenze temporali di elenchi, sequenze temporali di canali, ecc. Per ulteriori dettagli, consultare la {link}.\nPuoi vedere anche Timeline delle liste di profili (se ne hai create), canali, ecc... Per i dettagli, c'è la {link}." - _postNote: - title: "La Nota e le sue impostazioni" - description1: "Quando scrivi una Nota su Misskey, hai a disposizione varie opzioni. Il modulo di invio è simile a questo." - _visibility: - description: "Puoi limitare chi può vedere la tua Nota." - public: "Visibile a tutti." - home: "Pubblicato solo sulla Timeline Home (personale). Visibile anche da profili remoti follower, visitatori del tuo profilo e tramite i Rinota dei follower." - followers: "Visibile solo ai profili tuoi follower (locali o remoti). Nessun altro oltre a te può \"Rinotare\"." - direct: "Visibile solo ai profili specificati, i quali riceveranno una notifica. Puoi usarlo come se fossero messaggi diretti." - doNotSendConfidencialOnDirect1: "Attenzione, quando si inviano informazioni confidenziali." - doNotSendConfidencialOnDirect2: "Poiché le Note non sono crittografate, l'amministratore del server di destinazione potrebbe leggere cosa è stato scritto, quindi se spedisci una Nota diretta a un profilo che risiede su un server non attendibile, evita di scrivere informazioni riservate." - localOnly: "Indipendentemente dalla visualizzazione sopra indicata, i profili su altri server non saranno in grado di visualizzare la Nota, se questa impostazione è attivata. Non non verrà comunicata ad altri server." - _cw: - title: "Nascondere il contenuto esplicito" - description: "Verrà visualizzato il testo scritto nel campo \"Annotazione preventiva\" al posto del testo principale della Nota. Premere il bottone \"Continua la lettura\" se si intende davvero leggere il testo." - _exampleNote: - cw: "Attenzione: contiene zuccheri" - note: "Ho appena mangiato una ciambella ricoperta di cioccolato 🍩😋" - useCases: "Utilizzalo per chiarire il contenuto della Nota, prima che sia letta. Come richiesto dal regolamento del server o per autoregolamentare spoiler e testi troppo espliciti." - _howToMakeAttachmentsSensitive: - title: "Come indicare che gli allegati sono espliciti?" - description: "Si fa quando è richiesto dal regolamento del server o quando non devono essere visibili immediatamente." - tryThisFile: "Prova a rendere esplicite le immagini allegate a questo modulo!" - _exampleNote: - note: "AAA! Ho rotto il coperchio del natto... (fagioli di soia fermentati)" - method: "Tocca il file, si aprirà il menu, scegli la voce \"Segna come esplicito\"" - sensitiveSucceeded: "Quando alleghi file, assicurati di indicare se è materiale esplicito in modo appropriato, decidi in base al regolamento dell'istanza." - doItToContinue: "Imposta l'immagine come esplicita per procedere col tutorial." - _done: - title: "Il tutorial è finito! 🎉" - description: "Queste sono solamente alcune delle funzionalità principali di Misskey. Per ulteriori informazioni, {link}." -_timelineDescription: - home: "Nella Timeline Home, la tua cronologia principale, puoi vedere le Note provenienti dai profili che segui (Following)." - local: "La Timeline Locale è un flusso di Note pubblicate dai profili iscritti a questo server." - social: "La Timeline Sociale elenca, in ordine cronologico, il flusso di Note nella Timeline Home e Locale." - global: "Nella Timeline Federata trovi il flusso di Note provenienti da profili iscritti ad altri server, federati a questo." _serverRules: description: "In Europa è necessario mostrare l'informativa sul trattamento dei dati personali, prima della registrazione al servizio." -_serverSettings: - iconUrl: "URL dell'icona" - appIconDescription: "Indicare l'icona da usare quando {host} viene salvata come App." - appIconUsageExample: "Ad esempio quando si aggiunge il segnalibro alla PWA (Progressive Web App), oppure alla schermata iniziale del dispositivo mobile " - appIconStyleRecommendation: "Poiché l'icona potrebbe essere ritagliata in un quadrato o in un cerchio, si raccomanda che abbia un margine colorato." - appIconResolutionMustBe: "La risoluzione minima è {resolution}" - manifestJsonOverride: "Sostituire il file manifest.json" - shortName: "Abbreviazione" - shortNameDescription: "Un'abbreviazione o un nome comune che può essere visualizzato al posto del nome ufficiale lungo del server." - fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare." - fanoutTimelineDbFallback: "Elaborazione dati alternativa" - fanoutTimelineDbFallbackDescription: "Attivando l'elaborazione alternativa, verrà interrogato ulteriormente il database se la timeline non è nella cache. \nDisattivando, si può ridurre ulteriormente il carico del server, evitando l'elaborazione alternativa, ma limitando l'intervallo recuperabile delle timeline." - reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis." - inquiryUrl: "URL di contatto" - inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione." - openRegistration: "Registrazioni aperte" - openRegistrationWarning: "L’apertura della registrazione comporta dei rischi. Ti consigliamo di attivarla solo se hai predisposto il monitoraggio continuo del tuo server e puoi rispondere immediatamente se si verifica un problema." - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo." - deliverSuspendedSoftware: "Software fuori produzione" - deliverSuspendedSoftwareDescription: "A causa di vulnerabilità o altri motivi, puoi interrompere la distribuzione di un software da un server specificandone il nome e la versione. Le informazioni sono fornite dall'altro server e l'autenticità non è garantita. Puoi indicare un intervallo di versione semantica, ma specificando >= 2024.3.1 non verranno incluse le versioni personalizzate come ad esempio 2024.3.1-custom.0, pertanto ti consigliamo di specificare una versione come >= 2024.3.1-0." - singleUserMode: "Modalità utente singolo" - singleUserMode_description: "Se sei l'unica persona a utilizzare questo server, l'abilitazione di questa modalità ottimizzerà le prestazioni." - signToActivityPubGet: "Firma delle richieste GET" - signToActivityPubGet_description: "Normalmente questa opzione dovrebbe essere abilitata. Se si verificano problemi con la comunicazione federata, disabilitarla potrebbe migliorare la situazione, ma d'altro canto potrebbe rendere impossibile la comunicazione, a seconda del server." - proxyRemoteFiles: "Proxy di file remoti" - proxyRemoteFiles_description: "Se abilitato, i file remoti verranno serviti tramite proxy. Utile per generare miniature delle immagini e proteggere la privacy degli utenti." - allowExternalApRedirect: "Consenti reindirizzamenti per le query tramite ActivityPub" - allowExternalApRedirect_description: "Se abilitata, consente ad altri server di interrogare contenuti di terze parti tramite il tuo server, con conseguente potenziale falsificazione dei contenuti." - userGeneratedContentsVisibilityForVisitor: "Visibilità dei contenuti generati dagli utenti ai non utenti" - userGeneratedContentsVisibilityForVisitor_description: "Questa funzionalità è utile per impedire che contenuti remoti inappropriati e difficili da moderare vengano inavvertitamente resi pubblici su Internet tramite il proprio server." - userGeneratedContentsVisibilityForVisitor_description2: "Esistono dei rischi nell'esporre incondizionatamente su internet tutto il contenuto del tuo server, incluso il contenuto remoto ricevuto da altri server. In particolare, occorre prestare attenzione, perché le persone non consapevoli della federazione potrebbero erroneamente credere che il contenuto remoto sia stato invece creato all'interno del proprio server." - _userGeneratedContentsVisibilityForVisitor: - all: "Tutto pubblico" - localOnly: "Pubblica solo contenuti locali, mantieni privati ​​i contenuti remoti" - none: "Tutto privato" _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" moveFromSub: "Crea un alias verso un altro profilo remoto" - moveFromLabel: "Profilo da cui migrare n. {n}" - moveFromDescription: "Se desideri spostare i Follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@vecchia.istanza.it" + moveFromLabel: "Profilo da cui migrare:" + moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it" moveTo: "Migrare questo profilo verso un un altro" moveToLabel: "Profilo verso cui migrare" moveCannotBeUndone: "La migrazione è irreversibile, non può essere interrotta o annullata." @@ -1657,7 +1094,7 @@ _accountMigration: startMigration: "Avvia la migrazione" migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo." movedAndCannotBeUndone: "Il tuo profilo è stato migrato.\nLa migrazione non può essere annullata." - postMigrationNote: "Questo profilo smetterà di seguire gli altri profili remoti a 24 ore dal termine della migrazione.\nSia i Following che i Follower scenderanno a zero. I tuoi Follower saranno comunque in grado di vedere le Note per soli Follower, poiché non smetteranno di seguirti." + postMigrationNote: "Questo profilo smetterà di seguire gli altri profili remoti a 24 ore dal termine della migrazione.\nSia i Follow che i Follower scenderanno a zero. I tuoi follower saranno comunque in grado di vedere le Note per soli follower, poiché non smetteranno di seguirti." movedTo: "Profilo verso cui migrare" _achievements: earnedAt: "Data di conseguimento" @@ -1724,13 +1161,13 @@ _achievements: title: "Principiante III" description: "Hai totalizzato 15 accessi!" _login30: - title: "Missalcolista I" + title: "Misskist I" description: "Hai totalizzato 30 accessi!" _login60: - title: "Missalcolista II" + title: "Misskeist II" description: "Hai totalizzato 60 accessi!" _login100: - title: "Missalcolista III" + title: "Misskeist III" description: "Hai totalizzato 100 accessi!" flavor: "Violent Misskeist" _login200: @@ -1816,10 +1253,10 @@ _achievements: description: "Hai superato i 1.000 profili Follower" _collectAchievements30: title: "Collezionista di successi" - description: "Hai raggiunto 30 conquiste" + description: "Hai raggiunto 30 obiettivi" _viewAchievements3min: title: "Mi piacciono i risultati" - description: "Ammira la tua collezione di conquiste per almeno 3 minuti" + description: "Guarda la tua collezione di obiettivi per almeno 3 minuti" _iLoveMisskey: title: "I LOVE Misskey" description: "Pubblica «I ♥ #Misskey»" @@ -1898,19 +1335,6 @@ _achievements: title: "Brain Diver" description: "Pubblica un link a Brain Diver" flavor: "Sulle note di Brain Diver" - _smashTestNotificationButton: - title: "Prove eccessive" - description: "Hai provato le notifiche consecutivamente in un periodo di tempo molto breve" - _tutorialCompleted: - title: "Attestato di partecipazione al corso per principianti di Misskey" - description: "Ha completato il tutorial" - _bubbleGameExplodingHead: - title: "🤯" - description: "Estrai l'oggetto più grande dal Bubble Game" - _bubbleGameDoubleExplodingHead: - title: "Doppio 🤯" - description: "Due oggetti più grossi contemporaneamente nel Bubble Game" - flavor: "Ha le dimensioni di una bento-box 🤯 🤯" _role: new: "Nuovo ruolo" edit: "Modifica ruolo" @@ -1921,9 +1345,7 @@ _role: assignTarget: "Modalità di assegnazione del ruolo" descriptionOfAssignTarget: "Manuale: per assegnare manualmente questo ruolo ai profili.\nCondizionale: per assegnare o rimuovere automaticamente questo ruolo ai profili, a precise condizioni." manual: "Manuale" - manualRoles: "Ruoli assegnati manualmente" conditional: "Condizionale" - conditionalRoles: "Ruoli condizionati" condition: "Condizioni" isConditionalRole: "Questo è un ruolo condizionato" isPublic: "Ruolo pubblico" @@ -1940,8 +1362,6 @@ _role: descriptionOfIsExplorable: "Selezionandolo, la timeline del ruolo diventerà accessibile pubblicamente. Tranne se il ruolo non è pubblico." displayOrder: "Ordine di visualizzazione" descriptionOfDisplayOrder: "I valori più alti vengono visualizzati per primi" - preserveAssignmentOnMoveAccount: "Mantenere l'assegnazione alla migrazione del profilo" - preserveAssignmentOnMoveAccount_description: "Attivando, il ruolo verrà portato sul profilo destinatario, durante la migrazione." canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo" descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori." priority: "Priorità" @@ -1952,18 +1372,11 @@ _role: _options: gtlAvailable: "Disponibilità della Timeline Federata" ltlAvailable: "Disponibilità della Timeline Locale" - canPublicNote: "Scrivere Note con Visibilità Pubblica" - mentionMax: "Numero massimo di menzioni in una nota" - canInvite: "Generare codici di invito all'istanza" - inviteLimit: "Limite di codici invito" - inviteLimitCycle: "Intervallo di emissione del codice di invito" - inviteExpirationTime: "Scadenza del codice di invito" + canPublicNote: "Può scrivere Note con Visibilità Pubblica" + canInvite: "Genera codici di invito all'istanza" canManageCustomEmojis: "Gestire le emoji personalizzate" - canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo" driveCapacity: "Capienza del Drive" - maxFileSize: "Dimensione massima del file caricabile" - alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)" - canUpdateBioMedia: "Può aggiornare foto profilo e di testata" + alwaysMarkNsfw: "Imposta sempre come NSFW" pinMax: "Quantità massima di Note in primo piano" antennaMax: "Quantità massima di Antenne" wordMuteMax: "Lunghezza massima del filtro parole" @@ -1974,32 +1387,15 @@ _role: userEachUserListsMax: "Quantità massima di profili per lista" rateLimitFactor: "Limite del rapporto" descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più." - canHideAds: "Nascondere i banner" + canHideAds: "Può nascondere i banner" canSearchNotes: "Ricercare nelle Note" - canUseTranslator: "Tradurre le Note" - avatarDecorationLimit: "Numero massimo di decorazioni foto profilo installabili" - canImportAntennas: "Può importare Antenne" - canImportBlocking: "Può importare Blocchi" - canImportFollowing: "Può importare Following" - canImportMuting: "Può importare Silenziati" - canImportUserLists: "Può importare liste di Profili" - chatAvailability: "Chat consentita" - uploadableFileTypes: "Tipi di file caricabili" - uploadableFileTypes_caption: "Specifica il tipo MIME. Puoi specificare più valori separandoli andando a capo, oppure indicare caratteri jolly con un asterisco (*). Ad esempio: image/*" - uploadableFileTypes_caption2: "A seconda del file, il tipo potrebbe non essere determinato. Se si desidera consentire tali file, aggiungere {x} alla specifica." _condition: - roleAssignedTo: "Assegnato a ruoli manualmente" isLocal: "Profilo locale" isRemote: "Profilo remoto" - isCat: "È un gattino" - isBot: "È un bot" - isSuspended: "È sospeso" - isLocked: "È in stato privato" - isExplorable: "Autorizza la pubblicazione nei cataloghi" - createdLessThan: "Profilo creato da meno di N" - createdMoreThan: "Profilo creato da più di N" - followersLessThanOrEq: "Profilo con N follower o meno" - followersMoreThanOrEq: "Profilo con N follower o più" + createdLessThan: "Creato meno di" + createdMoreThan: "Creato più di" + followersLessThanOrEq: "Ha meno di N follower" + followersMoreThanOrEq: "Ha più di N follower" followingLessThanOrEq: "Segue N profili o meno" followingMoreThanOrEq: "Segue N profili o più" notesLessThanOrEq: "Conteggio Note inferiore o uguale a" @@ -2008,9 +1404,9 @@ _role: or: "O" not: "NON" _sensitiveMediaDetection: - description: "Utilizzare l'apprendimento automatico (machine learning) per riconoscere media espliciti e sottoporli alla moderazione. Aumenterà lievemente il carico del server." - sensitivity: "Sensibilità del rilevamento" - sensitivityDescription: "Abbassando la sensibilità si riducono i falsi positivi (rilevazioni errate). Aumentando la sensibilità si riduce il numero di rilevazioni mancate. (rilevazioni ignorate)." + description: "L'apprendimento automatico può essere utilizzato per individuare automaticamente i media sensibili da moderare. Il carico del server aumenta leggermente." + sensitivity: "Sensibilità di rilevamento" + sensitivityDescription: "Una minore sensibilità riduce i falsi positivi (false positività). Una maggiore sensibilità riduce le omissioni (falsi negativi)." setSensitiveFlagAutomatically: "Impostare il flag NSFW." setSensitiveFlagAutomaticallyDescription: "Anche se questa impostazione è disattivata, il risultato della decisione viene conservato internamente." analyzeVideos: "Abilitazione dell'analisi video." @@ -2018,12 +1414,11 @@ _sensitiveMediaDetection: _emailUnavailable: used: "Email già in uso" format: "Formato email non valido" - disposable: "Indirizzo email non utilizzabile" + disposable: "Email non riutilizzabile" mx: "Server email non corretto" smtp: "Il server email non risponde" - banned: "Non puoi registrarti con questo indirizzo email" _ffVisibility: - public: "Pubblica" + public: "Pubblico" followers: "Mostra solo ai follower" private: "Invisibile" _signup: @@ -2041,11 +1436,6 @@ _ad: back: "Indietro" reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso" hide: "Nascondi" - timezoneinfo: "Il giorno della settimana è determinato in base al fuso orario del server." - adsSettings: "Impostazioni banner" - notesPerOneAd: "Quantità di Note tra i banner" - setZeroToDisable: "Imposta 0 (zero) per disattivare la distribuzione dei banner durante gli aggiornamenti in tempo reale" - adsTooClose: "Attenzione, l'intervallo di pubblicazione dei banner è molto breve, potrebbe infastidire significativamente la fruizione" _forgotPassword: enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo." ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza." @@ -2057,21 +1447,19 @@ _gallery: unlike: "Non mi piace più" _email: _follow: - title: "Follower aggiuntivo" + title: "Ha iniziato a seguirti" _receiveFollowRequest: title: "Hai ricevuto una richiesta di follow" _plugin: install: "Installa estensioni" installWarn: "Si prega di installare soltanto estensioni che provengono da fonti affidabili." manage: "Gestisci estensioni" - viewSource: "Visualizza sorgente" - viewLog: "Mostra log" _preferencesBackups: - list: "Elenco di impostazioni salvate in precedenza" + list: "I backup creati." saveNew: "Nuovo salvataggio" - loadFile: "Carica da file" - apply: "Applica a questo dispositivo" - save: "Sovrascrivi il backup" + loadFile: "Importa file" + apply: "Applicabile a questo dispositivo" + save: "Sovrascrivi il file di salvataggio" inputName: "Inserire il nome del backup." cannotSave: "Impossibile salvare." nameAlreadyExists: "Il nome del backup \"{name}\" esiste già. Si prega di specificare un nome diverso." @@ -2091,21 +1479,14 @@ _registry: domain: "Dominio" createKey: "Crea chiave" _aboutMisskey: - about: "Misskey è software libero, open source, sviluppato da Syuilo fin dal lontano 2014." + about: "Misskey è un software libero e open source, sviluppato da syuilo dal 2014." contributors: "Principali sostenitori" allContributors: "Tutti i sostenitori" source: "Codice sorgente" - original: "Originale" - thisIsModifiedVersion: "{name} sta usando una versione modificata diversa da Misskey originale." translation: "Tradurre Misskey" donate: "Sostieni Misskey" morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰" patrons: "Sostenitori" - projectMembers: "Partecipanti al progetto" -_displayOfSensitiveMedia: - respect: "Nascondere i media espliciti" - ignore: "Non nascondere i media espliciti" - force: "Nascondi tutti i media" _instanceTicker: none: "Nascondi" remote: "Mostra solo per i profili remoti" @@ -2116,17 +1497,16 @@ _serverDisconnectedBehavior: quiet: "Visualizza avviso in modo discreto" _channel: create: "Nuovo canale" - edit: "Modifica il canale" + edit: "Gerisci canale" setBanner: "Scegli intestazione" removeBanner: "Rimuovi intestazione" - featured: "Popolari nel canale" + featured: "Tendenze" owned: "I miei canali" - following: "Following" + following: "Seguiti" usersCount: "{n} partecipanti" notesCount: "{n} note" nameAndDescription: "Nome e descrizione" nameOnly: "Solo il nome" - allowRenoteToExternal: "Consenti i Rinota e le citazioni all'esterno del canale" _menuDisplay: sideFull: "Laterale" sideIcon: "Laterale (solo icone)" @@ -2134,23 +1514,27 @@ _menuDisplay: hide: "Nascondere" _wordMute: muteWords: "Parole da filtrare" - muteWordsDescription: "Sparando con uno spazio indichi la condizione E (and). Separando con un a capo, indichi la condizione O (or)." + muteWordsDescription: "Separare con uno spazio indica la condizione \"E\". Separare con una interruzione di riga, indica la condizione \"O\"" muteWordsDescription2: "Se vuoi indicare delle Espressioni Regolari (regexp), metti la condizione all'interno di due slash (/)" + softDescription: "Verranno nascoste da tutte le Timeline quelle Note che soddisfano le seguenti condizioni" + hardDescription: "Impedisci alla istanza di caricare Note che soddisfano le seguenti condizioni. Le Note già filtrate sono già scomparse in modo irreversibile, fino al cambiamento delle condizioni. Dopo di che scompariranno quelle che soddisfano le nuove condizioni." + soft: "Leggero" + hard: "Pesante" + mutedNotes: "Note filtrate" _instanceMute: instanceMuteDescription: "Disattiva tutte le note, le note di rinvio (condivisione) dell'istanza configurata, comprese le risposte agli utenti dell'istanza." instanceMuteDescription2: "Impostazione separata da una nuova riga" title: "Nasconde le note dell'istanza configurata." - heading: "Istanze da silenziare" + heading: "Istanze da silenziare." _theme: explore: "Esplora temi" install: "Installa un tema" - manage: "Gestione dei temi" + manage: "Gerisci temi" code: "Codice tema" description: "Descrizione" installed: "{name} è installato" installedThemes: "Temi installati" builtinThemes: "Temi integrati" - instanceTheme: "Tema dell'istanza" alreadyInstalled: "Questo tema è già installato" invalid: "Il formato tema non è valido" make: "Crea un tema" @@ -2183,13 +1567,14 @@ _theme: header: "Intestazione" navBg: "Sfondo della barra laterale" navFg: "Testo della barra laterale" + navHoverFg: "Testo della barra laterale (al passaggio del mouse)" navActive: "Testo della barra laterale (attivo)" navIndicator: "Indicatore di barra laterale" link: "Link" hashtag: "Hashtag" mention: "Menzioni" mentionMe: "Menzioni (di me)" - renote: "Renota" + renote: "Rinota" modalBg: "Sfondo modale." divider: "Interruzione di linea" scrollbarHandle: "Maniglie della barra di scorrimento" @@ -2199,28 +1584,30 @@ _theme: infoFg: "Testo di informazioni" infoWarnBg: "Sfondo degli avvisi" infoWarnFg: "Testo di avviso" + cwBg: "Sfondo del CW" + cwFg: "Testo del pulsante CW" + cwHoverBg: "Sfondo del pulsante CW (sorvolato)" toastBg: "Sfondo di notifica a comparsa" toastFg: "Testo di notifica a comparsa" buttonBg: "Sfondo del pulsante" buttonHoverBg: "Sfondo del pulsante (sorvolato)" inputBorder: "Inquadra casella di testo" + listItemHoverBg: "Sfondo della voce di elenco (sorvolato)" + driveFolderBg: "Sfondo della cartella di disco" + wallpaperOverlay: "Sovrapposizione dello sfondo" badge: "Distintivo" messageBg: "Sfondo della chat" + accentDarken: "Temi (scuri)" + accentLighten: "Temi (luminosi)" fgHighlighted: "Testo in evidenza." _sfx: note: "Nota" noteMy: "Mia nota" notification: "Notifiche" - reaction: "Quando seleziono una reazione" - chatMessage: "Messaggio di chat" -_soundSettings: - driveFile: "Suoni del Drive" - driveFileWarn: "Seleziona file dal dispositivo" - driveFileTypeWarn: "Formato file non supportato" - driveFileTypeWarnDescription: "Per favore, scegli un file di tipo audio" - driveFileDurationWarn: "La durata dell'audio è troppo lunga" - driveFileDurationWarnDescription: "Scegliere un audio lungo potrebbe interferire con l'uso di Misskey. Vuoi continuare lo stesso?" - driveFileError: "Impossibile caricare l'audio. Si prega di modificare le impostazioni" + chat: "Messaggi" + chatBg: "Chat (sfondo)" + antenna: "Ricezione dell'antenna" + channel: "Notifiche di canale" _ago: future: "Futuro" justNow: "Adesso" @@ -2232,32 +1619,36 @@ _ago: monthsAgo: "{n} mesi fa" yearsAgo: "{n} anni fa" invalid: "Niente da visualizzare" -_timeIn: - seconds: "Dopo {n} secondi" - minutes: "Dopo {n} minuti" - hours: "Dopo {n} ore" - days: "Dopo {n} giorni" - weeks: "Dopo {n} settimane" - months: "Dopo {n} mesi" - years: "Dopo {n} anni" _time: second: "s" minute: "min" hour: "ore" day: "giorni" +_timelineTutorial: + title: "Come usare Misskey" + step1_1: "Questa è la \"Timeline\". tutte le \"Note\" pubblicate su {name} vengono elencate in ordine cronologico." + step1_2: "Le Timeline sono diverse, ad esempio, la \"Home\" elenca le Note dei profili che segui. Quella \"Locale\" elenca quelle di tutti i profili attivi su {name}." + step2_1: "Prova a pubblicare una Nota. Semplicemente premendo il bottone con l'icona di una matita." + step2_2: "Potresti scrivere la tua presentazione, oppure semplicemente \"Ciao da {name}!\"" + step3_1: "Hai pubblicato qualcosa?" + step3_2: "In tal caso, dovrebbe comparire subito nella tua \"Home\"" + step4_1: "Puoi reagire con un emoji alle Note." + step4_2: "To attach a reaction, press the \"+\" mark on a note and choose an emoji you'd like to react with.\nPer reagire con una emoji, premi il bottone \"+\" (più) visibile vicino ad ogni Nota e scegli dall'elenco la emoji che rappresenta la tua reazione." _2fa: alreadyRegistered: "La configurazione è stata già completata." - registerTOTP: "Registra una App di autenticazione a due fattori (2FA/MFA)" - step1: "Innanzitutto, installa sul dispositivo un'App di autenticazione come {a} o {b}." - step2: "Quindi, tramite la App installata, scansiona questo codice QR." - step2Uri: "Inserisci il seguente URL se desideri utilizzare una App per PC" + registerTOTP: "Registra un'app di autenticazione" + passwordToTOTP: "Inserire la password" + step1: "Innanzitutto, installare sul dispositivo un'applicazione di autenticazione come {a} o {b}." + step2: "Quindi, scansionare il codice QR visualizzato con l'app." + step2Click: "Cliccando sul codice QR, puoi registrarlo con l'app di autenticazione o il portachiavi installato sul tuo dispositivo." + step2Url: "Nell'applicazione desktop inserire il seguente URL: " step3Title: "Inserisci il codice di verifica" step3: "Inserite il token visualizzato nell'app e il gioco è fatto." - setupCompleted: "Impostazione completata! 🎉" step4: "D'ora in poi, quando si accede, si inserisce il token nello stesso modo." securityKeyNotSupported: "Il tuo browser non supporta le chiavi di sicurezza." registerTOTPBeforeKey: "Ti occorre un'app di autenticazione con OTP, prima di registrare la chiave di sicurezza." securityKeyInfo: "È possibile impostare il dispositivo per accedere utilizzando una chiave di sicurezza hardware che supporta FIDO2 o un'impronta digitale o un PIN sul dispositivo." + chromePasskeyNotSupported: "Le passkey di Chrome non sono attualmente supportate." registerSecurityKey: "Registra la chiave di sicurezza" securityKeyName: "Inserisci il nome della chiave" tapSecurityKey: "Segui le istruzioni del browser e registra la chiave di sicurezza." @@ -2268,12 +1659,6 @@ _2fa: renewTOTPConfirm: "I codici di verifica nelle app di autenticazione esistenti smetteranno di funzionare" renewTOTPOk: "Ripristina" renewTOTPCancel: "No grazie" - checkBackupCodesBeforeCloseThisWizard: "Prima di chiudere questa procedura guidata, salva i tuoi codici usa-e-getta in un posto sicuro." - backupCodes: "Codici usa-e-getta" - backupCodesDescription: "Puoi usare questi codici usa-e-getta per ottenere l'accesso al tuo profilo in caso sia impossibile usare l'App col codice OTP. Salvali in un posto sicuro." - backupCodeUsedWarning: "È stato usato un codice usa-e-getta. Per favore, riconfigura l'autenticazione a due fattori il prima possibile, nel caso la configurazione precedente abbia smesso di funzionare." - backupCodesExhaustedWarning: "Hai esaurito i codici usa-e-getta. Se l'App che genera il codice OTP non è più disponibile, non potrai più accedere al tuo profilo. Ripeti la configurazione per l'autenticazione a due fattori." - moreDetailedGuideHere: "Informazioni dettagliate sull'autenticazione multi fattore (2FA/MFA)" _permissions: "read:account": "Visualizza le informazioni sul profilo" "write:account": "Modifica le informazioni sul profilo" @@ -2284,83 +1669,29 @@ _permissions: "read:favorites": "Visualizza i tuoi preferiti" "write:favorites": "Gestisci i tuoi preferiti" "read:following": "Vedi le informazioni di follow" - "write:following": "Aggiungere e togliere Following" + "write:following": "Seguire / Non seguire altri profili" "read:messaging": "Visualizzare la chat" "write:messaging": "Gestire la chat" "read:mutes": "Vedi i profili silenziati" - "write:mutes": "Gestione dei profili silenziati" + "write:mutes": "Gestisci i profili silenziati" "write:notes": "Creare / Eliminare note" - "read:notifications": "Visualizzare notifiche" - "write:notifications": "Gestione delle notifiche" + "read:notifications": "Visualizza notifiche" + "write:notifications": "Gerisci notifiche" "read:reactions": "Vedi reazioni" - "write:reactions": "Gestione delle reazioni" + "write:reactions": "Gerisci reazioni" "write:votes": "Votare" "read:pages": "Visualizzare pagine" "write:pages": "Gestire pagine" "read:page-likes": "Visualizzare i \"Mi piace\" di pagine" "write:page-likes": "Gestire i \"Mi piace\" di pagine" - "read:user-groups": "Vedere i gruppi di utenti" - "write:user-groups": "Gestire i gruppi di utenti" + "read:user-groups": "Vedi gruppi di utenti" + "write:user-groups": "Gestisci gruppi di utenti" "read:channels": "Visualizza canali" - "write:channels": "Gestione dei canali" + "write:channels": "Gerisci canali" "read:gallery": "Visualizza la galleria." "write:gallery": "Gestione della galleria" "read:gallery-likes": "Visualizza i contenuti della galleria." "write:gallery-likes": "Manipolazione dei \"Mi piace\" della galleria." - "read:flash": "Visualizza Play" - "write:flash": "Modifica Play" - "read:flash-likes": "Visualizza lista di Play piaciuti" - "write:flash-likes": "Modifica lista di Play piaciuti" - "read:admin:abuse-user-reports": "Mostra i report dai profili utente" - "write:admin:delete-account": "Elimina l'account utente" - "write:admin:delete-all-files-of-a-user": "Elimina i file dell'account utente" - "read:admin:index-stats": "Visualizza informazioni sugli indici del database" - "read:admin:table-stats": "Visualizza informazioni sulle tabelle del database" - "read:admin:user-ips": "Visualizza indirizzi IP degli account" - "read:admin:meta": "Visualizza i metadati dell'istanza" - "write:admin:reset-password": "Ripristina la password dell'account utente" - "write:admin:resolve-abuse-user-report": "Risolvere le segnalazioni dagli account utente" - "write:admin:send-email": "Spedire email" - "read:admin:server-info": "Vedere le informazioni sul server" - "read:admin:show-moderation-log": "Vedere lo storico di moderazione" - "read:admin:show-user": "Vedere le informazioni private degli account utente" - "write:admin:suspend-user": "Sospendere i profili" - "write:admin:unset-user-avatar": "Rimuovere la foto profilo dai profili" - "write:admin:unset-user-banner": "Rimuovere l'immagine testata dai profili" - "write:admin:unsuspend-user": "Togliere la sospensione ai profili" - "write:admin:meta": "Modificare i metadati dell'istanza" - "write:admin:user-note": "Scrivere annotazioni di moderazione" - "write:admin:roles": "Gestire i ruoli" - "read:admin:roles": "Vedere i ruoli" - "write:admin:relays": "Gestire i Relay" - "read:admin:relays": "Vedere i Relay" - "write:admin:invite-codes": "Gestire codici di invito" - "read:admin:invite-codes": "Vedere codici di invito" - "write:admin:announcements": "Gestire gli annunci" - "read:admin:announcements": "Leggere gli annunci" - "write:admin:avatar-decorations": "Gestire le decorazioni" - "read:admin:avatar-decorations": "Vedere le decorazioni" - "write:admin:federation": "Gestire la federazione" - "write:admin:account": "Vedere la federazione" - "read:admin:account": "Vedere le utenze" - "write:admin:emoji": "Gestire le emoji personalizzate" - "read:admin:emoji": "Vedere le emoji personalizzate" - "write:admin:queue": "Gestire la coda di attività" - "read:admin:queue": "Vedere la coda di attività" - "write:admin:promo": "Gestire le promozioni" - "write:admin:drive": "Gestire il Drive degli account" - "read:admin:drive": "Vedere il Drive degli account" - "read:admin:stream": "Usare le API Websocket" - "write:admin:ad": "Gestire i banner pubblicitari" - "read:admin:ad": "Vedere i banner pubblicitari" - "write:invite-codes": "Creare codici di invito" - "read:invite-codes": "Vedere i codici di invito" - "write:clip-favorite": "Impostare Clip preferite" - "read:clip-favorite": "Vedere Clip preferite" - "read:federation": "Vedere la federazione" - "write:report-abuse": "Inviare segnalazioni" - "write:chat": "Gestire la chat" - "read:chat": "Visualizzare le chat" _auth: shareAccessTitle: "Permessi dell'applicazione" shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?" @@ -2369,17 +1700,13 @@ _auth: permissionAsk: "Questa app richiede le seguenti autorizzazioni:" pleaseGoBack: "Si prega di ritornare sulla app" callback: "Ritornando sulla app" - accepted: "Accesso concesso" denied: "Accesso negato" - scopeUser: "Sto funzionando per il seguente profilo" pleaseLogin: "Per favore accedi al tuo account per cambiare i permessi dell'applicazione" - byClickingYouWillBeRedirectedToThisUrl: "Consentendo l'accesso, si verrà reindirizzati presso questo indirizzo URL" _antennaSources: all: "Tutte le note" - homeTimeline: "Note dai tuoi Following" + homeTimeline: "Note dagli utenti che segui" users: "Note dagli utenti selezionati" userList: "Note dagli utenti della lista selezionata" - userBlacklist: "Tutte le Note tranne quelle di uno o più profili specificati" _weekday: sunday: "Domenica" monday: "Lunedì" @@ -2395,20 +1722,20 @@ _widgets: notifications: "Notifiche" timeline: "Timeline" calendar: "Calendario" - trends: "Hashtag popolari" + trends: "Tendenze" clock: "Orologio" - rss: "Lettura RSS" - rssTicker: "Nastro RSS" + rss: "Aggregatore rss" + rssTicker: "Ticker RSS" activity: "Attività" photos: "Foto" digitalClock: "Orologio digitale" unixClock: "Orologio UNIX" federation: "Federazione" - instanceCloud: "Nuvola di federazione" + instanceCloud: "Istanza Cloud" postForm: "Finestra di pubblicazione" slideshow: "Diapositive" button: "Pulsante" - onlineUsers: "Persone attive adesso" + onlineUsers: "Utenti online" jobQueue: "Coda di lavoro" serverMetric: "Statistiche server" aiscript: "Console AiScript" @@ -2417,12 +1744,10 @@ _widgets: userList: "Elenco utenti" _userList: chooseList: "Seleziona una lista" - clicker: "Cliccheria" - birthdayFollowings: "Compleanni del giorno" - chat: "Chat" + clicker: "Cliccaggio" _cw: hide: "Nascondere" - show: "Continua la lettura..." + show: "Apri..." chars: "{count} caratteri" files: "{count} file" _poll: @@ -2449,14 +1774,14 @@ _poll: remainingSeconds: "Rimangono {s} secondi" _visibility: public: "Pubblica" - publicDescription: "Visibilità pubblica" + publicDescription: "Visibile per tutti sul Fediverso" home: "Home" - homeDescription: "Visibile solo nella Home" + homeDescription: "Visibile solo sulla timeline locale" followers: "Follower" followersDescription: "Visibile solo ai tuoi follower" specified: "Nota diretta" specifiedDescription: "Visibile solo ai profili menzionati" - disableFederation: "Senza federazione" + disableFederation: "Non federare" disableFederationDescription: "Non spedire attività alle altre istanze remote" _postForm: replyPlaceholder: "Rispondi a questa nota..." @@ -2481,22 +1806,15 @@ _profile: metadataContent: "Contenuto" changeAvatar: "Modifica immagine profilo" changeBanner: "Cambia intestazione" - verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo.\nPer verificare il profilo tramite la spunta di conferma, devi inserire la url alla pagina che contiene un link al tuo profilo Misskey. Deve avere attributo rel='me'." - avatarDecorationMax: "Puoi aggiungere fino a {max} decorazioni." - followedMessage: "Messaggio, quando qualcuno ti segue" - followedMessageDescription: "Puoi impostare un breve messaggio da mostrare agli altri profili quando ti seguono." - followedMessageDescriptionForLockedAccount: "Quando approvi una richiesta di follow, verrà visualizzato questo testo." _exportOrImport: allNotes: "Tutte le note" favoritedNotes: "Note preferite" - clips: "Clip" - followingList: "Following" + followingList: "Follow" muteList: "Elenco profili silenziati" blockingList: "Elenco profili bloccati" userLists: "Liste" excludeMutingUsers: "Escludere gli utenti silenziati" excludeInactiveUsers: "Escludere i profili inutilizzati" - withReplies: "Includere le risposte da profili importati nella Timeline" _charts: federation: "Federazione" apRequest: "Richieste" @@ -2513,7 +1831,7 @@ _charts: storageUsageTotal: "Utilizzo totale dell'immagazzinamento" _instanceCharts: requests: "Richieste" - users: "Variazione del numero di profili" + users: "Variazione del numero di utenti" usersTotal: "Totale cumulativo di utenti" notes: "Variazione del numero di note" notesTotal: "Totale cumulato di note" @@ -2543,11 +1861,13 @@ _play: title: "Titolo" script: "Script" summary: "Descrizione" - visibilityDescription: "Impostarlo su privato significa che non verrà visualizzato sul tuo profilo, ma chiunque ha l'URL potrà comunque accedervi." _pages: newPage: "Crea pagina" editPage: "Modifica pagina" readPage: "Visualizzando fonte " + created: "Pagina creata!" + updated: "Pagina aggiornata con successo!" + deleted: "Pagina eliminata" pageSetting: "Impostazioni pagina" nameAlreadyExists: "Esiste già una pagina con lo stesso URL." invalidNameTitle: "L'URL di pagina definito non è valido" @@ -2572,10 +1892,9 @@ _pages: font: "Tipo di carattere" fontSerif: "Serif" fontSansSerif: "Sans serif" - eyeCatchingImageSet: "Imposta un'immagine attraente" - eyeCatchingImageRemove: "Elimina immagine attraente" + eyeCatchingImageSet: "Imposta un'immagine attrattiva" + eyeCatchingImageRemove: "Elimina l'anteprima immagine" chooseBlock: "Aggiungi blocco" - enterSectionTitle: "Inserisci il titolo della sezione" selectType: "Seleziona tipo" contentBlocks: "Contenuto" inputBlocks: "Blocchi di input" @@ -2586,8 +1905,6 @@ _pages: section: "Sezione" image: "Immagini" button: "Pulsante" - dynamic: "Riquadri dinamici" - dynamicDescription: "Questo riquadro è obsoleto. Utilizza {play} da ora in poi." note: "Nota integrata" _note: id: "ID nota" @@ -2603,62 +1920,35 @@ _notification: youGotReply: "{name} ti ha risposto" youGotQuote: "{name} ha citato la tua Nota e ha detto" youRenoted: "{name} ha rinotato" - youWereFollowed: "Follower aggiuntivo" + youWereFollowed: "Ha iniziato a seguirti" youReceivedFollowRequest: "Hai ricevuto una richiesta di follow" yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata" pollEnded: "Risultati del sondaggio." - newNote: "Nuove Note" unreadAntennaNote: "Antenna {name}" - roleAssigned: "Ruolo assegnato" - chatRoomInvitationReceived: "Invito in una stanza di chat" emptyPushNotificationMessage: "Le notifiche push sono state aggiornate." achievementEarned: "Obiettivo raggiunto" - testNotification: "Provare la notifica" - checkNotificationBehavior: "Provare il comportamento della notifica" - sendTestNotification: "Spedisci una notifica di prova" - notificationWillBeDisplayedLikeThis: "La notifica apparirà così" - reactedBySomeUsers: "{n} reazioni" - likedBySomeUsers: "{n} apprezzamenti" - renotedBySomeUsers: "{n} Rinota" - followedBySomeUsers: "{n} follower" - flushNotification: "Azzera le notifiche" - exportOfXCompleted: "Abbiamo completato l'esportazione di {x}" - login: "Autenticazione avvenuta" - createToken: "È stato creato un token di accesso" - createTokenDescription: "In caso contrario, eliminare il token di accesso tramite ({text})." _types: all: "Tutto" - note: "Nuove Note" - follow: "Follower" + follow: "Novità follower" mention: "Menzioni" reply: "Risposte" renote: "Rinota" quote: "Cita" reaction: "Reazioni" pollEnded: "Sondaggio chiuso." - receiveFollowRequest: "Richieste di follow in arrivo" - followRequestAccepted: "Richieste di follow accettate" - roleAssigned: "Ruolo concesso" - chatRoomInvitationReceived: "Invito in una stanza di chat" + receiveFollowRequest: "Richiesta di follow ricevuta" + followRequestAccepted: "Richiesta di follow accettata" achievementEarned: "Risultato raggiunto" - exportCompleted: "Esportazione completata" - login: "Accessi" - createToken: "Creare un token di accesso" - test: "Notifiche di test" app: "Notifiche da applicazioni" _actions: - followBack: "Following ricambiato" + followBack: "Segui" reply: "Rispondi" renote: "Rinota" _deck: alwaysShowMainColumn: "Mostra sempre la colonna principale" - columnAlign: "Allineamento delle colonne" - columnGap: "Spessore del margine tra colonne" - deckMenuPosition: "Posizione del menu Deck" - navbarPosition: "Posizione barra di navigazione" + columnAlign: "Allineare colonne" addColumn: "Aggiungi colonna" - newNoteNotificationSettings: "Preferenze per le notifiche di nuove Note" - configureColumn: "Impostazioni colonna" + configureColumn: "Impostazioni della colonna." swapLeft: "Sposta a sinistra" swapRight: "Sposta a destra" swapUp: "Sposta in alto" @@ -2671,10 +1961,6 @@ _deck: introduction: "Combinate le colonne per creare la vostra interfaccia!" introduction2: "È possibile aggiungere colonne in qualsiasi momento premendo + sulla destra dello schermo." widgetsIntroduction: "Dal menu della colonna, selezionare \"Modifica i riquadri\" per aggiungere un un riquadro con funzionalità" - useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice" - usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima" - flexible: "Larghezza flessibile" - enableSyncBetweenDevicesForProfiles: "Abilita la sincronizzazione delle informazioni profilo tra dispositivi" _columns: main: "Principale" widgets: "Riquadri" @@ -2682,433 +1968,30 @@ _deck: tl: "Timeline" antenna: "Antenne" list: "Liste" - channel: "Canali" + channel: "Canale" mentions: "Menzioni" - direct: "Note Dirette" + direct: "Diretta" roleTimeline: "Timeline Ruolo" - chat: "Chat" _dialog: - charactersExceeded: "Hai superato il limite di {max} caratteri! ({current})" - charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({current})" + charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})" + charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})" _disabledTimeline: title: "Timeline disabilitata" description: "Il ruolo in cui sei non ti permette di leggere questa timeline" _drivecleaner: - orderBySizeDesc: "Dal file più grosso al più piccolo" - orderByCreatedAtAsc: "Dal file più vecchio al più recente" + orderBySizeDesc: "Dal più grande al più piccolo" + orderByCreatedAtAsc: "Dal più vecchio al più recente" _webhookSettings: createWebhook: "Creazione Webhook" - modifyWebhook: "Modifica Webhook" name: "Nome" secret: "Segreto" - trigger: "Trigger" + events: "Quando eseguire il Webhook" active: "Attivo" _events: - follow: "Quando aggiungi Following" + follow: "Quando segui un profilo" followed: "Quando ti segue un profilo" note: "Quando pubblichi una Nota" reply: "Quando rispondono ad una Nota" renote: "Quando la Nota è Rinotata" reaction: "Quando ricevo una reazione" mention: "Quando mi menzionano" - _systemEvents: - abuseReport: "Quando arriva una segnalazione" - abuseReportResolved: "Quando una segnalazione è risolta" - userCreated: "Quando viene creato un profilo" - inactiveModeratorsWarning: "Quando un profilo moderatore rimane inattivo per un determinato periodo" - inactiveModeratorsInvitationOnlyChanged: "Quando la moderazione è rimasta inattiva per un determinato periodo e il sistema è cambiato in modalità \"solo inviti\"" - deleteConfirm: "Vuoi davvero eliminare il Webhook?" - testRemarks: "Clicca il bottone a destra dell'interruttore, per provare l'invio di un webhook con dati fittizi." -_abuseReport: - _notificationRecipient: - createRecipient: "Aggiungi destinatario della segnalazione" - modifyRecipient: "Modifica destinatario della segnalazione" - recipientType: "Tipo di notifica" - _recipientType: - mail: "Email" - webhook: "Webhook" - _captions: - mail: "Quando ricevi un abuso, notifica l'amministrazione via email" - webhook: "Spedire una notifica al SystemWebhook specificato (sia quando si riceve una segnalazione, che quando viene risolta)" - keywords: "Parole chiave" - notifiedUser: "Profili da notificare" - notifiedWebhook: "Webhook da usare" - deleteConfirm: "Vuoi davvero rimuovere il destinatario della notifica?" -_moderationLogTypes: - createRole: "Ruolo creato" - deleteRole: "Ruolo eliminato" - updateRole: "Ruolo aggiornato" - assignRole: "Ruolo assegnato" - unassignRole: "Ruolo disassegnato" - suspend: "Sospensione" - unsuspend: "Sospensione rimossa" - addCustomEmoji: "Emoji personalizzata aggiunta" - updateCustomEmoji: "Emoji personalizzata aggiornata" - deleteCustomEmoji: "Emoji personalizzata eliminata" - updateServerSettings: "Impostazioni del server aggiornate" - updateUserNote: "Promemoria di moderazione aggiornato" - deleteDriveFile: "File da Drive eliminato" - deleteNote: "Nota eliminata" - createGlobalAnnouncement: "Annuncio globale creato" - createUserAnnouncement: "Annuncio ai profili iscritti creato" - updateGlobalAnnouncement: "Annuncio globale aggiornato" - updateUserAnnouncement: "Annuncio ai profili iscritti aggiornato" - deleteGlobalAnnouncement: "Annuncio globale eliminato" - deleteUserAnnouncement: "Annuncio ai profili iscritti eliminato" - resetPassword: "Password azzerata" - suspendRemoteInstance: "Istanza remota sospesa" - unsuspendRemoteInstance: "Istanza remota riattivata" - updateRemoteInstanceNote: "Aggiornamento del promemoria di moderazione per il server remoto" - markSensitiveDriveFile: "File nel Drive segnato come esplicito" - unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito" - resolveAbuseReport: "Segnalazione risolta" - forwardAbuseReport: "Segnalazione inoltrata" - updateAbuseReportNote: "Ha aggiornato la segnalazione" - createInvitation: "Genera codice di invito" - createAd: "Banner creato" - deleteAd: "Banner eliminato" - updateAd: "Banner aggiornato" - createAvatarDecoration: "Creazione decorazione della foto profilo" - updateAvatarDecoration: "Aggiornamento decorazione foto profilo" - deleteAvatarDecoration: "Eliminazione decorazione della foto profilo" - unsetUserAvatar: "Rimossa foto profilo" - unsetUserBanner: "Rimossa intestazione profilo" - createSystemWebhook: "Crea un SystemWebhook" - updateSystemWebhook: "Modifica SystemWebhook" - deleteSystemWebhook: "Elimina SystemWebhook" - createAbuseReportNotificationRecipient: "Crea destinatario per le notifiche di segnalazioni" - updateAbuseReportNotificationRecipient: "Aggiorna destinatario notifiche di segnalazioni" - deleteAbuseReportNotificationRecipient: "Elimina destinatario notifiche di segnalazioni" - deleteAccount: "Quando viene eliminato un profilo" - deletePage: "Pagina eliminata" - deleteFlash: "Play eliminato" - deleteGalleryPost: "Eliminazione pubblicazione nella Galleria" - deleteChatRoom: "Elimina chat" - updateProxyAccountDescription: "Aggiornata la descrizione del profilo proxy" -_fileViewer: - title: "Dettagli del file" - type: "Tipo di file" - size: "Dimensioni file" - url: "URL" - uploadedAt: "Caricato il" - attachedNotes: "Note a cui è allegato" - thisPageCanBeSeenFromTheAuthor: "Questa pagina può essere vista solo da chi ha caricato il file." -_externalResourceInstaller: - title: "Installa da sito esterno" - checkVendorBeforeInstall: "Prima di installare, assicurati che la fonte sia affidabile." - _plugin: - title: "Vuoi davvero installare questo componente aggiuntivo?" - _theme: - title: "Vuoi davvero installare questa variazione grafica?" - _meta: - base: "Combinazione base di colori" - _vendorInfo: - title: "Informazioni sulla fonte" - endpoint: "Punto di riferimento della fonte" - hashVerify: "Codice di verifica della fonte" - _errors: - _invalidParams: - title: "Parametri non validi" - description: "Mancano alcuni parametri per il caricamento, per favore, verifica la URL." - _resourceTypeNotSupported: - title: "Questa risorsa esterna non è supportata" - description: "Il tipo di risorsa ottenuta da questo sito esterno non è supportato. Si prega di contattare la fonte di distribuizone." - _failedToFetch: - title: "Impossibile ottenere i dati" - fetchErrorDescription: "Si è verificato un errore di comunicazione con la fonte. Se riprovare di nuovo non aiuta, contattare la fonte di distribuzione." - parseErrorDescription: "Si è verificato un errore elaborando i dati ottenuti dalla fonte. Per favore contattare il distributore." - _hashUnmatched: - title: "Dati non verificabili, diversi da quelli della fonte" - description: "Si è verificato un errore durante la verifica di integrità dei dati ottenuti. Per sicurezza, l'installazione è stata interrotta. Contattare la fonte di distribuzione." - _pluginParseFailed: - title: "Errore AiScript" - description: "Sebbene i dati ottenuti siano validi, non è stato possibile interpretarli, perché si è verificato un errore durante l'analisi di AiScript. Si prega di contattare gli autori del componente aggiuntivo. Potresti controllare la console di Javascript per ottenere dettagli aggiuntivi." - _pluginInstallFailed: - title: "Impossibile installare il componente aggiuntivo" - description: "Si è verificato un impedimento durante l'installazione del componente aggiuntivo. Per favore riprova e consulta la console di Javascript per ottenere dettagli aggiuntivi." - _themeParseFailed: - title: "Impossibile interpretare la variazione grafica" - description: "Sebbene i dati siano stati ottenuti, non è stato possibile interpretarli, si è verificato un errore durante l'analisi della variazione grafica. Si prega di contattare gli autori. Potresti anche controllare la console di Javascript per ottenere dettagli aggiuntivi." - _themeInstallFailed: - title: "Impossibile installare la variazione grafica" - description: "Si è verificato un impedimento durante l'installazione della variazione grafica. Per favore riprova e consulta la console di Javascript per ottenere dettagli aggiuntivi." -_dataSaver: - _media: - title: "Caricamento dei media" - description: "Impedire il caricamento automatico di immagini e video. Devi toccare le immagini o i video nascosti per caricarli." - _avatar: - title: "Immagine del profilo" - description: "Impedire l'animazione per l'immagine del profilo. Le immagini animate possono avere dimensioni file maggiori rispetto a quelle normali, puoi ridurre ulteriormente l'utilizzo dei dati." - _urlPreviewThumbnail: - title: "Nascondi le miniature nell'anteprima URL" - description: "Le immagini in miniatura nell'anteprima URL non verranno più caricate." - _disableUrlPreview: - title: "Disabilita l'anteprima URL" - description: "Disabilita la funzione di anteprima URL. A differenza di una semplice immagine in miniatura, questo riduce il tempo necessario per caricare le informazioni collegate." - _code: - title: "Codice evidenziato" - description: "Impedire che il codice sorgente sia automaticamente evidenziato. Evidenziare il codice richiede il caricamento di un file per ogni linguaggio. Puoi evidenziare soltanto il codice che intendi leggere e ridurre il traffico inutilizzato." -_hemisphere: - N: "Emisfero boreale" - S: "Emisfero australe" - caption: "Utile per alcune impostazioni del client, per determinare la stagione." -_reversi: - reversi: "Reversi" - gameSettings: "Impostazioni di gioco" - chooseBoard: "Segli la tavola" - blackOrWhite: "Neri / Bianchi" - blackIs: "{name} muove i Neri" - rules: "Regole del gioco" - thisGameIsStartedSoon: "Il gioco sta per iniziare" - waitingForOther: "Attendere l'avversario" - waitingForMe: "Ti stanno aspettando" - waitingBoth: "Preparatevi" - ready: "Pronti" - cancelReady: "Riprendere la preparazione" - opponentTurn: "Turno avversario" - myTurn: "Tocca a te" - turnOf: "Tocca a {name}" - pastTurnOf: "Turno di {name}" - surrender: "Mi arrendo" - surrendered: "Ha ceduto" - timeout: "Tempo scaduto" - drawn: "Pareggio" - won: "Ha vinto {name}" - black: "Neri" - white: "Bianchi" - total: "Totale" - turnCount: "Turno N. {count}" - myGames: "Le mie sfide" - allGames: "Tutte le sfide" - ended: "Conclusione" - playing: "In gioco" - isLlotheo: "Vince chi ha meno pietre (Roseo)" - loopedMap: "Mappa ricorsiva" - canPutEverywhere: "Modalità che può essere posizionata ovunque" - timeLimitForEachTurn: "Tempo limite per turno" - freeMatch: "Sfida libera" - lookingForPlayer: "Alla ricerca di un avversario" - gameCanceled: "Sfida cancellata" - shareToTlTheGameWhenStart: "Pubblica l'inizio della partita sulla tua Timeline" - iStartedAGame: "Inizia la sfida! #MisskeyReversi" - opponentHasSettingsChanged: "L'avversario ha cambiato configurazione" - allowIrregularRules: "Regole inconsuete (completamente libere)" - disallowIrregularRules: "Impedire le regole inconsuete" - showBoardLabels: "Mostra le coordinate del gioco" - useAvatarAsStone: "Immagini profilo come pedine" -_offlineScreen: - title: "Scollegato. Impossibile connettersi al server" - header: "Impossibile connettersi al server" -_urlPreviewSetting: - title: "Impostazioni per l'anteprima delle URL" - enable: "Attiva l'anteprima delle URL" - timeout: "Timeout dell'anteprima in millisecondi" - timeoutDescription: "Impegna al massimo il tempo indicato, altrimenti ignora l'anteprima" - maximumContentLength: "Grandezza del contenuto (Content-Length in byte)" - maximumContentLengthDescription: "Se la grandezza supera il valore, l'anteprima verrà ignorata." - requireContentLength: "Genenerare l'anteprima solo quando è definito Content-Length" - requireContentLengthDescription: "In assenza di questo parametro dal server remoto, l'anteprima verrà ignorata." - userAgent: "User-Agent" - userAgentDescription: "Definire con quale User-Agent si intende identificarsi durante l'acquisizione di un'anteprima. Se è vuoto, useremo il valore predefinito." - summaryProxy: "Endpoint proxy che genera l'anteprima" - summaryProxyDescription: "Genera anteprime utilizzando un proxy Summaly anziché Misskey." - summaryProxyDescription2: "I parametri sono collegano al proxy come stringa query. Se il proxy non li supporta, verranno ignorati." -_mediaControls: - pip: "Sovraimpressione" - playbackRate: "Velocità di riproduzione" - loop: "Ripetizione infinita" -_contextMenu: - title: "Menu contestuale" - app: "Applicazione" - appWithShift: "Applicazione Shift+Tasto" - native: "Interfaccia utente del browser" -_gridComponent: - _error: - requiredValue: "Campo obbligatorio" - columnTypeNotSupport: "Solo le colonne type:text permettono la convalida delle Espresioni Regolari" - patternNotMatch: "Il valore non coincide con {pattern}" - notUnique: "Il valore deve essere univoco" -_roleSelectDialog: - notSelected: "Niente selezioato" -_customEmojisManager: - _gridCommon: - copySelectionRows: "Copia le righe selezionate" - copySelectionRanges: "Copia l'intervallo selezionato" - deleteSelectionRows: "Elimina le righe selezionate" - deleteSelectionRanges: "Elimina le righe nell'intervallo selezionato" - searchSettings: "Impostazioni di ricerca" - searchSettingCaption: "Imposta condizioni di ricerca dettagliate." - searchLimit: "Risultati visualizzati" - sortOrder: "Ordine" - registrationLogs: "Storico della registrazione" - registrationLogsCaption: "Lo storico verrà visualizzato in base alla attività sulle emoji. Scompare quando si esegue un'operazione di aggiornamento/eliminazione o si modifica/ricarica la pagina." - alertEmojisRegisterFailedDescription: "Attenzione, è impossibile modificare la emoji. Si prega di controllare lo storico per ulteriori dettagli." - _logs: - showSuccessLogSwitch: "Mostra le azioni a buon fine" - failureLogNothing: "Non ci sono errori nello storico delle emoji" - logNothing: "Lo storico è vuoto." - _remote: - selectionRowDetail: "Dettagli della riga selezionata" - importSelectionRows: "Importa le righe selezionate" - importSelectionRangesRows: "Importa le righe nell'intervallo selezionato" - importEmojisButton: "Importa le emoji selezionate" - confirmImportEmojisTitle: "Importazione emoji" - confirmImportEmojisDescription: "Importazione di {count} emoji ricevute da remoto. Si prega di prestare molta attenzione al tipo di licenza delle emoji. Vuoi confermare?" - _local: - tabTitleList: "Elenco delle emoji registrate" - tabTitleRegister: "Registrazione emoji" - _list: - emojisNothing: "Non ci sono emoji registrate." - markAsDeleteTargetRows: "Selezionare le righe come eliminabili" - markAsDeleteTargetRanges: "Selezionare le righe nell'intervallo come eliminabili" - alertUpdateEmojisNothingDescription: "Non ci sono emoji aggiornate." - alertDeleteEmojisNothingDescription: "Non ci sono emoji da eliminare." - confirmMovePage: "Vuoi davvero spostare la pagina?" - confirmChangeView: "Vuoi davvero cambiare la vista?" - confirmUpdateEmojisDescription: "Aggiornamento di {count} emoji. Vuoi davvero continuare?" - confirmDeleteEmojisDescription: "Eliminazione delle {count} emoji selezionate. Vuoi davvero continuare?" - confirmResetDescription: "Verranno ripristinate tutte le modifiche apportate finora." - confirmMovePageDesciption: "Sono state modificate le emoji in questa pagina.\nUscendo senza salvare, tutte le modifiche verranno ignorate." - dialogSelectRoleTitle: "Cerca emoji per ruolo" - _register: - uploadSettingTitle: "Caricamento impostazioni" - uploadSettingDescription: "Questa schermata ti permette di scegliere il comportamento durante il caricamento delle emoji." - directoryToCategoryLabel: "Inseriscile in una cartella omonima alla categoria" - directoryToCategoryCaption: "Crea il campo categoria in base alla cartella." - confirmRegisterEmojisDescription: "Registrazione delle emoji elencate come nuove emoji personalizzate. Vuoi davvero procedere? (Per evitare sovraccarichi, puoi registrare al massimo {count} emoji per volta)" - confirmClearEmojisDescription: "Annullare le modifiche e cancella le emoji nell'elenco. Confermi?" - confirmUploadEmojisDescription: "Caricamento sul Drive di {count} file locali. Vuoi davvero procedere?" -_embedCodeGen: - title: "Personalizza il codice di incorporamento" - header: "Mostra la testata" - autoload: "Carica automaticamente di più (sconsigliato)" - maxHeight: "Altezza massima" - maxHeightDescription: "Specifica un valore per evitare che continui a crescere verticalmente. Il valore 0 disabilita il limite d'altezza." - maxHeightWarn: "L'altezza massima è disabilitata (0). Se l'effetto è indesiderato, prova a impostare l'altezza massima a un valore specifico." - previewIsNotActual: "Poiché supera l'intervallo che può essere visualizzato in anteprima, la visualizzazione vera e propria sarà diversa quando effettivamente incorporata." - rounded: "Bordo arrotondato" - border: "Aggiungi un bordo al contenitore" - applyToPreview: "Applica all'anteprima" - generateCode: "Crea il codice di incorporamento" - codeGenerated: "Codice generato" - codeGeneratedDescription: "Incolla il codice appena generato sul tuo sito web." -_selfXssPrevention: - warning: "Avviso" - title: "\"Incolla qualcosa su questa schermata\" è tutta una truffa." - description1: "Incollando qualcosa qui, malintenzionati potrebbero prendere il controllo del tuo profilo o rubare i tuoi dati personali." - description2: "Se non sai esattamente cosa stai facendo, %c smetti subito e chiudi questa finestra." - description3: "Per favore, controlla questo collegamento per avere maggiori dettagli. {link}" -_followRequest: - recieved: "Richieste in ingresso" - sent: "Richieste in uscita" -_remoteLookupErrors: - _federationNotAllowed: - title: "Server irraggiungibile" - description: "La comunicazione con questo server potrebbe essere disattivata. Hai bloccato il server? Oppure potrebbero averlo bloccato gli amministratori. Contattali per ulteriori informazioni." - _uriInvalid: - title: "URL non valido" - description: "Controlla che l'indirizzo sia valido e sia privo di caratteri non validi." - _requestFailed: - title: "Richiesta fallita" - description: "La comunicazione col server non è riuscita. Potrebbe essere inattivo. Assicurati anche che la URL sia valida." - _responseInvalid: - title: "Risposta non valida" - description: "La comunicazione col server è andata a buon fine, ma abbiamo ricevuto dati non validi." - _noSuchObject: - title: "Non trovato" - description: "La risorsa richiesta non è stata trovata. Verificare nuovamente la URL." -_captcha: - verify: "Per favore, controlla la verifica CAPTCHA" - testSiteKeyMessage: "Puoi provare l'anteprima inserendo valori di test, sia per la chiave del sito che per la chiave segreta.\nSi prega di controllare la pagina qui sotto per i dettagli." - _error: - _requestFailed: - title: "Errore durante la richiesta del CAPTCHA" - text: "Riprova più tardi o controlla nuovamente le impostazioni." - _verificationFailed: - title: "Convalida CAPTCHA non riuscita" - text: "Si prega di verificare nuovamente se le impostazioni sono corrette." - _unknown: - title: "Errore CAPTCHA" - text: "Si è verificato un errore imprevisto." -_bootErrors: - title: "Caricamento non riuscito" - serverError: "Dopo una breve attesa, e dopo aver ricaricato la pagina, se il problema persiste, contatta l'amministrazione comunicando il seguente ID di errore." - solution: "Di seguito, alcune probabili soluzioni al problema." - solution1: "Aggiornare browser e il sistema operativo all'ultima versione" - solution2: "Disattivare gli adblocker" - solution3: "Cancellare la cache del browser" - solution4: "(Per chi utilizza il Browser Tor) Impostare dom.webaudio.enabled = vero" - otherOption: "Altre opzioni" - otherOption1: "Nelle impostazioni, cancellare le impostazioni del client e svuotare la cache" - otherOption2: "Avviare il client predefinito" - otherOption3: "Avviare lo strumento di riparazione" -_search: - searchScopeAll: "Tutte" - searchScopeLocal: "Locale" - searchScopeServer: "Specifiche del server" - searchScopeUser: "Profilo specifico" - pleaseEnterServerHost: "Inserire il nome host" - pleaseSelectUser: "Per favore, seleziona un profilo" - serverHostPlaceholder: "Es: misskey.example.com" -_serverSetupWizard: - installCompleted: "L'installazione di Misskey è completata!" - firstCreateAccount: "Per prima cosa, crea un account amministratore." - accountCreated: "Il tuo account amministratore è stato creato!" - serverSetting: "Configurazione del server" - youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Questa procedura guidata ti aiuterà a configurare facilmente il tuo server in modo ottimale." - settingsYouMakeHereCanBeChangedLater: "Potrai anche modificare le impostazioni in seguito." - howWillYouUseMisskey: "Come si usa Misskey?" - _use: - single: "Modalità utenza singola" - single_description: "Se intendi usarlo come tuo server personale" - single_youCanCreateMultipleAccounts: "Anche se lo utilizzi come server per una sola persona, puoi creare più account in base alle tue esigenze." - group: "Modalità multi utentza" - group_description: "Invita altre persone fidate ad usare il server insieme a te" - open: "Server aperto" - open_description: "Per ospitare un numero imprecisato di persone" - openServerAdvice: "Ospitare un numero imprecisato di persone comporta dei rischi. Ti consigliamo di adottare un solido sistema di moderazione, in modo da poter gestire eventuali problemi che potrebbero presentarsi pubblicando contenuti proposti da altre persone, che potrebbero essere sconosciute." - openServerAntiSpamAdvice: "Presta molta attenzione alla sicurezza, ad esempio attivando funzionalità anti-bot (iscrizioni automatiche) come reCAPTCHA. Questo può evitare che il tuo server diventi un trampolino di lancio per lo spam di altri." - howManyUsersDoYouExpect: "Quante persone pensi che parteciperanno?" - _scale: - small: "100 persone o meno (piccolo)" - medium: "Da 100 a 1000 persone (medio)" - large: "Oltre 1000 persone (grande)" - largeScaleServerAdvice: "Configurare grandi server potrebbe richiedere conoscenze infrastrutturali avanzate, ad esempio, il bilanciamento del carico e la replicazione del database." - doYouConnectToFediverse: "Vuoi connetterti al Fediverso?" - doYouConnectToFediverse_description1: "Collegandosi a una rete di server distribuiti, denominata Fediverso, potrai scambiare contenuti con altri server, tramite il protocollo di comunicazione ActivityPub." - doYouConnectToFediverse_description2: "Connettersi al Fediverso è anche detto \"federazione\"." - youCanConfigureMoreFederationSettingsLater: "Puoi svolgere la configurazione avanzata anche dopo. Ad esempio specificando quali server possono federarsi." - adminInfo: "Informazioni sull'amministratore" - adminInfo_description: "Imposta le informazioni dell'amministratore utilizzate per accettare le richieste." - adminInfo_mustBeFilled: "Questa operazione è necessaria su un server aperto o se è attiva la federazione." - followingSettingsAreRecommended: "Si consigliano le seguenti impostazioni:" - applyTheseSettings: "Applica questa impostazione" - skipSettings: "Salta l'installazione" - settingsCompleted: "Installazione completata!" - settingsCompleted_description: "Grazie per il tuo impegno. Adesso che hai completato la configurazione, puoi iniziare a utilizzare il tuo server." - settingsCompleted_description2: "Le impostazioni dettagliate del server possono essere effettuate tramite il Pannello di controllo." - donationRequest: "Per favore Fai una donazione" - _donationRequest: - text1: "Misskey è un software libero sviluppato da volontari." - text2: "Se puoi, ti preghiamo di prendere in considerazione l'idea di fare una donazione, così potremo continuare a sviluppare." - text3: "Sono previsti anche dei vantaggi speciali per i sostenitori!" -_uploader: - compressedToX: "Compresso in {x}" - savedXPercent: "{x}% risparmiati" - abortConfirm: "Alcuni file non sono stati caricati. Vuoi annullare l'operazione?" - doneConfirm: "Alcuni file non sono stati caricati. Vuoi completarli?" - maxFileSizeIsX: "La dimensione massima del file che puoi caricare è {x}." - allowedTypes: "Tipi di file caricabili" - tip: "Il file non è ancora stato caricato. Puoi controllare, rinominare, comprimere, ritagliare, prima del caricamento. Quando hai finito, premi il bottone \"Carica\" ​​per completare." -_clientPerformanceIssueTip: - title: "Se ritieni che la batteria si stia scaricando troppo" - makeSureDisabledAdBlocker: "Disattiva il tuo AdBlocker" - makeSureDisabledAdBlocker_description: "Gli AdBlocker possono influire sulle prestazioni. Controlla se nel tuo sistema operativo, nel browser o nei componenti aggiuntivi è abilitato un AdBlocker." - makeSureDisabledCustomCss: "Disabilita CSS personalizzato" - makeSureDisabledCustomCss_description: "La riscrittura degli stili CSS può influire sulle prestazioni. Assicurati di non avere CSS personalizzati o estensioni abilitate che sovrascrivano i tuoi stili." - makeSureDisabledAddons: "Disabilitare le estensioni" - makeSureDisabledAddons_description: "Alcune estensioni potrebbero interferire con il funzionamento del client e comprometterne le prestazioni. Prova a disattivare le estensioni del browser e vedi se il problema persiste." -_clip: - tip: "Le clip sono una funzionalità che consente di raggruppare le Note." -_userLists: - tip: "Puoi creare un elenco di Note create da qualsiasi profilo. L'elenco è visualizzato come una sequenza temporale." diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c7971507aa..839edd25b2 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -5,13 +5,9 @@ introMisskey: "ようこそ!Misskeyは、オープンソースの分散型マ poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォームMisskeyのサーバーのひとつです。" monthAndDay: "{month}月 {day}日" search: "検索" -reset: "リセット" notifications: "通知" username: "ユーザー名" password: "パスワード" -initialPasswordForSetup: "初期設定開始用パスワード" -initialPasswordIsIncorrect: "初期設定開始用のパスワードが違います。" -initialPasswordForSetupDescription: "Misskeyを自分でインストールした場合は、設定ファイルに入力したパスワードを使用してください。\nMisskeyのホスティングサービスなどを使用している場合は、提供されたパスワードを使用してください。\nパスワードを設定していない場合は、空欄にしたまま続行してください。" forgotPassword: "パスワードを忘れた" fetchingAsApObject: "連合に照会中" ok: "OK" @@ -19,7 +15,7 @@ gotIt: "わかった" cancel: "キャンセル" noThankYou: "やめておく" enterUsername: "ユーザー名を入力" -renotedBy: "{user}がリノート" +renotedBy: "{user}がRenote" noNotes: "ノートはありません" noNotifications: "通知はありません" instance: "サーバー" @@ -49,11 +45,9 @@ pin: "ピン留め" unpin: "ピン留め解除" copyContent: "内容をコピー" copyLink: "リンクをコピー" -copyRemoteLink: "リモートのリンクをコピー" -copyLinkRenote: "リノートのリンクをコピー" delete: "削除" deleteAndEdit: "削除して編集" -deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、リノート、返信も全て削除されます。" +deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、Renote、返信も全て削除されます。" addToList: "リストに追加" addToAntenna: "アンテナに追加" sendMessage: "メッセージを送信" @@ -65,7 +59,6 @@ copyFileId: "ファイルIDをコピー" copyFolderId: "フォルダーIDをコピー" copyProfileUrl: "プロフィールURLをコピー" searchUser: "ユーザーを検索" -searchThisUsersNotes: "ユーザーのノートを検索" reply: "返信" loadMore: "もっと見る" showMore: "もっと見る" @@ -81,7 +74,7 @@ import: "インポート" export: "エクスポート" files: "ファイル" download: "ダウンロード" -driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した一部のコンテンツも削除されます。" +driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した全てのコンテンツからも削除されます。" unfollowConfirm: "{name}のフォローを解除しますか?" exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。" importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。" @@ -111,17 +104,14 @@ followRequests: "フォロー申請" unfollow: "フォロー解除" followRequestPending: "フォロー許可待ち" enterEmoji: "絵文字を入力" -renote: "リノート" -unrenote: "リノート解除" -renoted: "リノートしました。" -renotedToX: "{name} にリノートしました。" -cantRenote: "この投稿はリノートできません。" -cantReRenote: "リノートをリノートすることはできません。" +renote: "Renote" +unrenote: "Renote解除" +renoted: "Renoteしました。" +cantRenote: "この投稿はRenoteできません。" +cantReRenote: "RenoteをRenoteすることはできません。" quote: "引用" -inChannelRenote: "チャンネル内リノート" +inChannelRenote: "チャンネル内Renote" inChannelQuote: "チャンネル内引用" -renoteToChannel: "チャンネルにリノート" -renoteToOtherChannel: "他のチャンネルにリノート" pinnedNote: "ピン留めされたノート" pinned: "ピン留め" you: "あなた" @@ -130,16 +120,10 @@ sensitive: "センシティブ" add: "追加" reaction: "リアクション" reactions: "リアクション" -emojiPicker: "絵文字ピッカー" -pinnedEmojisForReactionSettingDescription: "リアクション時にピン留め表示する絵文字を設定できます" -pinnedEmojisSettingDescription: "絵文字入力時にピン留め表示する絵文字を設定できます" -emojiPickerDisplay: "ピッカーの表示" -overwriteFromPinnedEmojisForReaction: "リアクション設定から上書きする" -overwriteFromPinnedEmojis: "全般設定から上書きする" +reactionSetting: "ピッカーに表示するリアクション" reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。" rememberNoteVisibility: "公開範囲を記憶する" attachCancel: "添付取り消し" -deleteFile: "ファイルを削除" markAsSensitive: "センシティブとして設定" unmarkAsSensitive: "センシティブを解除する" enterFileName: "ファイル名を入力" @@ -160,7 +144,6 @@ editList: "リストを編集" selectChannel: "チャンネルを選択" selectAntenna: "アンテナを選択" editAntenna: "アンテナを編集" -createAntenna: "アンテナを作成" selectWidget: "ウィジェットを選択" editWidgets: "ウィジェットを編集" editWidgetsExit: "編集を終了" @@ -172,10 +155,9 @@ emojiUrl: "絵文字画像URL" addEmoji: "絵文字を追加" settingGuide: "おすすめ設定" cacheRemoteFiles: "リモートのファイルをキャッシュする" -cacheRemoteFilesDescription: "この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持します。" -youCanCleanRemoteFilesCache: "ファイル管理の🗑️ボタンで全てのキャッシュを削除できます。" -cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする" -cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。" +cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。" +cacheRemoteSensitiveFiles: "リモートのNSFWファイルをキャッシュする" +cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのNSFWファイルだけはキャッシュせず直リンクするようになります。" flagAsBot: "Botとして設定" flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。" flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!" @@ -187,10 +169,6 @@ addAccount: "アカウントを追加" reloadAccountsList: "アカウントリストの情報を更新" loginFailed: "ログインに失敗しました" showOnRemote: "リモートで表示" -continueOnRemote: "リモートで続行" -chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択" -specifyServerHost: "サーバーのドメインを直接指定" -inputHostName: "ドメインを入力してください" general: "全般" wallpaper: "壁紙" setWallpaper: "壁紙を設定" @@ -201,7 +179,6 @@ followConfirm: "{name}をフォローしますか?" proxyAccount: "プロキシアカウント" proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" host: "ホスト" -selectSelf: "自分を選択" selectUser: "ユーザーを選択" recipient: "宛先" annotation: "注釈" @@ -216,11 +193,8 @@ perHour: "1時間ごと" perDay: "1日ごと" stopActivityDelivery: "アクティビティの配送を停止" blockThisInstance: "このサーバーをブロック" -silenceThisInstance: "サーバーをサイレンス" -mediaSilenceThisInstance: "サーバーをメディアサイレンス" operations: "操作" software: "ソフトウェア" -softwareName: "ソフトウェア名" version: "バージョン" metadata: "メタデータ" withNFiles: "{n}つのファイル" @@ -237,13 +211,7 @@ clearQueueConfirmText: "未配達の投稿は配送されなくなります。 clearCachedFiles: "キャッシュをクリア" clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?" blockedInstances: "ブロックしたサーバー" -blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。" -silencedInstances: "サイレンスしたサーバー" -silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。" -mediaSilencedInstances: "メディアサイレンスしたサーバー" -mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。" -federationAllowedHosts: "連合を許可するサーバー" -federationAllowedHostsDescription: "連合を許可するサーバーのホストを改行で区切って設定します。" +blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このサーバーとやり取りできなくなります。サブドメインもブロックされます。" muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしたユーザー" blockedUsers: "ブロックしたユーザー" @@ -251,6 +219,7 @@ noUsers: "ユーザーはいません" editProfile: "プロフィールを編集" noteDeleteConfirm: "このノートを削除しますか?" pinLimitExceeded: "これ以上ピン留めできません" +intro: "Misskeyのインストールが完了しました!管理者アカウントを作成しましょう。" done: "完了" processing: "処理中" preview: "プレビュー" @@ -287,8 +256,8 @@ removed: "削除しました" removeAreYouSure: "「{x}」を削除しますか?" deleteAreYouSure: "「{x}」を削除しますか?" resetAreYouSure: "リセットしますか?" -areYouSure: "よろしいですか?" saved: "保存しました" +messaging: "チャット" upload: "アップロード" keepOriginalUploading: "オリジナル画像を保持" keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。" @@ -298,11 +267,10 @@ uploadFromUrl: "URLアップロード" uploadFromUrlDescription: "アップロードしたいファイルのURL" uploadFromUrlRequested: "アップロードをリクエストしました" uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。" -uploadNFiles: "{n}個のファイルをアップロード" explore: "みつける" messageRead: "既読" noMoreHistory: "これより過去の履歴はありません" -startChat: "チャットを始める" +startMessaging: "チャットを開始" nUsersRead: "{n}人が読みました" agreeTo: "{0}に同意" agree: "同意する" @@ -327,22 +295,18 @@ dark: "ダーク" lightThemes: "明るいテーマ" darkThemes: "暗いテーマ" syncDeviceDarkMode: "デバイスのダークモードと同期する" -switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」がオンになっています。同期をオフにして手動でモードを切り替えますか?" drive: "ドライブ" fileName: "ファイル名" selectFile: "ファイルを選択" selectFiles: "ファイルを選択" selectFolder: "フォルダーを選択" selectFolders: "フォルダーを選択" -fileNotSelected: "ファイルが選択されていません" renameFile: "ファイル名を変更" folderName: "フォルダー名" createFolder: "フォルダーを作成" renameFolder: "フォルダー名を変更" deleteFolder: "フォルダーを削除" -folder: "フォルダー" addFile: "ファイルを追加" -showFile: "ファイルを表示" emptyDrive: "ドライブは空です" emptyFolder: "フォルダーは空です" unableToDelete: "削除できません" @@ -365,7 +329,7 @@ watch: "ウォッチ" unwatch: "ウォッチ解除" accept: "許可" reject: "拒否" -normal: "通常" +normal: "正常" instanceName: "サーバー名" instanceDescription: "サーバーの紹介" maintainerName: "管理者の名前" @@ -385,10 +349,12 @@ enableLocalTimeline: "ローカルタイムラインを有効にする" enableGlobalTimeline: "グローバルタイムラインを有効にする" disablingTimelinesInfo: "これらのタイムラインを無効化しても、利便性のため管理者およびモデレーターは引き続き利用することができます。" registration: "登録" +enableRegistration: "誰でも新規登録できるようにする" invite: "招待" driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量" driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量" inMb: "メガバイト単位" +iconUrl: "アイコン画像のURL (faviconなど)" bannerUrl: "バナー画像のURL" backgroundImageUrl: "背景画像のURL" basicInfo: "基本情報" @@ -402,11 +368,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptchaを有効にする" hcaptchaSiteKey: "サイトキー" hcaptchaSecretKey: "シークレットキー" -mcaptcha: "mCaptcha" -enableMcaptcha: "mCaptchaを有効にする" -mcaptchaSiteKey: "サイトキー" -mcaptchaSecretKey: "シークレットキー" -mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHAを有効にする" recaptchaSiteKey: "サイトキー" @@ -422,11 +383,9 @@ name: "名前" antennaSource: "受信ソース" antennaKeywords: "受信キーワード" antennaExcludeKeywords: "除外キーワード" -antennaExcludeBots: "Botアカウントを除外" antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" notifyAntenna: "新しいノートを通知する" withFileAntenna: "ファイルが添付されたノートのみ" -excludeNotesInSensitiveChannel: "センシティブなチャンネルのノートを除外" enableServiceworker: "ブラウザへのプッシュ通知を有効にする" antennaUsersDescription: "ユーザー名を改行で区切って指定します" caseSensitive: "大文字小文字を区別する" @@ -451,15 +410,10 @@ aboutMisskey: "Misskeyについて" administrator: "管理者" token: "確認コード" 2fa: "二要素認証" -setupOf2fa: "二要素認証のセットアップ" totp: "認証アプリ" totpDescription: "認証アプリを使ってワンタイムパスワードを入力" moderator: "モデレーター" moderation: "モデレーション" -moderationNote: "モデレーションノート" -moderationNoteDescription: "モデレーター間でだけ共有されるメモを記入することができます。" -addModerationNote: "モデレーションノートを追加する" -moderationLogs: "モデログ" nUsersMentioned: "{n}人が投稿" securityKeyAndPasskey: "セキュリティキー・パスキー" securityKey: "セキュリティキー" @@ -475,6 +429,7 @@ share: "共有" notFound: "見つかりません" notFoundDescription: "指定されたURLに該当するページはありませんでした。" uploadFolder: "既定アップロード先" +cacheClear: "キャッシュを削除" markAsReadAllNotifications: "すべての通知を既読にする" markAsReadAllUnreadNotes: "すべての投稿を既読にする" markAsReadAllTalkMessages: "すべてのチャットを既読にする" @@ -492,10 +447,10 @@ retype: "再入力" noteOf: "{user}のノート" quoteAttached: "引用付き" quoteQuestion: "引用として添付しますか?" -attachAsFileQuestion: "クリップボードのテキストが長いです。テキストファイルとして添付しますか?" +noMessagesYet: "まだチャットはありません" +newMessageExists: "新しいメッセージがあります" onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです" -signinRequired: "続行する前に、登録またはログインが必要です" -signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります" +signinRequired: "続行する前に、サインアップまたはサインインが必要です" invitations: "招待" invitationCode: "招待コード" checking: "確認しています" @@ -517,12 +472,8 @@ uiLanguage: "UIの表示言語" aboutX: "{x}について" emojiStyle: "絵文字のスタイル" native: "ネイティブ" -menuStyle: "メニューのスタイル" -style: "スタイル" -drawer: "ドロワー" -popup: "ポップアップ" +disableDrawer: "メニューをドロワーで表示しない" showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する" -showReactionsCount: "ノートのリアクション数を表示する" noHistory: "履歴はありません" signinHistory: "ログイン履歴" enableAdvancedMfm: "高度なMFMを有効にする" @@ -575,22 +526,16 @@ serverLogs: "サーバーログ" deleteAll: "全て削除" showFixedPostForm: "タイムライン上部に投稿フォームを表示する" showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)" -withRepliesByDefaultForNewlyFollowed: "フォローする際、デフォルトで返信をTLに含むようにする" newNoteRecived: "新しいノートがあります" -newNote: "新しいノート" sounds: "サウンド" sound: "サウンド" -notificationSoundSettings: "通知音の設定" listen: "聴く" none: "なし" showInPage: "ページで表示" popout: "ポップアウト" volume: "音量" masterVolume: "マスター音量" -notUseSound: "サウンドを出力しない" -useSoundOnlyWhenActive: "Misskeyがアクティブな時のみサウンドを出力する" details: "詳細" -renoteDetails: "リノートの詳細" chooseEmoji: "絵文字を選択" unableToProcess: "操作を完了できません" recentUsed: "最近使用" @@ -606,16 +551,10 @@ ascendingOrder: "昇順" descendingOrder: "降順" scratchpad: "スクラッチパッド" scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。" -uiInspector: "UIインスペクター" -uiInspectorDescription: "メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。" output: "出力" script: "スクリプト" disablePagesScript: "Pagesのスクリプトを無効にする" updateRemoteUser: "リモートユーザー情報の更新" -unsetUserAvatar: "アイコンを解除" -unsetUserAvatarConfirm: "アイコンを解除しますか?" -unsetUserBanner: "バナーを解除" -unsetUserBannerConfirm: "バナーを解除しますか?" deleteAllFiles: "すべてのファイルを削除" deleteAllFilesConfirm: "すべてのファイルを削除しますか?" removeAllFollowing: "フォローを全解除" @@ -645,7 +584,7 @@ poll: "アンケート" useCw: "内容を隠す" enablePlayer: "プレイヤーを開く" disablePlayer: "プレイヤーを閉じる" -expandTweet: "ポストを展開する" +expandTweet: "ツイートを展開する" themeEditor: "テーマエディター" description: "説明" describeFile: "キャプションを付ける" @@ -666,7 +605,6 @@ medium: "中" small: "小" generateAccessToken: "アクセストークンの発行" permission: "権限" -adminPermission: "管理者権限" enableAll: "全て有効にする" disableAll: "全て無効にする" tokenRequested: "アカウントへのアクセス許可" @@ -688,19 +626,13 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecureInfo: "STARTTLS使用時はオフにします。" testEmail: "配信テスト" wordMute: "ワードミュート" -wordMuteDescription: "指定した語句を含むノートを最小化します。最小化されたノートをクリックすることで表示することができます。" -hardWordMute: "ハードワードミュート" -showMutedWord: "ミュートされたワードを表示" -hardWordMuteDescription: "指定した語句を含むノートを隠します。ワードミュートとは異なり、ノートは完全に表示されなくなります。" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:" instanceMute: "サーバーミュート" userSaysSomething: "{name}が何かを言いました" -userSaysSomethingAbout: "{name}が「{word}」について何かを言いました" makeActive: "アクティブにする" display: "表示" copy: "コピー" -copiedToClipboard: "クリップボードにコピーされました" metrics: "メトリクス" overview: "概要" logs: "ログ" @@ -715,21 +647,22 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使 other: "その他" regenerateLoginToken: "ログイントークンを再生成" regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" -theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。" setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" fileIdOrUrl: "ファイルIDまたはURL" behavior: "動作" sample: "サンプル" abuseReports: "通報" reportAbuse: "通報" -reportAbuseRenote: "リノートを通報" reportAbuseOf: "{name}を通報する" -fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。" +fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。" abuseReported: "内容が送信されました。ご報告ありがとうございました。" reporter: "通報者" reporteeOrigin: "通報先" reporterOrigin: "通報元" +forwardReport: "リモートサーバーに通報を転送する" +forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。" send: "送信" +abuseMarkAsResolved: "対応済みにする" openInNewTab: "新しいタブで開く" openInSideView: "サイドビューで開く" defaultNavigationBehaviour: "デフォルトのナビゲーション" @@ -747,15 +680,14 @@ createNewClip: "新しいクリップを作成" unclip: "クリップ解除" confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?" public: "パブリック" -private: "非公開" i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。" manageAccessTokens: "アクセストークンの管理" accountInfo: "アカウント情報" notesCount: "ノートの数" repliesCount: "返信した数" -renotesCount: "リノートした数" +renotesCount: "Renoteした数" repliedCount: "返信された数" -renotedCount: "リノートされた数" +renotedCount: "Renoteされた数" followingCount: "フォロー数" followersCount: "フォロワー数" sentReactionsCount: "リアクションした数" @@ -772,7 +704,6 @@ lockedAccountInfo: "フォローを承認制にしても、ノートの公開範 alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする" loadRawImages: "添付画像のサムネイルをオリジナル画質にする" disableShowingAnimatedImages: "アニメーション画像を再生しない" -highlightSensitiveMedia: "メディアがセンシティブであることを分かりやすく表示" verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。" notSet: "未設定" emailVerified: "メールアドレスが確認されました" @@ -788,14 +719,14 @@ thisIsExperimentalFeature: "これは実験的な機能です。仕様が変更 developer: "開発者" makeExplorable: "アカウントを見つけやすくする" makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。" +showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示" duplicate: "複製" left: "左" center: "中央" wide: "広い" narrow: "狭い" -reloadToApplySetting: "設定はページリロード後に反映されます。" +reloadToApplySetting: "設定はページリロード後に反映されます。今すぐリロードしますか?" needReloadToApply: "反映には再起動が必要です。" -needToRestartServerToApply: "反映にはサーバーの再起動が必要です。" showTitlebar: "タイトルバーを表示する" clearCache: "キャッシュをクリア" onlineUsersCount: "{n}人がオンライン" @@ -855,7 +786,7 @@ active: "アクティブ" offline: "オフライン" notRecommended: "非推奨" botProtection: "Botプロテクション" -instanceBlocking: "サーバーブロック・サイレンス" +instanceBlocking: "サーバーブロック" selectAccount: "アカウントを選択" switchAccount: "アカウントを切り替え" enabled: "有効" @@ -866,7 +797,6 @@ administration: "管理" accounts: "アカウント" switch: "切り替え" noMaintainerInformationWarning: "管理者情報が設定されていません。" -noInquiryUrlWarning: "問い合わせ先URLが設定されていません。" noBotProtectionWarning: "Botプロテクションが設定されていません。" configure: "設定する" postToGallery: "ギャラリーへ投稿" @@ -926,12 +856,11 @@ makeReactionsPublicDescription: "あなたがしたリアクション一覧を classic: "クラシック" muteThread: "スレッドをミュート" unmuteThread: "スレッドのミュートを解除" -followingVisibility: "フォローの公開範囲" -followersVisibility: "フォロワーの公開範囲" +ffVisibility: "つながりの公開範囲" +ffVisibilityDescription: "自分のフォロー/フォロワー情報の公開範囲を設定できます。" continueThread: "さらにスレッドを見る" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" incorrectPassword: "パスワードが間違っています。" -incorrectTotp: "ワンタイムパスワードが間違っているか、期限切れになっています。" voteConfirm: "「{choice}」に投票しますか?" hide: "隠す" useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示" @@ -956,9 +885,6 @@ oneHour: "1時間" oneDay: "1日" oneWeek: "1週間" oneMonth: "1ヶ月" -threeMonths: "3ヶ月" -oneYear: "1年" -threeDays: "3日" reflectMayTakeTime: "反映されるまで時間がかかる場合があります。" failedToFetchAccountInformation: "アカウント情報の取得に失敗しました" rateLimitExceeded: "レート制限を超えました" @@ -983,7 +909,6 @@ document: "ドキュメント" numberOfPageCache: "ページキャッシュ数" numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。" logoutConfirm: "ログアウトしますか?" -logoutWillClearClientData: "ログアウトするとクライアントの設定情報がブラウザから消去されます。再ログイン時に設定情報を復元できるようにするためには、設定の自動バックアップを有効にしてください。" lastActiveDate: "最終利用日時" statusbar: "ステータスバー" pleaseSelect: "選択してください" @@ -1002,7 +927,6 @@ failedToUpload: "アップロード失敗" cannotUploadBecauseInappropriate: "不適切な内容を含む可能性があると判定されたためアップロードできません。" cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。" -cannotUploadBecauseUnallowedFileType: "許可されていないファイル種別のためアップロードできません。" beta: "ベータ" enableAutoSensitive: "自動センシティブ判定" enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。" @@ -1034,7 +958,6 @@ neverShow: "今後表示しない" remindMeLater: "また後で" didYouLikeMisskey: "Misskeyを気に入っていただけましたか?" pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!" -correspondingSourceIsAvailable: "対応するソースコードは{anchor}から利用可能です。" roles: "ロール" role: "ロール" noRole: "ロールはありません" @@ -1044,12 +967,11 @@ assign: "アサイン" unassign: "アサインを解除" color: "色" manageCustomEmojis: "カスタム絵文字の管理" -manageAvatarDecorations: "アバターデコレーションの管理" youCannotCreateAnymore: "これ以上作成することはできません。" cannotPerformTemporary: "一時的に利用できません" cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。" invalidParamError: "パラメータエラー" -invalidParamErrorDescription: "リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる・許可されていない文字を入力している等の可能性もあります。" +invalidParamErrorDescription: "リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる等の可能性もあります。" permissionDeniedError: "操作が拒否されました" permissionDeniedErrorDescription: "このアカウントにはこの操作を行うための権限がありません。" preset: "プリセット" @@ -1061,8 +983,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑になる可能性があります thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingCancel: "やめる" thisPostMayBeAnnoyingIgnore: "このまま投稿" -collapseRenotes: "リノートのスマート省略" -collapseRenotesDescription: "リアクションやリノートをしたことがあるノートをたたんで表示します。" +collapseRenotes: "見たことのあるRenoteを省略して表示" internalServerError: "サーバー内部エラー" internalServerErrorDescription: "サーバー内部で予期しないエラーが発生しました。" copyErrorInfo: "エラー情報をコピー" @@ -1086,11 +1007,6 @@ resetPasswordConfirm: "パスワードリセットしますか?" sensitiveWords: "センシティブワード" sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" -prohibitedWords: "禁止ワード" -prohibitedWordsDescription: "設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。" -prohibitedWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" -hiddenTags: "非表示ハッシュタグ" -hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。" notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" unfavoriteConfirm: "お気に入り解除しますか?" @@ -1101,15 +1017,11 @@ retryAllQueuesConfirmTitle: "今すぐ再試行しますか?" retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。" enableChartsForRemoteUser: "リモートユーザーのチャートを生成" enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" -enableStatsForFederatedInstances: "リモートサーバーの情報を取得" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" -reactionsDisplaySize: "リアクションの表示サイズ" -limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する" +largeNoteReactions: "ノートのリアクションを大きく表示" noteIdOrUrl: "ノートIDまたはURL" video: "動画" videos: "動画" -audio: "音声" -audioFiles: "音声" dataSaver: "データセーバー" accountMigration: "アカウントの移行" accountMoved: "このユーザーは新しいアカウントに移行しました:" @@ -1119,7 +1031,7 @@ forceShowAds: "常に広告を表示する" addMemo: "メモを追加" editMemo: "メモを編集" reactionsList: "リアクション一覧" -renotesList: "リノート一覧" +renotesList: "Renote一覧" notificationDisplay: "通知の表示" leftTop: "左上" rightTop: "右上" @@ -1130,15 +1042,13 @@ vertical: "縦" horizontal: "横" position: "位置" serverRules: "サーバールール" -pleaseConfirmBelowBeforeSignup: "このサーバーに登録するには、以下の内容を確認し同意する必要があります。" +pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。" pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。" continue: "続ける" preservedUsernames: "予約ユーザー名" preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。" createNoteFromTheFile: "このファイルからノートを作成" archive: "アーカイブ" -archived: "アーカイブ済み" -unarchive: "アーカイブ解除" channelArchiveConfirmTitle: "{name}をアーカイブしますか?" channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。" thisChannelArchived: "このチャンネルはアーカイブされています。" @@ -1149,9 +1059,6 @@ preventAiLearning: "生成AIによる学習を拒否" preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。" options: "オプション" specifyUser: "ユーザー指定" -lookupConfirm: "照会しますか?" -openTagPageConfirm: "ハッシュタグのページを開きますか?" -specifyHost: "ホスト指定" failedToPreviewUrl: "プレビューできません" update: "更新" rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール" @@ -1167,381 +1074,10 @@ branding: "ブランディング" enableServerMachineStats: "サーバーのマシン情報を公開する" enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。" -createInviteCode: "招待コードを作成" -createWithOptions: "オプションを指定して作成" -createCount: "作成数" -inviteCodeCreated: "招待コードを作成しました" -inviteLimitExceeded: "作成できる招待コードの数が上限に達しています。" -createLimitRemaining: "作成できる招待コード: 残り {limit} 個" -inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作成できます。" -expirationDate: "有効期限" -noExpirationDate: "有効期限を設けない" -inviteCodeUsedAt: "招待コードが使用された日時" -registeredUserUsingInviteCode: "招待コードを使用したユーザー" -waitingForMailAuth: "メール認証待ち" -inviteCodeCreator: "招待コードを作成したユーザー" -usedAt: "使用日時" -unused: "未使用" -used: "使用済み" -expired: "期限切れ" -doYouAgree: "同意しますか?" -beSureToReadThisAsItIsImportant: "重要ですので必ずお読みください。" -iHaveReadXCarefullyAndAgree: "「{x}」の内容をよく読み、同意します。" -dialog: "ダイアログ" -icon: "アイコン" -forYou: "あなたへ" -currentAnnouncements: "現在のお知らせ" -pastAnnouncements: "過去のお知らせ" -youHaveUnreadAnnouncements: "未読のお知らせがあります。" -useSecurityKey: "ブラウザまたはデバイスの指示に従って、セキュリティキーまたはパスキーを使用してください。" -replies: "返信" -renotes: "リノート" -loadReplies: "返信を見る" -loadConversation: "会話を見る" -pinnedList: "ピン留めされたリスト" -keepScreenOn: "デバイスの画面を常にオンにする" -verifiedLink: "このリンク先の所有者であることが確認されました" -notifyNotes: "投稿を通知" -unnotifyNotes: "投稿の通知を解除" -authentication: "認証" -authenticationRequiredToContinue: "続けるには認証を行ってください" -dateAndTime: "日時" -showRenotes: "リノートを表示" -edited: "編集済み" -notificationRecieveConfig: "通知の受信設定" -mutualFollow: "相互フォロー" -followingOrFollower: "フォロー中またはフォロワー" -fileAttachedOnly: "ファイル付きのみ" -showRepliesToOthersInTimeline: "TLに他の人への返信を含める" -hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" -showRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めるようにする" -hideRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めないようにする" -confirmShowRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか?" -confirmHideRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか?" -externalServices: "外部サービス" -sourceCode: "ソースコード" -sourceCodeIsNotYetProvided: "ソースコードはまだ提供されていません。この問題の修正について管理者に問い合わせてください。" -repositoryUrl: "リポジトリURL" -repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入します。Misskeyを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://github.com/misskey-dev/misskey と記入します。" -repositoryUrlOrTarballRequired: "リポジトリを公開していない場合、代わりにtarballを提供する必要があります。詳細は.config/example.ymlを参照してください。" -feedback: "フィードバック" -feedbackUrl: "フィードバックURL" -impressum: "運営者情報" -impressumUrl: "運営者情報URL" -impressumDescription: "ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。" -privacyPolicy: "プライバシーポリシー" -privacyPolicyUrl: "プライバシーポリシーURL" -tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" -avatarDecorations: "アイコンデコレーション" -attach: "付ける" -detach: "外す" -detachAll: "全て外す" -angle: "角度" -flip: "反転" -showAvatarDecorations: "アイコンのデコレーションを表示" -releaseToRefresh: "離してリロード" -refreshing: "リロード中" -pullDownToRefresh: "引っ張ってリロード" -useGroupedNotifications: "通知をグルーピング" -signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" -cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" -doReaction: "リアクションする" -code: "コード" -reloadRequiredToApplySettings: "設定の反映にはリロードが必要です。" -remainingN: "残り: {n}" -overwriteContentConfirm: "現在の内容に上書きされますがよろしいですか?" -seasonalScreenEffect: "季節に応じた画面の演出" -decorate: "デコる" -addMfmFunction: "装飾を追加" -enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" -bubbleGame: "バブルゲーム" -sfx: "効果音" -soundWillBePlayed: "サウンドが再生されます" -showReplay: "リプレイを見る" -replay: "リプレイ" -replaying: "リプレイ中" -endReplay: "リプレイを終了" -copyReplayData: "リプレイデータをコピー" -ranking: "ランキング" -lastNDays: "直近{n}日" -backToTitle: "タイトルへ" -hemisphere: "お住まいの地域" -withSensitive: "センシティブなファイルを含むノートを表示" -userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" -enableHorizontalSwipe: "スワイプしてタブを切り替える" -loading: "読み込み中" -surrender: "やめる" -gameRetry: "リトライ" -notUsePleaseLeaveBlank: "使用しない場合は空欄にしてください" -useTotp: "ワンタイムパスワードを使う" -useBackupCode: "バックアップコードを使う" -launchApp: "アプリを起動" -useNativeUIForVideoAudioPlayer: "動画・音声の再生にブラウザのUIを使用する" -keepOriginalFilename: "オリジナルのファイル名を保持" -keepOriginalFilenameDescription: "この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。" -noDescription: "説明文はありません" -alwaysConfirmFollow: "フォローの際常に確認する" -inquiry: "お問い合わせ" -tryAgain: "もう一度お試しください。" -confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する" -sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?" -createdLists: "作成したリスト" -createdAntennas: "作成したアンテナ" -fromX: "{x}から" -genEmbedCode: "埋め込みコードを生成" -noteOfThisUser: "このユーザーのノート一覧" -clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。" -performance: "パフォーマンス" -modified: "変更あり" -discard: "破棄" -thereAreNChanges: "{n}件の変更があります" -signinWithPasskey: "パスキーでログイン" -unknownWebAuthnKey: "登録されていないパスキーです。" -passkeyVerificationFailed: "パスキーの検証に失敗しました。" -passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。" -messageToFollower: "フォロワーへのメッセージ" -target: "対象" -testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。本番環境で使用しないでください。" -prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)" -prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。" -yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています" -yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。" -thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています" -lockdown: "ロックダウン" -pleaseSelectAccount: "アカウントを選択してください" -availableRoles: "利用可能なロール" -acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。" -federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。" -federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" -confirmOnReact: "リアクションする際に確認する" -reactAreYouSure: "\" {emoji} \" をリアクションしますか?" -markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?" -unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?" -preferences: "環境設定" -accessibility: "アクセシビリティ" -preferencesProfile: "設定のプロファイル" -copyPreferenceId: "設定IDをコピー" -resetToDefaultValue: "初期値に戻す" -overrideByAccount: "アカウントで上書き" -untitled: "無題" -noName: "名前はありません" -skip: "スキップ" -restore: "復元" -syncBetweenDevices: "デバイス間で同期" -preferenceSyncConflictTitle: "サーバーに設定値が存在します" -preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どうしますか?" -preferenceSyncConflictChoiceMerge: "統合する" -preferenceSyncConflictChoiceServer: "サーバーの設定値で上書き" -preferenceSyncConflictChoiceDevice: "デバイスの設定値で上書き" -preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル" -paste: "ペースト" -emojiPalette: "絵文字パレット" -postForm: "投稿フォーム" -textCount: "文字数" -information: "情報" -chat: "チャット" -migrateOldSettings: "旧設定情報を移行" -migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。" -compress: "圧縮" -right: "右" -bottom: "下" -top: "上" -embed: "埋め込み" -settingsMigrating: "設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)" -readonly: "読み取り専用" -goToDeck: "デッキへ戻る" -federationJobs: "連合ジョブ" -driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。
\nノートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。
\nファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。
\nフォルダを作って整理することもできます。" -scrollToClose: "スクロールして閉じる" -advice: "アドバイス" -realtimeMode: "リアルタイムモード" -turnItOn: "オンにする" -turnItOff: "オフにする" -emojiMute: "絵文字ミュート" -emojiUnmute: "絵文字ミュート解除" -muteX: "{x}をミュート" -unmuteX: "{x}のミュートを解除" -abort: "中止" -tip: "ヒントとコツ" -redisplayAllTips: "全ての「ヒントとコツ」を再表示" -hideAllTips: "全ての「ヒントとコツ」を非表示" - -_chat: - noMessagesYet: "まだメッセージはありません" - newMessage: "新しいメッセージ" - individualChat: "個人チャット" - individualChat_description: "特定ユーザーとの一対一のチャットができます。" - roomChat: "ルームチャット" - roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。" - createRoom: "ルームを作成" - inviteUserToChat: "ユーザーを招待してチャットを始めましょう" - yourRooms: "作成したルーム" - joiningRooms: "参加中のルーム" - invitations: "招待" - noInvitations: "招待はありません" - history: "履歴" - noHistory: "履歴はありません" - noRooms: "ルームはありません" - inviteUser: "ユーザーを招待" - sentInvitations: "送信した招待" - join: "参加" - ignore: "無視" - leave: "ルームから退出" - members: "メンバー" - searchMessages: "メッセージを検索" - home: "ホーム" - send: "送信" - newline: "改行" - muteThisRoom: "このルームをミュート" - deleteRoom: "ルームを削除" - chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。" - chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。" - chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。" - cannotChatWithTheUser: "このユーザーとのチャットを開始できません" - cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。" - youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。" - doYouAcceptInvitation: "招待を承認しますか?" - chatWithThisUser: "チャットする" - thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。" - thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。" - thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。" - thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。" - chatAllowedUsers: "チャットを許可する相手" - chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。" - _chatAllowedUsers: - everyone: "誰でも" - followers: "自分のフォロワーのみ" - following: "自分がフォローしているユーザーのみ" - mutual: "相互フォローのユーザーのみ" - none: "誰も許可しない" - -_emojiPalette: - palettes: "パレット" - enableSyncBetweenDevicesForPalettes: "パレットのデバイス間同期を有効にする" - paletteForMain: "メインで使用するパレット" - paletteForReaction: "リアクションで使用するパレット" - -_settings: - driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。" - pluginBanner: "プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。" - notificationsBanner: "サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。" - api: "API" - webhook: "Webhook" - serviceConnection: "サービス連携" - serviceConnectionBanner: "外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。" - accountData: "アカウントのデータ" - accountDataBanner: "アカウントデータのアーカイブをエクスポート/インポートして管理できます。" - muteAndBlockBanner: "非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。" - accessibilityBanner: "クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。" - privacyBanner: "コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。" - securityBanner: "パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。" - preferencesBanner: "好みに応じた、クライアントの全体的な動作の設定が行えます。" - appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。" - soundsBanner: "クライアントで再生するサウンドの設定が行えます。" - timelineAndNote: "タイムラインとノート" - makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする" - makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。" - useStickyIcons: "アイコンをスクロールに追従させる" - enableHighQualityImagePlaceholders: "高品質な画像のプレースホルダを表示" - uiAnimations: "UIのアニメーション" - showNavbarSubButtons: "ナビゲーションバーに副ボタンを表示" - ifOn: "オンのとき" - ifOff: "オフのとき" - enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期" - enablePullToRefresh: "ひっぱって更新" - enablePullToRefresh_description: "マウスでは、ホイールを押し込みながらドラッグします。" - realtimeMode_description: "サーバーと接続を確立し、リアルタイムでコンテンツを更新します。通信量とバッテリーの消費が多くなる場合があります。" - contentsUpdateFrequency: "コンテンツの取得頻度" - contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。" - contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。" - showUrlPreview: "URLプレビューを表示する" - - _chat: - showSenderName: "送信者の名前を表示" - sendOnEnter: "Enterで送信" - -_preferencesProfile: - profileName: "プロファイル名" - profileNameDescription: "このデバイスを識別する名前を設定してください。" - profileNameDescription2: "例: 「メインPC」、「スマホ」など" - manageProfiles: "プロファイルの管理" - -_preferencesBackup: - autoBackup: "自動バックアップ" - restoreFromBackup: "バックアップから復元" - noBackupsFoundTitle: "バックアップが見つかりませんでした" - noBackupsFoundDescription: "自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。" - selectBackupToRestore: "復元するバックアップを選択してください" - youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効にするにはプロファイル名の設定が必要です。" - autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になっていません。" - backupFound: "設定のバックアップが見つかりました" - -_accountSettings: - requireSigninToViewContents: "コンテンツの表示にログインを必須にする" - requireSigninToViewContentsDescription1: "あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーに情報が収集されるのを防ぐ効果が期待できます。" - requireSigninToViewContentsDescription2: "URLプレビュー(OGP)、Webページへの埋め込み、ノートの引用に対応していないサーバーからの表示も不可になります。" - requireSigninToViewContentsDescription3: "リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。" - makeNotesFollowersOnlyBefore: "過去のノートをフォロワーのみ表示可能にする" - makeNotesFollowersOnlyBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートがフォロワーのみ表示可能になります。無効に戻すと、ノートの公開状態も元に戻ります。" - makeNotesHiddenBefore: "過去のノートを非公開化する" - makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。" - mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。" - mayNotEffectSomeSituations: "これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。" - notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート" - notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート" - -_abuseUserReport: - forward: "転送" - forwardDescription: "匿名のシステムアカウントとして、リモートサーバーに通報を転送します。" - resolve: "解決" - accept: "是認" - reject: "否認" - resolveTutorial: "内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。\n内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。" - -_delivery: - status: "配信状態" - stop: "配信停止" - resume: "配信再開" - _type: - none: "配信中" - manuallySuspended: "手動停止中" - goneSuspended: "サーバー削除のため停止中" - autoSuspendedForNotResponding: "サーバー応答なしのため停止中" - softwareSuspended: "配信停止中のソフトウェアであるため停止中" - -_bubbleGame: - howToPlay: "遊び方" - hold: "ホールド" - _score: - score: "スコア" - scoreYen: "稼いだ金額" - highScore: "ハイスコア" - maxChain: "最大チェーン数" - yen: "{yen}円" - estimatedQty: "{qty}個分" - scoreSweets: "おにぎり {onigiriQtyWithUnit}" - _howToPlay: - section1: "位置を調整してハコにモノを落とします。" - section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。" - section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!" - -_announcement: - forExistingUsers: "既存ユーザーのみ" - forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。" - needConfirmationToRead: "既読にするのに確認が必要" - needConfirmationToReadDescription: "有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。また、一括既読操作の対象になりません。" - end: "お知らせを終了" - tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。" - readConfirmTitle: "既読にしますか?" - readConfirmText: "「{title}」の内容を読み、既読にします。" - shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、常時掲示するための情報ではなく、即時性が求められる情報の掲示のためにお知らせを使用することを推奨します。" - dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。" - silence: "非通知" - silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。" _initialAccountSetting: accountCreated: "アカウントの作成が完了しました!" - letsStartAccountSetup: "さっそくアカウントの初期設定を行いましょう。" + letsStartAccountSetup: "アカウントの初期設定を行いましょう。" letsFillYourProfile: "まずはあなたのプロフィールを設定しましょう。" profileSetting: "プロフィール設定" privacySetting: "プライバシー設定" @@ -1551,120 +1087,13 @@ _initialAccountSetting: pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。" initialAccountSettingCompleted: "初期設定が完了しました!" haveFun: "{name}をお楽しみください!" - youCanContinueTutorial: "このまま{name}(Misskey)の使い方についてのチュートリアルに進むこともできますが、ここで中断してすぐに使い始めることもできます。" - startTutorial: "チュートリアルを開始" + ifYouNeedLearnMore: "{name}(Misskey)の使い方などを詳しく知るには{link}をご覧ください。" skipAreYouSure: "初期設定をスキップしますか?" laterAreYouSure: "初期設定をあとでやり直しますか?" -_initialTutorial: - launchTutorial: "チュートリアルを見る" - title: "チュートリアル" - wellDone: "よくできました" - skipAreYouSure: "チュートリアルを終了しますか?" - _landing: - title: "チュートリアルへようこそ" - description: "ここでは、Misskeyの基本的な使い方や機能を確認できます。" - _note: - title: "ノートって何?" - description: "Misskeyでの投稿は「ノート」と呼びます。ノートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。" - reply: "返信することができます。返信に対しての返信も可能で、スレッドのように会話を続けることもできます。" - renote: "そのノートを自分のタイムラインに流して共有することができます。テキストを追加して引用することも可能です。" - reaction: "リアクションをつけることができます。詳しくは次のページで解説します。" - menu: "ノートの詳細を表示したり、リンクをコピーしたりなどの様々な操作が行えます。" - _reaction: - title: "リアクションって何?" - description: "ノートには「リアクション」をつけることができます。「いいね」では伝わらないニュアンスも、リアクションで簡単・気軽に表現できます。" - letsTryReacting: "リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!" - reactToContinue: "リアクションをつけると先に進めるようになります。" - reactNotification: "あなたのノートが誰かにリアクションされると、リアルタイムで通知を受け取ります。" - reactDone: "「ー」ボタンを押すとリアクションを取り消すことができます。" - _timeline: - title: "タイムラインのしくみ" - description1: "Misskeyには、使い方に応じて複数のタイムラインが用意されています(サーバーによってはいずれかが無効になっていることがあります)。" - home: "あなたがフォローしているアカウントの投稿を見られます。" - local: "このサーバーにいるユーザー全員の投稿を見られます。" - social: "ホームタイムラインとローカルタイムラインの投稿が両方表示されます。" - global: "接続している他のすべてのサーバーからの投稿を見られます。" - description2: "それぞれのタイムラインは、画面上部でいつでも切り替えられます。" - description3: "その他にも、リストタイムラインやチャンネルタイムラインなどがあります。詳しくは{link}をご覧ください。" - _postNote: - title: "ノートの投稿設定" - description1: "Misskeyにノートを投稿する際には、様々なオプションの設定が可能です。投稿フォームはこのようになっています。" - _visibility: - description: "ノートを表示できる相手を制限できます。" - public: "すべてのユーザーに公開。" - home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。" - followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。" - direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。" - doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。" - doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。" - localOnly: "他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。" - _cw: - title: "内容を隠す(CW)" - description: "本文のかわりに「注釈」に書いた内容が表示されます。「もっと見る」を押すと本文が表示されます。" - _exampleNote: - cw: "飯テロ注意" - note: "チョコのかかったドーナツを食べました🍩😋" - useCases: "サーバーのガイドラインにより必要とされるノートに指定したり、ネタバレ投稿やセンシティブな文章を自主規制したりするときに使います。" - _howToMakeAttachmentsSensitive: - title: "添付ファイルをセンシティブにするには?" - description: "サーバーのガイドラインにより必要とされる際や、そのまま見れる状態にしておくべきではない添付ファイルには、「センシティブ」設定を付けます。" - tryThisFile: "試しに、このフォームに添付された画像をセンシティブにしてみてください!" - _exampleNote: - note: "納豆のフタ開けるのミスったわね…" - method: "添付ファイルをセンシティブにする際は、そのファイルをクリックしてメニューを開き、「センシティブとして設定」をクリックします。" - sensitiveSucceeded: "ファイルを添付する際は、サーバーのガイドラインに従ってセンシティブを適切に設定してください。" - doItToContinue: "画像をセンシティブに設定すると先に進めるようになります。" - _done: - title: "チュートリアルは終了です🎉" - description: "ここで紹介した機能はほんの一部にすぎません。Misskeyの使い方をより詳しく知るには、{link}をご覧ください。" - -_timelineDescription: - home: "ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。" - local: "ローカルタイムラインでは、このサーバーにいるユーザー全員の投稿を見られます。" - social: "ソーシャルタイムラインには、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。" - global: "グローバルタイムラインでは、接続している他のすべてのサーバーからの投稿を見られます。" - _serverRules: description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。" -_serverSettings: - iconUrl: "アイコン画像のURL" - appIconDescription: "{host}がアプリとして表示される際のアイコンを指定します。" - appIconUsageExample: "例: PWAや、スマートフォンのホーム画面にブックマークとして追加された時など" - appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるため、塗り潰された余白のある背景を持つことが推奨されます。" - appIconResolutionMustBe: "解像度は必ず{resolution}である必要があります。" - manifestJsonOverride: "manifest.jsonのオーバーライド" - shortName: "略称" - shortNameDescription: "サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。" - fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。" - fanoutTimelineDbFallback: "データベースへのフォールバック" - fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。" - reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。" - inquiryUrl: "問い合わせ先URL" - inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。" - openRegistration: "アカウントの作成をオープンにする" - openRegistrationWarning: "登録を開放することはリスクが伴います。サーバーを常に監視し、トラブルが発生した際にすぐに対応できる体制がある場合のみオンにすることを推奨します。" - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。" - deliverSuspendedSoftware: "配信停止中のソフトウェア" - deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。" - singleUserMode: "お一人様モード" - singleUserMode_description: "このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。" - signToActivityPubGet: "GETリクエストに署名する" - signToActivityPubGet_description: "通常は有効にしてください。連合の通信に関する問題がある場合に、無効にすると改善することがありますが、逆にサーバーによっては通信が不可になることがあります。" - proxyRemoteFiles: "リモートファイルをプロキシする" - proxyRemoteFiles_description: "有効にすると、リモートのファイルをプロキシして提供します。画像のサムネイル生成やユーザーのプライバシー保護に役立ちます。" - allowExternalApRedirect: "ActivityPub経由の照会にリダイレクトを許可する" - allowExternalApRedirect_description: "有効にすると、他のサーバーがこのサーバーを通して第三者のコンテンツを照会することが可能になりますが、コンテンツのなりすましが発生する可能性があります。" - userGeneratedContentsVisibilityForVisitor: "非利用者に対するユーザー作成コンテンツの公開範囲" - userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。" - userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。" - - _userGeneratedContentsVisibilityForVisitor: - all: "全て公開" - localOnly: "ローカルコンテンツのみ公開し、リモートコンテンツは非公開" - none: "全て非公開" - _accountMigration: moveFrom: "別のアカウントからこのアカウントに移行" moveFromSub: "別のアカウントへエイリアスを作成" @@ -1920,19 +1349,6 @@ _achievements: title: "Brain Diver" description: "Brain Diverへのリンクを投稿した" flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "テスト過剰" - description: "通知のテストをごく短時間のうちに連続して行った" - _tutorialCompleted: - title: "Misskey初心者講座 修了証" - description: "チュートリアルを完了した" - _bubbleGameExplodingHead: - title: "🤯" - description: "バブルゲームで最も大きいモノを出した" - _bubbleGameDoubleExplodingHead: - title: "ダブル🤯" - description: "バブルゲームで最も大きいモノを2つ同時に出した" - flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて" _role: new: "ロールの作成" @@ -1944,9 +1360,7 @@ _role: assignTarget: "アサイン" descriptionOfAssignTarget: "マニュアルは誰がこのロールに含まれるかを手動で管理します。\nコンディショナルは条件を設定し、それに合致するユーザーが自動で含まれるようになります。" manual: "マニュアル" - manualRoles: "マニュアルロール" conditional: "コンディショナル" - conditionalRoles: "コンディショナルロール" condition: "条件" isConditionalRole: "これはコンディショナルロールです。" isPublic: "公開ロール" @@ -1963,8 +1377,6 @@ _role: descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。" displayOrder: "表示順" descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。" - preserveAssignmentOnMoveAccount: "アサイン状態を移行先アカウントにも引き継ぐ" - preserveAssignmentOnMoveAccount_description: "オンにすると、このロールが付与されたアカウントが移行された際に、移行先アカウントにもこのロールが引き継がれるようになります。" canEditMembersByModerator: "モデレーターのメンバー編集を許可" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" priority: "優先度" @@ -1976,17 +1388,10 @@ _role: gtlAvailable: "グローバルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" - mentionMax: "ノート内の最大メンション数" canInvite: "サーバー招待コードの発行" - inviteLimit: "招待コードの作成可能数" - inviteLimitCycle: "招待コードの発行間隔" - inviteExpirationTime: "招待コードの有効期限" canManageCustomEmojis: "カスタム絵文字の管理" - canManageAvatarDecorations: "アバターデコレーションの管理" driveCapacity: "ドライブ容量" - maxFileSize: "アップロード可能な最大ファイルサイズ" alwaysMarkNsfw: "ファイルにNSFWを常に付与" - canUpdateBioMedia: "アイコンとバナーの更新を許可" pinMax: "ノートのピン留めの最大数" antennaMax: "アンテナの作成可能数" wordMuteMax: "ワードミュートの最大文字数" @@ -1998,27 +1403,10 @@ _role: rateLimitFactor: "レートリミット" descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" canHideAds: "広告の非表示" - canSearchNotes: "ノート検索の利用" - canUseTranslator: "翻訳機能の利用" - avatarDecorationLimit: "アイコンデコレーションの最大取付個数" - canImportAntennas: "アンテナのインポートを許可" - canImportBlocking: "ブロックのインポートを許可" - canImportFollowing: "フォローのインポートを許可" - canImportMuting: "ミュートのインポートを許可" - canImportUserLists: "リストのインポートを許可" - chatAvailability: "チャットを許可" - uploadableFileTypes: "アップロード可能なファイル種別" - uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)" - uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。" + canSearchNotes: "ノート検索の利用可否" _condition: - roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" isRemote: "リモートユーザー" - isCat: "猫ユーザー" - isBot: "botユーザー" - isSuspended: "サスペンド済みユーザー" - isLocked: "鍵アカウントユーザー" - isExplorable: "「アカウントを見つけやすくする」が有効なユーザー" createdLessThan: "アカウント作成から~以内" createdMoreThan: "アカウント作成から~経過" followersLessThanOrEq: "フォロワー数が~以下" @@ -2046,7 +1434,6 @@ _emailUnavailable: disposable: "恒久的に使用可能なアドレスではありません" mx: "正しいメールサーバーではありません" smtp: "メールサーバーが応答しません" - banned: "このメールアドレスでは登録できません" _ffVisibility: public: "公開" @@ -2056,7 +1443,7 @@ _ffVisibility: _signup: almostThere: "ほとんど完了です" emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。" - emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。" + emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。" _accountDelete: accountDelete: "アカウントの削除" @@ -2071,10 +1458,6 @@ _ad: reduceFrequencyOfThisAd: "この広告の表示頻度を下げる" hide: "表示しない" timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。" - adsSettings: "広告配信設定" - notesPerOneAd: "リアルタイム更新中に広告を配信する間隔(ノートの個数)" - setZeroToDisable: "0でリアルタイム更新時の広告配信を無効" - adsTooClose: "広告の配信間隔が極めて短いため、ユーザー体験が著しく損われる可能性があります。" _forgotPassword: enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" @@ -2097,8 +1480,6 @@ _plugin: install: "プラグインのインストール" installWarn: "信頼できないプラグインはインストールしないでください。" manage: "プラグインの管理" - viewSource: "ソースを表示" - viewLog: "ログを表示" _preferencesBackups: list: "作成したバックアップ" @@ -2128,16 +1509,13 @@ _registry: _aboutMisskey: about: "Misskeyはsyuiloによって2014年から開発されている、オープンソースのソフトウェアです。" - contributors: "コントリビューター" + contributors: "主なコントリビューター" allContributors: "全てのコントリビューター" source: "ソースコード" - original: "オリジナル" - thisIsModifiedVersion: "{name}はオリジナルのMisskeyを改変したバージョンを使用しています。" translation: "Misskeyを翻訳" donate: "Misskeyに寄付" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" patrons: "支援者" - projectMembers: "プロジェクトメンバー" _displayOfSensitiveMedia: respect: "センシティブ設定されたメディアを隠す" @@ -2166,7 +1544,6 @@ _channel: notesCount: "{n}投稿があります" nameAndDescription: "名前と説明" nameOnly: "名前のみ" - allowRenoteToExternal: "チャンネル外へのリノートと引用リノートを許可する" _menuDisplay: sideFull: "横" @@ -2178,6 +1555,11 @@ _wordMute: muteWords: "ミュートするワード" muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。" muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。" + softDescription: "指定した条件のノートをタイムラインから隠します。" + hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。" + soft: "ソフト" + hard: "ハード" + mutedNotes: "ミュートされたノート" _instanceMute: instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。" @@ -2194,7 +1576,6 @@ _theme: installed: "{name}をインストールしました" installedThemes: "インストールされたテーマ" builtinThemes: "標準のテーマ" - instanceTheme: "サーバーのテーマ" alreadyInstalled: "そのテーマは既にインストールされています" invalid: "テーマの形式が間違っています" make: "テーマを作る" @@ -2226,15 +1607,16 @@ _theme: panel: "パネル" shadow: "影" header: "ヘッダー" - navBg: "ナビゲーションバーの背景" - navFg: "ナビゲーションバーの文字" - navActive: "ナビゲーションバー文字(アクティブ)" - navIndicator: "ナビゲーションバーのインジケーター" + navBg: "サイドバーの背景" + navFg: "サイドバーの文字" + navHoverFg: "サイドバー文字(ホバー)" + navActive: "サイドバー文字(アクティブ)" + navIndicator: "サイドバーのインジケーター" link: "リンク" hashtag: "ハッシュタグ" mention: "メンション" mentionMe: "あなた宛てメンション" - renote: "リノート" + renote: "Renote" modalBg: "モーダルの背景" divider: "分割線" scrollbarHandle: "スクロールバーの取っ手" @@ -2244,30 +1626,31 @@ _theme: infoFg: "情報の文字" infoWarnBg: "警告の背景" infoWarnFg: "警告の文字" + cwBg: "CW ボタンの背景" + cwFg: "CW ボタンの文字" + cwHoverBg: "CW ボタンの背景 (ホバー)" toastBg: "通知トーストの背景" toastFg: "通知トーストの文字" buttonBg: "ボタンの背景" buttonHoverBg: "ボタンの背景 (ホバー)" inputBorder: "入力ボックスの縁取り" + listItemHoverBg: "リスト項目の背景 (ホバー)" + driveFolderBg: "ドライブフォルダーの背景" + wallpaperOverlay: "壁紙のオーバーレイ" badge: "バッジ" messageBg: "チャットの背景" + accentDarken: "アクセント (暗め)" + accentLighten: "アクセント (明るめ)" fgHighlighted: "強調された文字" _sfx: note: "ノート" noteMy: "ノート(自分)" notification: "通知" - reaction: "リアクション選択時" - chatMessage: "チャットのメッセージ" - -_soundSettings: - driveFile: "ドライブの音声を使用" - driveFileWarn: "ドライブのファイルを選択してください" - driveFileTypeWarn: "このファイルは対応していません" - driveFileTypeWarnDescription: "音声ファイルを選択してください" - driveFileDurationWarn: "音声が長すぎます" - driveFileDurationWarnDescription: "長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?" - driveFileError: "音声が読み込めませんでした。設定を変更してください" + chat: "チャット" + chatBg: "チャット(バックグラウンド)" + antenna: "アンテナ受信" + channel: "チャンネル通知" _ago: future: "未来" @@ -2279,16 +1662,7 @@ _ago: weeksAgo: "{n}週間前" monthsAgo: "{n}ヶ月前" yearsAgo: "{n}年前" - invalid: "日時の解析に失敗" - -_timeIn: - seconds: "{n}秒後" - minutes: "{n}分後" - hours: "{n}時間後" - days: "{n}日後" - weeks: "{n}週間後" - months: "{n}ヶ月後" - years: "{n}年後" + invalid: "ありません" _time: second: "秒" @@ -2296,19 +1670,32 @@ _time: hour: "時間" day: "日" +_timelineTutorial: + title: "Misskeyの使い方" + step1_1: "この画面は「タイムライン」です。{name}に投稿された「ノート」が時系列で表示されます。" + step1_2: "タイムラインにはいくつか種類があり、例えば「ホームタイムライン」にはあなたがフォローしている人のノートが流れ、「ローカルタイムライン」には{name}全体のノートが流れます。" + step2_1: "試しに、何かノートを投稿してみましょう。画面上にある鉛筆マークのボタンを押すとフォームが開きます。" + step2_2: "初めてのノートの内容は、あなたの自己紹介や「{name}始めました」などがおすすめです。" + step3_1: "投稿できましたか?" + step3_2: "あなたのノートがタイムラインに表示されていれば成功です。" + step4_1: "ノートには、「リアクション」を付けることができます。" + step4_2: "リアクションを付けるには、ノートの「+」マークをクリックして、好きな絵文字を選択します。" + _2fa: alreadyRegistered: "既に設定は完了しています。" registerTOTP: "認証アプリの設定を開始" + passwordToTOTP: "パスワードを入力してください" step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。" - step2: "次に、表示されているQRコードをアプリでスキャンするか、ボタンをクリックして端末上でアプリを開きます。" - step2Uri: "デスクトップアプリを使用する場合は次のURIを入力します" + step2: "次に、表示されているQRコードをアプリでスキャンします。" + step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。" + step2Url: "デスクトップアプリでは次のURIを入力します:" step3Title: "確認コードを入力" - step3: "アプリに表示されている確認コード(トークン)を入力します。" - setupCompleted: "設定が完了しました" - step4: "これからログインするときも、同じようにコードを入力します。" + step3: "アプリに表示されている確認コード(トークン)を入力して完了です。" + step4: "これからログインするときも、同じように確認コードを入力します。" securityKeyNotSupported: "お使いのブラウザはセキュリティキーに対応していません。" registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するには、まず認証アプリの設定を行なってください。" securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキー、端末の生体認証やPINロック、パスキーといった、WebAuthn由来の鍵を登録します。" + chromePasskeyNotSupported: "Chromeのパスキーは現在サポートしていません。" registerSecurityKey: "セキュリティキー・パスキーを登録する" securityKeyName: "キーの名前を入力" tapSecurityKey: "ブラウザの指示に従い、セキュリティキーやパスキーを登録してください" @@ -2316,15 +1703,9 @@ _2fa: removeKeyConfirm: "{name}を削除しますか?" whyTOTPOnlyRenew: "セキュリティキーが登録されている場合、認証アプリの設定は解除できません。" renewTOTP: "認証アプリを再設定" - renewTOTPConfirm: "今までの認証アプリの確認コードおよびバックアップコードは使用できなくなります" + renewTOTPConfirm: "今までの認証アプリの確認コードは使用できなくなります" renewTOTPOk: "再設定する" renewTOTPCancel: "やめておく" - checkBackupCodesBeforeCloseThisWizard: "このウィザードを閉じる前に、以下のバックアップコードを確認してください。" - backupCodes: "バックアップコード" - backupCodesDescription: "認証アプリが使用できなくなった場合、以下のバックアップコードを使ってアカウントにアクセスできます。これらのコードは必ず安全な場所に保管してください。各コードは一回だけ使用できます。" - backupCodeUsedWarning: "バックアップコードが使用されました。認証アプリが使えなくなっている場合、なるべく早く認証アプリを再設定してください。" - backupCodesExhaustedWarning: "バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。" - moreDetailedGuideHere: "詳細なガイドはこちら" _permissions: "read:account": "アカウントの情報を見る" @@ -2359,60 +1740,6 @@ _permissions: "write:gallery": "ギャラリーを操作する" "read:gallery-likes": "ギャラリーのいいねを見る" "write:gallery-likes": "ギャラリーのいいねを操作する" - "read:flash": "Playを見る" - "write:flash": "Playを操作する" - "read:flash-likes": "Playのいいねを見る" - "write:flash-likes": "Playのいいねを操作する" - "read:admin:abuse-user-reports": "ユーザーからの通報を見る" - "write:admin:delete-account": "ユーザーアカウントを削除する" - "write:admin:delete-all-files-of-a-user": "ユーザーのすべてのファイルを削除する" - "read:admin:index-stats": "データベースインデックスに関する情報を見る" - "read:admin:table-stats": "データベーステーブルに関する情報を見る" - "read:admin:user-ips": "ユーザーのIPアドレスを見る" - "read:admin:meta": "インスタンスのメタデータを見る" - "write:admin:reset-password": "ユーザーのパスワードをリセットする" - "write:admin:resolve-abuse-user-report": "ユーザーからの通報を解決する" - "write:admin:send-email": "メールを送る" - "read:admin:server-info": "サーバーの情報を見る" - "read:admin:show-moderation-log": "モデレーションログを見る" - "read:admin:show-user": "ユーザーのプライベートな情報を見る" - "write:admin:suspend-user": "ユーザーを凍結する" - "write:admin:unset-user-avatar": "ユーザーのアバターを削除する" - "write:admin:unset-user-banner": "ユーザーのバーナーを削除する" - "write:admin:unsuspend-user": "ユーザーの凍結を解除する" - "write:admin:meta": "インスタンスのメタデータを操作する" - "write:admin:user-note": "モデレーションノートを操作する" - "write:admin:roles": "ロールを操作する" - "read:admin:roles": "ロールを見る" - "write:admin:relays": "リレーを操作する" - "read:admin:relays": "リレーを見る" - "write:admin:invite-codes": "招待コードを操作する" - "read:admin:invite-codes": "招待コードを見る" - "write:admin:announcements": "お知らせを操作する" - "read:admin:announcements": "お知らせを見る" - "write:admin:avatar-decorations": "アバターデコレーションを操作する" - "read:admin:avatar-decorations": "アバターデコレーションを見る" - "write:admin:federation": "連合に関する情報を操作する" - "write:admin:account": "ユーザーアカウントを操作する" - "read:admin:account": "ユーザーに関する情報を見る" - "write:admin:emoji": "絵文字を操作する" - "read:admin:emoji": "絵文字を見る" - "write:admin:queue": "ジョブキューを操作する" - "read:admin:queue": "ジョブキューに関する情報を見る" - "write:admin:promo": "プロモーションノートを操作する" - "write:admin:drive": "ユーザーのドライブを操作する" - "read:admin:drive": "ユーザーのドライブの関する情報を見る" - "read:admin:stream": "管理者用のWebsocket APIを使う" - "write:admin:ad": "広告を操作する" - "read:admin:ad": "広告を見る" - "write:invite-codes": "招待コードを作成する" - "read:invite-codes": "招待コードを取得する" - "write:clip-favorite": "クリップのいいねを操作する" - "read:clip-favorite": "クリップのいいねを見る" - "read:federation": "連合に関する情報を取得する" - "write:report-abuse": "違反を報告する" - "write:chat": "チャットを操作する" - "read:chat": "チャットを閲覧する" _auth: shareAccessTitle: "アプリへのアクセス許可" @@ -2422,18 +1749,14 @@ _auth: permissionAsk: "このアプリは次の権限を要求しています" pleaseGoBack: "アプリケーションに戻ってやっていってください" callback: "アプリケーションに戻っています" - accepted: "アクセスを許可しました" denied: "アクセスを拒否しました" - scopeUser: "以下のユーザーとして操作しています" pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。" - byClickingYouWillBeRedirectedToThisUrl: "アクセスを許可すると、自動で以下のURLに遷移します" _antennaSources: all: "全てのノート" homeTimeline: "フォローしているユーザーのノート" users: "指定した一人または複数のユーザーのノート" userList: "指定したリストのユーザーのノート" - userBlacklist: "指定した一人または複数のユーザーを除いた全てのノート" _weekday: sunday: "日曜日" @@ -2474,8 +1797,6 @@ _widgets: _userList: chooseList: "リストを選択" clicker: "クリッカー" - birthdayFollowings: "今日誕生日のユーザー" - chat: "チャット" _cw: hide: "隠す" @@ -2542,23 +1863,16 @@ _profile: metadataContent: "内容" changeAvatar: "アイコン画像を変更" changeBanner: "バナー画像を変更" - verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" - avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。" - followedMessage: "フォローされた時のメッセージ" - followedMessageDescription: "フォローされた時に相手に表示する短いメッセージを設定できます。" - followedMessageDescriptionForLockedAccount: "フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。" _exportOrImport: allNotes: "全てのノート" favoritedNotes: "お気に入りにしたノート" - clips: "クリップ" followingList: "フォロー" muteList: "ミュート" blockingList: "ブロック" userLists: "リスト" excludeMutingUsers: "ミュートしているユーザーを除外" excludeInactiveUsers: "使われていないアカウントを除外" - withReplies: "返信をTLに含むかの情報がファイルにない場合に、インポートした人による返信をTLに含むようにする" _charts: federation: "連合" @@ -2609,12 +1923,14 @@ _play: title: "タイトル" script: "スクリプト" summary: "説明" - visibilityDescription: "非公開に設定するとプロフィールに表示されなくなりますが、URLを知っている人は引き続きアクセスできます。" _pages: newPage: "ページの作成" editPage: "ページの編集" readPage: "ソースを表示中" + created: "ページを作成しました" + updated: "ページを更新しました" + deleted: "ページを削除しました" pageSetting: "ページ設定" nameAlreadyExists: "指定されたページURLは既に存在しています" invalidNameTitle: "不正なページURLです" @@ -2642,7 +1958,6 @@ _pages: eyeCatchingImageSet: "アイキャッチ画像を設定" eyeCatchingImageRemove: "アイキャッチ画像を削除" chooseBlock: "ブロックを追加" - enterSectionTitle: "セクションタイトルを入力" selectType: "種類を選択" contentBlocks: "コンテンツ" inputBlocks: "入力" @@ -2653,8 +1968,6 @@ _pages: section: "セクション" image: "画像" button: "ボタン" - dynamic: "動的ブロック" - dynamicDescription: "このブロックは廃止されています。今後は{play}を利用してください。" note: "ノート埋め込み" _note: @@ -2672,65 +1985,38 @@ _notification: youGotMention: "{name}からのメンション" youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" - youRenoted: "{name}がリノートしました" + youRenoted: "{name}がRenoteしました" youWereFollowed: "フォローされました" youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" pollEnded: "アンケートの結果が出ました" - newNote: "新しい投稿" unreadAntennaNote: "アンテナ {name}" - roleAssigned: "ロールが付与されました" - chatRoomInvitationReceived: "チャットルームへ招待されました" emptyPushNotificationMessage: "プッシュ通知の更新をしました" achievementEarned: "実績を獲得" - testNotification: "通知テスト" - checkNotificationBehavior: "通知の表示を確かめる" - sendTestNotification: "テスト通知を送信する" - notificationWillBeDisplayedLikeThis: "通知はこのように表示されます" - reactedBySomeUsers: "{n}人がリアクションしました" - likedBySomeUsers: "{n}人がいいねしました" - renotedBySomeUsers: "{n}人がリノートしました" - followedBySomeUsers: "{n}人にフォローされました" - flushNotification: "通知の履歴をリセットする" - exportOfXCompleted: "{x}のエクスポートが完了しました" - login: "ログインがありました" - createToken: "アクセストークンが作成されました" - createTokenDescription: "心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。" _types: all: "すべて" - note: "ユーザーの新規投稿" follow: "フォロー" mention: "メンション" reply: "リプライ" - renote: "リノート" + renote: "Renote" quote: "引用" reaction: "リアクション" pollEnded: "アンケートが終了" receiveFollowRequest: "フォロー申請を受け取った" followRequestAccepted: "フォローが受理された" - roleAssigned: "ロールが付与された" - chatRoomInvitationReceived: "チャットルームへ招待された" achievementEarned: "実績の獲得" - exportCompleted: "エクスポートが完了した" - login: "ログイン" - createToken: "アクセストークンの作成" - test: "通知のテスト" app: "連携アプリからの通知" _actions: followBack: "フォローバック" reply: "返信" - renote: "リノート" + renote: "Renote" _deck: alwaysShowMainColumn: "常にメインカラムを表示" columnAlign: "カラムの寄せ" - columnGap: "カラム間のマージン" - deckMenuPosition: "デッキメニューの位置" - navbarPosition: "ナビゲーションバーの位置" addColumn: "カラムを追加" - newNoteNotificationSettings: "新着ノート通知の設定" configureColumn: "カラムの設定" swapLeft: "左に移動" swapRight: "右に移動" @@ -2742,12 +2028,8 @@ _deck: newProfile: "新規プロファイル" deleteProfile: "プロファイルを削除" introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!" - introduction2: "カラムを追加するには、画面の + をクリックします。" + introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。" widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください" - useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示" - usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります" - flexible: "幅を自動調整" - enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする" _columns: main: "メイン" @@ -2760,7 +2042,6 @@ _deck: mentions: "あなた宛て" direct: "ダイレクト" roleTimeline: "ロールタイムライン" - chat: "チャット" _dialog: charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}" @@ -2776,10 +2057,9 @@ _drivecleaner: _webhookSettings: createWebhook: "Webhookを作成" - modifyWebhook: "Webhookを編集" name: "名前" secret: "シークレット" - trigger: "トリガー" + events: "Webhookを実行するタイミング" active: "有効" _events: follow: "フォローしたとき" @@ -2789,432 +2069,3 @@ _webhookSettings: renote: "Renoteされたとき" reaction: "リアクションがあったとき" mention: "メンションされたとき" - _systemEvents: - abuseReport: "ユーザーから通報があったとき" - abuseReportResolved: "ユーザーからの通報を処理したとき" - userCreated: "ユーザーが作成されたとき" - inactiveModeratorsWarning: "モデレーターが一定期間非アクティブになったとき" - inactiveModeratorsInvitationOnlyChanged: "モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき" - deleteConfirm: "Webhookを削除しますか?" - testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。" - -_abuseReport: - _notificationRecipient: - createRecipient: "通報の通知先を追加" - modifyRecipient: "通報の通知先を編集" - recipientType: "通知先の種類" - _recipientType: - mail: "メール" - webhook: "Webhook" - _captions: - mail: "モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)" - webhook: "指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)" - keywords: "キーワード" - notifiedUser: "通知先ユーザー" - notifiedWebhook: "使用するWebhook" - deleteConfirm: "通知先を削除しますか?" - -_moderationLogTypes: - createRole: "ロールを作成" - deleteRole: "ロールを削除" - updateRole: "ロールを更新" - assignRole: "ロールへアサイン" - unassignRole: "ロールのアサイン解除" - suspend: "凍結" - unsuspend: "凍結解除" - addCustomEmoji: "カスタム絵文字追加" - updateCustomEmoji: "カスタム絵文字更新" - deleteCustomEmoji: "カスタム絵文字削除" - updateServerSettings: "サーバー設定更新" - updateUserNote: "ユーザーのモデレーションノート更新" - deleteDriveFile: "ファイルを削除" - deleteNote: "ノートを削除" - createGlobalAnnouncement: "全体のお知らせを作成" - createUserAnnouncement: "ユーザーへお知らせを作成" - updateGlobalAnnouncement: "全体のお知らせを更新" - updateUserAnnouncement: "ユーザーのお知らせを更新" - deleteGlobalAnnouncement: "全体のお知らせを削除" - deleteUserAnnouncement: "ユーザーのお知らせを削除" - resetPassword: "パスワードをリセット" - suspendRemoteInstance: "リモートサーバーを停止" - unsuspendRemoteInstance: "リモートサーバーを再開" - updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新" - markSensitiveDriveFile: "ファイルをセンシティブ付与" - unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" - resolveAbuseReport: "通報を解決" - forwardAbuseReport: "通報を転送" - updateAbuseReportNote: "通報のモデレーションノート更新" - createInvitation: "招待コードを作成" - createAd: "広告を作成" - deleteAd: "広告を削除" - updateAd: "広告を更新" - createAvatarDecoration: "アイコンデコレーションを作成" - updateAvatarDecoration: "アイコンデコレーションを更新" - deleteAvatarDecoration: "アイコンデコレーションを削除" - unsetUserAvatar: "ユーザーのアイコンを解除" - unsetUserBanner: "ユーザーのバナーを解除" - createSystemWebhook: "SystemWebhookを作成" - updateSystemWebhook: "SystemWebhookを更新" - deleteSystemWebhook: "SystemWebhookを削除" - createAbuseReportNotificationRecipient: "通報の通知先を作成" - updateAbuseReportNotificationRecipient: "通報の通知先を更新" - deleteAbuseReportNotificationRecipient: "通報の通知先を削除" - deleteAccount: "アカウントを削除" - deletePage: "ページを削除" - deleteFlash: "Playを削除" - deleteGalleryPost: "ギャラリーの投稿を削除" - deleteChatRoom: "チャットルームを削除" - updateProxyAccountDescription: "プロキシアカウントの説明を更新" - -_fileViewer: - title: "ファイルの詳細" - type: "ファイルタイプ" - size: "ファイルサイズ" - url: "URL" - uploadedAt: "追加日" - attachedNotes: "添付されているノート" - thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。" - -_externalResourceInstaller: - title: "外部サイトからインストール" - checkVendorBeforeInstall: "配布元が信頼できるかを確認した上でインストールしてください。" - _plugin: - title: "このプラグインをインストールしますか?" - _theme: - title: "このテーマをインストールしますか?" - _meta: - base: "基本のカラースキーム" - _vendorInfo: - title: "配布元情報" - endpoint: "参照したエンドポイント" - hashVerify: "ファイル整合性の確認" - _errors: - _invalidParams: - title: "パラメータが不足しています" - description: "外部サイトからデータを取得するために必要な情報が不足しています。URLをお確かめください。" - _resourceTypeNotSupported: - title: "この外部リソースには対応していません" - description: "この外部サイトから取得したリソースの種別には対応していません。サイト管理者にお問い合わせください。" - _failedToFetch: - title: "データの取得に失敗しました" - fetchErrorDescription: "外部サイトとの通信に失敗しました。もう一度試しても改善しない場合、サイト管理者にお問い合わせください。" - parseErrorDescription: "外部サイトから取得したデータが読み取れませんでした。サイト管理者にお問い合わせください。" - _hashUnmatched: - title: "正しいデータが取得できませんでした" - description: "提供されたデータの整合性の確認に失敗しました。セキュリティ上、インストールは続行できません。サイト管理者にお問い合わせください。" - _pluginParseFailed: - title: "AiScript エラー" - description: "データは取得できたものの、AiScriptの解析時にエラーがあったため読み込めませんでした。プラグインの作者にお問い合わせください。エラーの詳細はJavascriptコンソールをご確認ください。" - _pluginInstallFailed: - title: "プラグインのインストールに失敗しました" - description: "プラグインのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。" - _themeParseFailed: - title: "テーマ解析エラー" - description: "データは取得できたものの、テーマファイルの解析時にエラーがあったため読み込めませんでした。テーマの作者にお問い合わせください。エラーの詳細はJavascriptコンソールをご確認ください。" - _themeInstallFailed: - title: "テーマのインストールに失敗しました" - description: "テーマのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。" - -_dataSaver: - _media: - title: "メディアの読み込みを無効化" - description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。" - _avatar: - title: "アイコン画像のアニメーションを無効化" - description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。" - _urlPreviewThumbnail: - title: "URLプレビューのサムネイルを非表示" - description: "URLプレビューのサムネイル画像が読み込まれなくなります。" - _disableUrlPreview: - title: "URLプレビューを無効化" - description: "URLプレビュー機能を無効化します。サムネイル画像だけと違い、リンク先の情報の読み込み自体を削減できます。" - _code: - title: "コードハイライトを非表示" - description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" - -_hemisphere: - N: "北半球" - S: "南半球" - caption: "一部のクライアント設定で、季節を判定するために使用します。" - -_reversi: - reversi: "リバーシ" - gameSettings: "対局の設定" - chooseBoard: "ボードを選択" - blackOrWhite: "先行/後攻" - blackIs: "{name}が黒(先行)" - rules: "ルール" - thisGameIsStartedSoon: "対局はまもなく開始されます" - waitingForOther: "相手の準備が完了するのを待っています" - waitingForMe: "あなたの準備が完了するのを待っています" - waitingBoth: "準備してください" - ready: "準備完了" - cancelReady: "準備を再開" - opponentTurn: "相手のターンです" - myTurn: "あなたのターンです" - turnOf: "{name}のターンです" - pastTurnOf: "{name}のターン" - surrender: "投了" - surrendered: "投了により" - timeout: "時間切れ" - drawn: "引き分け" - won: "{name}の勝ち" - black: "黒" - white: "白" - total: "合計" - turnCount: "{count}ターン目" - myGames: "自分の対局" - allGames: "みんなの対局" - ended: "終了" - playing: "対局中" - isLlotheo: "石の少ない方が勝ち(ロセオ)" - loopedMap: "ループマップ" - canPutEverywhere: "どこでも置けるモード" - timeLimitForEachTurn: "1ターンの時間制限" - freeMatch: "フリーマッチ" - lookingForPlayer: "対戦相手を探しています" - gameCanceled: "対局がキャンセルされました" - shareToTlTheGameWhenStart: "開始時に対局をタイムラインに投稿" - iStartedAGame: "対局を開始しました! #MisskeyReversi" - opponentHasSettingsChanged: "相手が設定を変更しました" - allowIrregularRules: "変則許可 (完全フリー)" - disallowIrregularRules: "変則なし" - showBoardLabels: "盤面に行・列番号を表示" - useAvatarAsStone: "石をアイコンにする" - -_offlineScreen: - title: "オフライン - サーバーに接続できません" - header: "サーバーに接続できません" - -_urlPreviewSetting: - title: "URLプレビューの設定" - enable: "URLプレビューを有効にする" - allowRedirect: "プレビュー先のリダイレクトを許可" - allowRedirectDescription: "入力されたURLがリダイレクトされる場合に、そのリダイレクト先をたどってプレビューを表示するかどうかを設定します。無効にするとサーバーリソースの節約になりますが、リダイレクト先の内容は表示されなくなります。" - timeout: "プレビュー取得時のタイムアウト(ms)" - timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。" - maximumContentLength: "Content-Lengthの最大値(byte)" - maximumContentLengthDescription: "Content-Lengthがこの値を超えた場合、プレビューは生成されません。" - requireContentLength: "Content-Lengthが取得できた場合のみプレビューを生成" - requireContentLengthDescription: "相手サーバがContent-Lengthを返さない場合、プレビューは生成されません。" - userAgent: "User-Agent" - userAgentDescription: "プレビュー取得時に使用されるUser-Agentを設定します。空欄の場合、デフォルトのUser-Agentが使用されます。" - summaryProxy: "プレビューを生成するプロキシのエンドポイント" - summaryProxyDescription: "Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。" - summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。" - -_mediaControls: - pip: "ピクチャインピクチャ" - playbackRate: "再生速度" - loop: "ループ再生" - -_contextMenu: - title: "コンテキストメニュー" - app: "アプリケーション" - appWithShift: "Shiftキーでアプリケーション" - native: "ブラウザのUI" - -_gridComponent: - _error: - requiredValue: "この値は必須項目です" - columnTypeNotSupport: "正規表現によるバリデーションはtype:textのカラムのみサポートします。" - patternNotMatch: "この値は{pattern}のパターンに一致しません" - notUnique: "この値は一意である必要があります" - -_roleSelectDialog: - notSelected: "選択されていません" - -_customEmojisManager: - _gridCommon: - copySelectionRows: "選択行をコピー" - copySelectionRanges: "選択範囲をコピー" - deleteSelectionRows: "選択行を削除" - deleteSelectionRanges: "選択範囲の値をクリア" - searchSettings: "検索設定" - searchSettingCaption: "検索条件を詳細に設定します。" - searchLimit: "表示件数" - sortOrder: "並び順" - registrationLogs: "登録ログ" - registrationLogsCaption: "絵文字更新・削除時のログが表示されます。更新・削除操作を行ったり、ページを遷移・リロードすると消えます。" - alertEmojisRegisterFailedDescription: "絵文字の更新・削除に失敗しました。詳細は登録ログをご確認ください。" - _logs: - showSuccessLogSwitch: "成功ログを表示" - failureLogNothing: "失敗ログはありません。" - logNothing: "ログはありません。" - _remote: - selectionRowDetail: "選択行の詳細" - importSelectionRows: "選択行をインポート" - importSelectionRangesRows: "選択範囲の行をインポート" - importEmojisButton: "チェックされた絵文字をインポート" - confirmImportEmojisTitle: "絵文字のインポート" - confirmImportEmojisDescription: "リモートから受信した{count}個の絵文字のインポートを行います。絵文字のライセンスに十分な注意を払ってください。実行しますか?" - _local: - tabTitleList: "登録済み絵文字一覧" - tabTitleRegister: "絵文字の登録" - _list: - emojisNothing: "登録された絵文字はありません。" - markAsDeleteTargetRows: "選択行を削除対象にする" - markAsDeleteTargetRanges: "選択範囲の行を削除対象にする" - alertUpdateEmojisNothingDescription: "変更された絵文字はありません。" - alertDeleteEmojisNothingDescription: "削除対象の絵文字はありません。" - confirmMovePage: "ページを移動しますか?" - confirmChangeView: "表示を変更しますか?" - confirmUpdateEmojisDescription: "{count}個の絵文字を更新します。実行しますか?" - confirmDeleteEmojisDescription: "チェックがつけられた{count}個の絵文字を削除します。実行しますか?" - confirmResetDescription: "今までに加えた変更がすべてリセットされます。" - confirmMovePageDesciption: "このページの絵文字に変更が加えられています。\n保存せずにこのままページを移動すると、このページで加えた変更はすべて破棄されます。" - dialogSelectRoleTitle: "絵文字に設定されたロールで検索" - _register: - uploadSettingTitle: "アップロード設定" - uploadSettingDescription: "この画面で絵文字アップロードを行う際の動作を設定できます。" - directoryToCategoryLabel: "ディレクトリ名を\"category\"に入力する" - directoryToCategoryCaption: "ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を\"category\"に入力します。" - confirmRegisterEmojisDescription: "リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです)" - confirmClearEmojisDescription: "編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか?" - confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードします。実行しますか?" - -_embedCodeGen: - title: "埋め込みコードをカスタマイズ" - header: "ヘッダーを表示" - autoload: "自動で続きを読み込む(非推奨)" - maxHeight: "高さの最大値" - maxHeightDescription: "0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。" - maxHeightWarn: "高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。" - previewIsNotActual: "プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。" - rounded: "角丸にする" - border: "外枠に枠線をつける" - applyToPreview: "プレビューに反映" - generateCode: "埋め込みコードを作成" - codeGenerated: "コードが生成されました" - codeGeneratedDescription: "生成されたコードをウェブサイトに貼り付けてご利用ください。" - -_selfXssPrevention: - warning: "警告" - title: "「この画面に何か貼り付けろ」はすべて詐欺です。" - description1: "ここに何かを貼り付けると、悪意のあるユーザーにアカウントを乗っ取られたり、個人情報を盗まれたりする可能性があります。" - description2: "貼り付けようとしているものが何なのかを正確に理解していない場合は、%c今すぐ作業を中止してこのウィンドウを閉じてください。" - description3: "詳しくはこちらをご確認ください。 {link}" - -_followRequest: - recieved: "受け取った申請" - sent: "送った申請" - -_remoteLookupErrors: - _federationNotAllowed: - title: "このサーバーとは通信できません" - description: "このサーバーとの通信が無効化されているか、このサーバーをブロックしている・ブロックされている可能性があります。\nサーバー管理者にお問い合わせください。" - _uriInvalid: - title: "URIが不正です" - description: "入力されたURIに問題があります。URIに使用できない文字を入力していないか確認してください。" - _requestFailed: - title: "リクエストに失敗しました" - description: "このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。" - _responseInvalid: - title: "レスポンスが不正です" - description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。" - _noSuchObject: - title: "見つかりません" - description: "要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。" - -_captcha: - verify: "CAPTCHAを通過してください" - testSiteKeyMessage: "サイトキーとシークレットキーにテスト用の値を入力することでプレビューを確認できます。\n詳細は下記ページをご確認ください。" - _error: - _requestFailed: - title: "CAPTCHAのリクエストに失敗しました" - text: "しばらく後に実行するか、設定をもう一度ご確認ください。" - _verificationFailed: - title: "CAPTCHAの検証に失敗しました" - text: "設定が正しいかどうかもう一度確認ください。" - _unknown: - title: "CAPTCHAエラー" - text: "想定外のエラーが発生しました。" - -_bootErrors: - title: "読み込みに失敗しました" - serverError: "少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。" - solution: "以下を行うと解決する可能性があります。" - solution1: "ブラウザおよびOSを最新バージョンに更新する" - solution2: "アドブロッカーを無効にする" - solution3: "ブラウザのキャッシュをクリアする" - solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する" - otherOption: "その他のオプション" - otherOption1: "クライアント設定とキャッシュを削除" - otherOption2: "簡易クライアントを起動" - otherOption3: "修復ツールを起動" - -_search: - searchScopeAll: "全て" - searchScopeLocal: "ローカル" - searchScopeServer: "サーバー指定" - searchScopeUser: "ユーザー指定" - pleaseEnterServerHost: "サーバーのホストを入力してください" - pleaseSelectUser: "ユーザーを選択してください" - serverHostPlaceholder: "例: misskey.example.com" - -_serverSetupWizard: - installCompleted: "Misskeyのインストールが完了しました!" - firstCreateAccount: "まずは、管理者アカウントを作成しましょう。" - accountCreated: "管理者アカウントが作成されました!" - serverSetting: "サーバーの設定" - youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "このウィザードで簡単に最適なサーバーの設定が行えます。" - settingsYouMakeHereCanBeChangedLater: "ここでの設定は、あとからでも変更できます。" - howWillYouUseMisskey: "Misskeyをどのように使いますか?" - _use: - single: "お一人様サーバー" - single_description: "自分専用のサーバーとして、一人で使う" - single_youCanCreateMultipleAccounts: "お一人様サーバーとして運用する場合でも、アカウントは必要に応じて複数作成可能です。" - group: "グループサーバー" - group_description: "信頼できる他の利用者を招待して、複数人で使う" - open: "オープンサーバー" - open_description: "不特定多数の利用者を受け入れる運営を行う" - openServerAdvice: "不特定多数の利用者を受け入れることはリスクが伴います。トラブルに対処できるよう、確実なモデレーション体制で運営することを推奨します。" - openServerAntiSpamAdvice: "自サーバーがスパムの踏み台にならないように、reCAPTCHAといったアンチボット機能を有効にするなど、セキュリティについても細心の注意が必要です。" - howManyUsersDoYouExpect: "どれくらいの人数を想定していますか?" - _scale: - small: "100人以下 (小規模)" - medium: "100人以上1000人以下 (中規模)" - large: "1000人以上 (大規模)" - largeScaleServerAdvice: "大規模なサーバーでは、ロードバランシングやデータベースのレプリケーションなど、高度なインフラストラクチャーの知識が必要になる場合があります。" - doYouConnectToFediverse: "Fediverseと接続しますか?" - doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。" - doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。" - youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。" - adminInfo: "管理者情報" - adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。" - adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。" - followingSettingsAreRecommended: "以下の設定が推奨されます" - applyTheseSettings: "この設定を適用" - skipSettings: "設定をスキップ" - settingsCompleted: "設定が完了しました!" - settingsCompleted_description: "お疲れ様でした。準備が整ったので、さっそくサーバーの使用を開始できます。" - settingsCompleted_description2: "詳細なサーバー設定は、「コントロールパネル」から行えます。" - donationRequest: "寄付のお願い" - _donationRequest: - text1: "Misskeyは有志によって開発されている無料のソフトウェアです。" - text2: "今後も開発を続けられるように、よろしければぜひカンパをお願いいたします。" - text3: "支援者向け特典もあります!" - -_uploader: - compressedToX: "{x}に圧縮" - savedXPercent: "{x}%節約" - abortConfirm: "アップロードされていないファイルがありますが、中止しますか?" - doneConfirm: "アップロードされていないファイルがありますが、完了しますか?" - maxFileSizeIsX: "アップロード可能な最大ファイルサイズは{x}です。" - allowedTypes: "アップロード可能なファイル種別" - tip: "ファイルはまだアップロードされていません。このダイアログで、アップロード前の確認・リネーム・圧縮・クロッピングなどが行えます。準備が出来たら、「アップロード」ボタンを押してアップロードを開始できます。" - -_clientPerformanceIssueTip: - title: "バッテリー消費が多いと感じたら" - makeSureDisabledAdBlocker: "アドブロッカーを無効にしてください" - makeSureDisabledAdBlocker_description: "アドブロッカーはパフォーマンスに影響を及ぼすことがあります。OSの機能やブラウザの機能・アドオンなどでアドブロッカーが有効になっていないか確認してください。" - makeSureDisabledCustomCss: "カスタムCSSを無効にしてください" - makeSureDisabledCustomCss_description: "スタイルを上書きするとパフォーマンスに影響を及ぼすことがあります。カスタムCSSや、スタイルを上書きする拡張機能が有効になっていないか確認してください。" - makeSureDisabledAddons: "拡張機能を無効にしてください" - makeSureDisabledAddons_description: "一部の拡張機能はクライアントの動作に干渉しパフォーマンスに影響を及ぼすことがあります。ブラウザの拡張機能を無効にして改善するか確認してください。" - -_clip: - tip: "クリップは、ノートをまとめることができる機能です。" - -_userLists: - tip: "任意のユーザーが含まれるリストを作成できます。作成したリストはタイムラインとして表示可能です。" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 677baf4aa8..ec1aeb31ef 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -5,21 +5,17 @@ introMisskey: "ようお越し!Misskeyは、オープンソースの分散型 poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォームMisskeyのサーバーのひとつなんやで。" monthAndDay: "{month}月 {day}日" search: "探す" -reset: "リセット" notifications: "通知" username: "ユーザー名" password: "パスワード" -initialPasswordForSetup: "初期設定開始用パスワード" -initialPasswordIsIncorrect: "初期設定開始用のパスワードがちゃうで。" -initialPasswordForSetupDescription: "Miskkeyを自分でインストールしたんやったら、設定ファイルに入れたパスワードを使ってや。\nホスティングサービスを使っとるんやったら、サービスから言われたやつを使うんやで。\n別に何も設定しとらんのやったら、何も入れずに空けといてな。" forgotPassword: "パスワード忘れたん?" fetchingAsApObject: "今ちと連合に照会しとるで" ok: "ええで" gotIt: "ほい" -cancel: "やめる" +cancel: "やめとく" noThankYou: "やめとく" enterUsername: "ユーザー名を入れてや" -renotedBy: "{user}がリノートしたで" +renotedBy: "{user}がRenoteしたで" noNotes: "ノートはあらへん" noNotifications: "通知はあらへん" instance: "サーバー" @@ -27,7 +23,7 @@ settings: "設定" notificationSettings: "通知の設定" basicSettings: "基本設定" otherSettings: "ほかの設定" -openInWindow: "ウィンドウで開く" +openInWindow: "ウィンドウで開くで" profile: "プロフィール" timeline: "タイムライン" noAccountDescription: "自己紹介食ってもた" @@ -42,30 +38,23 @@ addUser: "ユーザーを追加や" favorite: "お気に入り" favorites: "お気に入り" unfavorite: "やっぱ気に入らん" -favorited: "お気に入りに入れたで。" +favorited: "お気に入りに入れたで" alreadyFavorited: "もうお気に入りに入れとるがな。" cantFavorite: "アカン、お気に入りに入れれんかったわ。" pin: "ピン留めしとく" -unpin: "ピン留めやめる" +unpin: "やっぱピン留めせん" copyContent: "内容をコピー" copyLink: "リンクをコピー" -copyRemoteLink: "リモートのリンクをコピーするで?" -copyLinkRenote: "リノートのリンクをコピーするで?" delete: "ほかす" deleteAndEdit: "ほかして直す" -deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、リノート、返信も全部消えるんやけどそれでもええん?" +deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、Renote、返信も全部消えるんやけどそれでもええん?" addToList: "リストに入れたる" -addToAntenna: "アンテナに入れる" sendMessage: "メッセージを送る" copyRSS: "RSSをコピー" copyUsername: "ユーザー名をコピー" copyUserId: "ユーザーIDをコピー" copyNoteId: "ノートIDをコピー" -copyFileId: "ファイルIDをコピー" -copyFolderId: "フォルダーIDをコピー" -copyProfileUrl: "プロフィールURLをコピー" -searchUser: "ユーザーを探す" -searchThisUsersNotes: "ユーザーのノートを探す" +searchUser: "ユーザーを検索" reply: "返事" loadMore: "まだまだあるで!" showMore: "まだまだあるで!" @@ -74,7 +63,7 @@ youGotNewFollower: "フォローされたで" receiveFollowRequest: "フォローリクエストされたで" followRequestAccepted: "フォローが承認されたで" mention: "メンション" -mentions: "あんた宛て" +mentions: "自分宛て" directNotes: "ダイレクト投稿" importAndExport: "インポートとエクスポート" import: "インポート" @@ -83,7 +72,7 @@ files: "ファイル" download: "ダウンロード" driveFileDeleteConfirm: "ファイル「{name}」をほかしてええか?このファイルを添付したノートも消えてまうで。" unfollowConfirm: "{name}のフォローを解除してもええんか?" -exportRequested: "エクスポートしてな、って言うたけど、これ多分めっちゃ時間かかるで。エクスポート終わったら「ドライブ」に突っ込んどくで。" +exportRequested: "エクスポートしてな、ってリクエストしたけど、これ多分めっちゃ時間かかるで。エクスポート終わったら「ドライブ」に突っ込んどくで。" importRequested: "インポートしてな、ってリクエストしたけど、これ多分めっちゃ時間かかるで。" lists: "リスト" noLists: "リストなんてあらへんで" @@ -94,7 +83,7 @@ followers: "フォロワー" followsYou: "フォローされとるで" createList: "リスト作る" manageLists: "リストの管理" -error: "おかしなったで" +error: "エラー" somethingHappened: "なんかあかんわ" retry: "もっぺんやる?" pageLoadError: "ページが読み込めんかったわ。" @@ -111,17 +100,14 @@ followRequests: "フォロー申請" unfollow: "フォローやめる" followRequestPending: "フォロー許してくれるん待っとる" enterEmoji: "絵文字を入れてや" -renote: "リノート" -unrenote: "リノートやめる" -renoted: "リノートしたで。" -renotedToX: "{name}にリノートしたで" -cantRenote: "この投稿はリノートできへんっぽい。" -cantReRenote: "リノート自体はリノートできへんで。" +renote: "Renote" +unrenote: "Renoteやめる" +renoted: "Renoteしたで。" +cantRenote: "この投稿はRenoteできへんらしい。" +cantReRenote: "Renote自体はRenoteできへんで。" quote: "引用" -inChannelRenote: "チャンネルの中でリノート" +inChannelRenote: "チャンネル内Renote" inChannelQuote: "チャンネル内引用" -renoteToChannel: "チャンネルにリノート" -renoteToOtherChannel: "他のチャンネルにリノート" pinnedNote: "ピン留めされとるノート" pinned: "ピン留めしとく" you: "あんた" @@ -130,23 +116,17 @@ sensitive: "気いつけて見いや" add: "増やす" reaction: "ツッコミ" reactions: "ツッコミ" -emojiPicker: "絵文字ピッカー" -pinnedEmojisForReactionSettingDescription: "リアクションしたときにピンで留めてる表示をする絵文字を設定するで" -pinnedEmojisSettingDescription: "絵文字打ったときにピン留め表示する絵文字設定できるで" -emojiPickerDisplay: "ピッカーの表示" -overwriteFromPinnedEmojisForReaction: "リアクション設定から上書きする" -overwriteFromPinnedEmojis: "全般設定から上書きする" +reactionSetting: "ピッカーに出しとくツッコミ" reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。" rememberNoteVisibility: "公開範囲覚えといて" attachCancel: "のっけるのやめる" -deleteFile: "ファイルをほかす" -markAsSensitive: "ちょっと見せられへんわ" -unmarkAsSensitive: "別にええんじゃね?" +markAsSensitive: "ちょっとこれはアカン" +unmarkAsSensitive: "そこまでアカンことないやろ" enterFileName: "ファイル名を入れてや" mute: "ミュート" unmute: "ミュートやめたる" -renoteMute: "リノートは見いひん" -renoteUnmute: "リノートもやっぱ見るわ" +renoteMute: "Renoteは見いひん" +renoteUnmute: "Renoteもやっぱ見るわ" block: "ブロック" unblock: "ブロックやめたる" suspend: "凍結" @@ -154,16 +134,15 @@ unsuspend: "溶かす" blockConfirm: "ブロックしてもええんか?" unblockConfirm: "ブロックやめたるってほんまか?" suspendConfirm: "凍結してしもうてええか?" -unsuspendConfirm: "溶かしたるけどええか?" +unsuspendConfirm: "解凍するけどええか?" selectList: "リストを選ぶ" -editList: "リストいじる" +editList: "リスト直すで" selectChannel: "チャンネルを選ぶ" selectAntenna: "アンテナを選ぶ" -editAntenna: "アンテナいじる" -createAntenna: "アンテナを作る" +editAntenna: "アンテナを編集" selectWidget: "ウィジェットを選ぶ" editWidgets: "ウィジェットをいじる" -editWidgetsExit: "いじるのをやめる" +editWidgetsExit: "編集終ったで" customEmojis: "カスタム絵文字" emoji: "絵文字" emojis: "絵文字" @@ -172,14 +151,11 @@ emojiUrl: "絵文字画像URL" addEmoji: "絵文字を追加" settingGuide: "ええ感じの設定" cacheRemoteFiles: "リモートのファイルをキャッシュする" -cacheRemoteFilesDescription: "この設定を入れとったら、リモートのファイルを端から端までこのサーバーのキャッシュん中突っ込むようになるで。画像映し出すんがめっちゃ速うなるけど、サーバーの容量をやたらと食うようになるで。リモートの人がどんだけ長くキャッシュを持っとくかはドライブ容量の制限で決めとくで。制限を超えたら古いのから順々に消してって、かわりにリンクになるで。この設定を切ったら、リモートのファイルは最初っからリンクとして扱うことにするけど、画像のサムネ作るのとかみんなのプライバシー守るために、default.ymlのproxyRemoteFilesをtrueにしといたほうがええよ。" -youCanCleanRemoteFilesCache: "ファイル管理にある🗑️ボタンでキャッシュ全部ほかすで。" -cacheRemoteSensitiveFiles: "リモートのきわどいファイルをキャッシュする" -cacheRemoteSensitiveFilesDescription: "この設定を切ると、リモートのきわどいファイルはキャッシュせず直でリンクするようになるで。" +cacheRemoteFilesDescription: "この設定を切っとったら、リモートファイルをキャッシュせんと直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルを作らんなるから通信量が増えるで。" flagAsBot: "Botにするで" flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。" -flagAsCat: "猫や。かわええな。" -flagAsCatDescription: "猫になりたいんならこれつけとき。" +flagAsCat: "Catやで" +flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?" flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで" flagShowTimelineRepliesDescription: "オンにしたら、タイムラインにユーザーのノートの他にもそのユーザーの他のノートへの返信を表示するで。" autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく" @@ -187,10 +163,6 @@ addAccount: "アカウントを追加" reloadAccountsList: "アカウントリストの情報を更新" loginFailed: "ログインに失敗してもうた…" showOnRemote: "リモートで見る" -continueOnRemote: "リモートで続行" -chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選ぶ" -specifyServerHost: "サーバーのドメインを直接指定" -inputHostName: "ドメインを入力してや" general: "全般" wallpaper: "壁紙" setWallpaper: "壁紙を設定" @@ -201,7 +173,6 @@ followConfirm: "{name}をフォローしてええか?" proxyAccount: "プロキシアカウント" proxyAccountDescription: "プロキシアカウントは、代わりにフォローしてくれるアカウントや。例えば、551に豚まんが無いときやったり、ユーザーがリモートユーザーをアカウントに入れたとき、リストに入れられたユーザーが誰からもフォローされてないと寂しいやん。寂しいし、アクティビティも配達されへんから、プロキシアカウントがフォローしてくれるで。ええやつやん…" host: "ホスト" -selectSelf: "自分を選択" selectUser: "ユーザーを選ぶ" recipient: "宛先" annotation: "注釈" @@ -216,8 +187,6 @@ perHour: "1時間ごと" perDay: "1日ごと" stopActivityDelivery: "アクティビティの配送をやめる" blockThisInstance: "このサーバーをブロックすんで" -silenceThisInstance: "サーバーサイレンスすんで?" -mediaSilenceThisInstance: "サーバーをメディアサイレンス" operations: "操作" software: "ソフトウェア" version: "バージョン" @@ -234,22 +203,17 @@ clearQueue: "キューをほかす" clearQueueConfirmTitle: "キューをほかしとこか?" clearQueueConfirmText: "未配達の投稿は配送されんなるで。ふつうこの操作を行う必要は無いんやけどな。" clearCachedFiles: "キャッシュをほかす" -clearCachedFilesConfirm: "キャッシュされとるリモートファイルを全部ほかしてええか?" +clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?" blockedInstances: "ブロックしたサーバー" -blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。" -silencedInstances: "サーバーサイレンスされてんねん" -silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定すんで。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなんねん。ブロックしたインスタンスには影響せーへんで。" -mediaSilencedInstances: "メディアサイレンスしたサーバー" -mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定するで。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われてな、カスタム絵文字が使えへんようになるで。ブロックしたインスタンスには影響せえへんで。" -federationAllowedHosts: "連合を許すサーバー" -federationAllowedHostsDescription: "連合してもいいサーバーのホストを行ごとに区切って設定してや。" +blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。" muteAndBlock: "ミュートとブロック" -mutedUsers: "ミュートしとるユーザー" -blockedUsers: "ブロックしとるユーザー" +mutedUsers: "ミュートしたユーザー" +blockedUsers: "ブロックしたユーザー" noUsers: "ユーザーはおらん" editProfile: "プロフィールをいじる" noteDeleteConfirm: "このノートをほかしてええか?" pinLimitExceeded: "これ以上ピン留めできひん" +intro: "Misskeyのインストールが完了したで!管理者アカウントを作ってや。" done: "でけた" processing: "処理しとる" preview: "プレビュー" @@ -271,7 +235,7 @@ changePassword: "パスワードをいじる" security: "セキュリティ" retypedNotMatch: "入れたやつ合うてへんわ。" currentPassword: "今のパスワード" -newPassword: "今度のパスワード" +newPassword: "次のパスワード" newPasswordRetype: "今度のパスワード(もっぺん入れて)" attachFile: "ファイルのっける" more: "他のん" @@ -286,8 +250,8 @@ removed: "ほかしたで!" removeAreYouSure: "「{x}」はほかしてええか?" deleteAreYouSure: "「{x}」はほかしてええか?" resetAreYouSure: "リセットしてええん?" -areYouSure: "いいん?" saved: "保存したで!" +messaging: "チャット" upload: "アップロード" keepOriginalUploading: "オリジナル画像のまんま" keepOriginalUploadingDescription: "画像を上げるときにオリジナル版のまんまにするで。オフにしたら、上げたときにブラウザでWeb公開用の画像を生成するで。 " @@ -300,6 +264,7 @@ uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間か explore: "みつける" messageRead: "もう読んだ" noMoreHistory: "これより昔のんはあらへんで" +startMessaging: "チャットやるで" nUsersRead: "{n}人が読んでもうた" agreeTo: "{0}に同意したで" agree: "せやな" @@ -330,15 +295,12 @@ selectFile: "ファイル選んでや" selectFiles: "ファイル選んでや" selectFolder: "フォルダ選んでや" selectFolders: "フォルダ選んでや" -fileNotSelected: "ファイルが選択されてへんで" renameFile: "ファイル名をいらう" folderName: "フォルダー名" createFolder: "フォルダー作る" renameFolder: "フォルダー名を変える" deleteFolder: "フォルダーをほかす" -folder: "フォルダー" addFile: "ファイルを追加" -showFile: "ファイル出す" emptyDrive: "ドライブは空っぽや" emptyFolder: "このフォルダーは空や" unableToDelete: "消せんかったわ" @@ -351,7 +313,6 @@ copyUrl: "URLをコピー" rename: "名前を変えるで" avatar: "アイコン" banner: "バナー" -displayOfSensitiveMedia: "きわどいやつの表示" whenServerDisconnected: "サーバーとの接続が失くなってしもうたとき" disconnectedFromServer: "サーバーが機嫌悪いねん" reload: "リロード" @@ -381,10 +342,12 @@ enableLocalTimeline: "ローカルタイムラインを使えるようにする enableGlobalTimeline: "グローバルタイムラインを使えるようにするわ" disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。" registration: "登録" +enableRegistration: "一見さんでも誰でもいらっしゃ~い" invite: "来てや" driveCapacityPerLocalAccount: "ローカルユーザーはんひとりあたりのドライブ容量" driveCapacityPerRemoteAccount: "リモートユーザーはんひとりあたりのドライブ容量" inMb: "メガバイト単位" +iconUrl: "アイコン画像のURL" bannerUrl: "バナー画像のURL" backgroundImageUrl: "背景画像のURL" basicInfo: "基本情報" @@ -398,11 +361,6 @@ hcaptcha: "hCaptcha(キャプチャ)" enableHcaptcha: "hCaptcha(キャプチャ)をつけとく" hcaptchaSiteKey: "サイトキー" hcaptchaSecretKey: "シークレットキー" -mcaptcha: "mCaptcha" -enableMcaptcha: "hCaptcha(キャプチャ)をつけとく" -mcaptchaSiteKey: "サイトキー" -mcaptchaSecretKey: "シークレットキー" -mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHA(リキャプチャ)を有効にする" recaptchaSiteKey: "サイトキー" @@ -418,7 +376,6 @@ name: "名前" antennaSource: "受信ソース(このソースは食われへん)" antennaKeywords: "受信キーワード" antennaExcludeKeywords: "除外キーワード" -antennaExcludeBots: "Botアカウントを除外" antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や" notifyAntenna: "新しいノートを通知すんで" withFileAntenna: "なんか添付されたノートだけ" @@ -444,21 +401,16 @@ userList: "リスト" about: "情報" aboutMisskey: "Misskeyってなんや?" administrator: "管理者" -token: "確認コード" +token: "トークン" 2fa: "二要素認証" -setupOf2fa: "二要素認証のセットアップ" totp: "認証アプリ" totpDescription: "認証アプリ使うてワンタイムパスワードを入れる" moderator: "モデレーター" moderation: "モデレーション" -moderationNote: "モデレーションノート" -moderationNoteDescription: "モデレーターの中だけで共有するメモを入れれるで。" -addModerationNote: "モデレーションノートを追加するで" -moderationLogs: "モデログ" nUsersMentioned: "{n}人が投稿" securityKeyAndPasskey: "セキュリティキー・パスキー" securityKey: "セキュリティキー" -lastUsed: "最後に使うた日" +lastUsed: "最後につこうた日" lastUsedAt: "最後に使うたんは: {t}" unregister: "登録やめる" passwordLessLogin: "パスワード無くてもログインできるようにする" @@ -470,6 +422,7 @@ share: "わけわけ" notFound: "見つからへんね" notFoundDescription: "言われたURLにはまるページはなかったで。" uploadFolder: "とりあえずアップロードしたやつ置いとく所" +cacheClear: "キャッシュをほかす" markAsReadAllNotifications: "通知はもう全て読んだわっ" markAsReadAllUnreadNotes: "投稿は全て読んだわっ" markAsReadAllTalkMessages: "チャットはもうぜんぶ読んだわっ" @@ -487,10 +440,10 @@ retype: "もっかい入力" noteOf: "{user}はんのノート" quoteAttached: "引用付いとるで" quoteQuestion: "引用として添付してもええか?" -attachAsFileQuestion: "クリップボードのテキストが長すぎるからテキストファイルとして添付してもええか?" +noMessagesYet: "まだチャットはあらへんで" +newMessageExists: "新しいメッセージがきたで" onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。" signinRequired: "ログインしてくれへん?" -signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があるで" invitations: "来てや" invitationCode: "招待コード" checking: "確認しとるで" @@ -503,7 +456,7 @@ weakPassword: "へぼいパスワード" normalPassword: "ぼちぼちのパスワード" strongPassword: "ええ感じのパスワード" passwordMatched: "よし!一致や!" -passwordNotMatched: "ちゃうで?" +passwordNotMatched: "一致しとらんで?" signinWith: "{x}でログイン" signinFailed: "ログインできんかったで。もっかいユーザー名とパスワードを確認してみてや。" or: "それか" @@ -512,12 +465,8 @@ uiLanguage: "UIの表示言語" aboutX: "{x}について" emojiStyle: "絵文字のスタイル" native: "ネイティブ" -menuStyle: "メニューのスタイル" -style: "スタイル" -drawer: "ドロワー" -popup: "ポップアップ" +disableDrawer: "メニューをドロワーで表示せえへん" showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで" -showReactionsCount: "ノートのリアクション数を表示する" noHistory: "履歴はないわ。" signinHistory: "ログイン履歴" enableAdvancedMfm: "ややこしいMFMもありにする" @@ -561,7 +510,7 @@ objectStorageEndpointDesc: "S3のときは空、それ以外は各サービス objectStorageRegion: "Region" objectStorageRegionDesc: "'xx-east-1'みたいなregionを指定したってやー。使ってるサービスにregionの概念がないときは、空か'us-east-1'にするんやで。" objectStorageUseSSL: "SSLを使う" -objectStorageUseSSLDesc: "API接続にhttpsを使わんのやったら消しといて" +objectStorageUseSSLDesc: "API接続にhttpsを使わん場合はオフにするんやで" objectStorageUseProxy: "Proxyを使う" objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん?" objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや" @@ -570,20 +519,16 @@ serverLogs: "サーバーログ" deleteAll: "全部ほかす" showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?" showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)" -withRepliesByDefaultForNewlyFollowed: "フォローする時、デフォルトで返信をタイムラインに含むようにしよか" newNoteRecived: "新しいノートがあるで" -sounds: "音" -sound: "音" +sounds: "サウンド" +sound: "サウンド" listen: "聴く" none: "なし" showInPage: "ページで表示" popout: "ポップアウト" volume: "やかましさ" masterVolume: "全体のやかましさ" -notUseSound: "音出さへん" -useSoundOnlyWhenActive: "Misskeyがアクティブなときだけ音出す" details: "もっと" -renoteDetails: "リノートの詳細" chooseEmoji: "絵文字を選ぶ" unableToProcess: "なんか奥の方で詰まってもうた" recentUsed: "最近使ったやつ" @@ -599,16 +544,10 @@ ascendingOrder: "小さい順" descendingOrder: "大きい順" scratchpad: "スクラッチパッド" scratchpadDescription: "スクラッチパッドではAiScriptを色々試すことができるんや。Misskeyに対して色々できるコードを書いて動かしてみたり、結果を見たりできるで。" -uiInspector: "UIインスペクター" -uiInspectorDescription: "メモリ上にあるUIコンポーネントのインスタンス一覧を見れるで。UIコンポーネントはUi:C:系関数で生成されるで。" output: "出力" script: "スクリプト" disablePagesScript: "Pagesのスクリプトを無効にしてや" updateRemoteUser: "リモートユーザー情報の更新してくれん?" -unsetUserAvatar: "アイコン戻す" -unsetUserAvatarConfirm: "アイコンを元に戻すで?" -unsetUserBanner: "バナー戻す" -unsetUserBannerConfirm: "バナー元に戻すで?" deleteAllFiles: "ファイルを全部ほかす" deleteAllFilesConfirm: "ホンマにファイル全部ほかすんか?消したもんはもう戻ってこんのやで?" removeAllFollowing: "フォローを全解除" @@ -620,7 +559,7 @@ yourAccountSuspendedDescription: "あんたのアカウントは、サーバー tokenRevoked: "トークンが無効やで" tokenRevokedDescription: "ログイントークンが失効しとるで。もっかいログインしてもろてもええか?" accountDeleted: "アカウントは削除されとるで" -accountDeletedDescription: "このアカウントはもう消えとる。" +accountDeletedDescription: "このアカウントは削除されとるで。" menu: "メニュー" divider: "分割線" addItem: "項目を追加" @@ -636,9 +575,9 @@ enableInfiniteScroll: "自動でもっと見る" visibility: "公開範囲" poll: "アンケート" useCw: "内容を隠す" -enablePlayer: "プレイヤー開く" -disablePlayer: "プレイヤー閉じる" -expandTweet: "ポスト展開しとく" +enablePlayer: "プレイヤーを開く" +disablePlayer: "プレイヤーを閉じる" +expandTweet: "ツイートを展開する" themeEditor: "テーマエディター" description: "説明" describeFile: "キャプションを付ける" @@ -651,7 +590,7 @@ preferencesBackups: "設定のバックアップ" deck: "デッキ" undeck: "デッキ解除" useBlurEffectForModal: "モーダルにぼかし効果を使用" -useFullReactionPicker: "フルフルのツッコミピッカーを使う" +useFullReactionPicker: "フル機能の突っ込みピッカーを使用" width: "幅" height: "高さ" large: "大" @@ -659,7 +598,6 @@ medium: "中" small: "小" generateAccessToken: "アクセストークンの発行" permission: "権限" -adminPermission: "管理者権限" enableAll: "全部使えるようにする" disableAll: "全部使えへんようにする" tokenRequested: "アカウントへのアクセス許してやったらどうや" @@ -681,15 +619,10 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecureInfo: "STARTTLS使っとる時はオフにしてや。" testEmail: "配信テスト" wordMute: "ワードミュート" -wordMuteDescription: "指定した語句が入ってるノートを最小化するで。最小化されたノートをクリックしたら、表示できるようになるで。" -hardWordMute: "ハードワードミュート" -showMutedWord: "ミュートされたワードを表示するで" -hardWordMuteDescription: "指定した語句が入ってるノートを隠すで。ワードミュートとちゃうて、ノートは完全に表示されんようになるで。" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:" instanceMute: "サーバーミュート" userSaysSomething: "{name}が何か言うとるわ" -userSaysSomethingAbout: "{name}が「{word}」についてなんか言うてたで" makeActive: "使うで" display: "表示" copy: "コピー" @@ -701,27 +634,28 @@ database: "データベース" channel: "チャンネル" create: "作成" notificationSetting: "通知設定" -notificationSettingDesc: "出す通知の種類えらんでや。" +notificationSettingDesc: "表示する通知の種類えらんでや。" useGlobalSetting: "グローバル設定を使ってや" useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使われるで。オフにすると、別々に設定できるようになるで。" other: "その他" regenerateLoginToken: "ログイントークンを再生成" regenerateLoginTokenDescription: "ログインに使われる内部トークンをもっかい作るで。いつもならこれをやる必要はないで。もっかい作ると、全部のデバイスでログアウトされるで気ぃつけてなー。" -theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を探すときのキーワードになるで。" setMultipleBySeparatingWithSpace: "スペースで区切って何個でも設定できるで。" fileIdOrUrl: "ファイルIDかURL" behavior: "動作" sample: "サンプル" abuseReports: "通報" reportAbuse: "通報" -reportAbuseRenote: "リノート苦情だすで?" reportAbuseOf: "{name}を通報する" fillAbuseReportDescription: "細かい通報理由を書いてなー。対象ノートがある時はそのURLも書いといてなー。" abuseReported: "無事内容が送信されたみたいやで。おおきに〜。" reporter: "通報者" reporteeOrigin: "通報先" reporterOrigin: "通報元" +forwardReport: "リモートサーバーに通報を転送するで" +forwardReportIsAnonymous: "リモートサーバーからはあんたの情報は見えんなって、匿名のシステムアカウントとして表示されるで。" send: "送信" +abuseMarkAsResolved: "対応したで" openInNewTab: "新しいタブで開く" openInSideView: "サイドビューで開く" defaultNavigationBehaviour: "デフォルトのナビゲーション" @@ -736,18 +670,17 @@ clip: "クリップ" createNew: "新しく作るで" optional: "任意" createNewClip: "新しいクリップを作るで" -unclip: "クリップやめとく" -confirmToUnclipAlreadyClippedNote: "このノートはもう「{name}」に含まれとるで。ノート、このクリップから外そか?" +unclip: "クリップ解除するで" +confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外しよか?" public: "パブリック" -private: "非公開" -i18nInfo: "Misskeyは有志がいろんな言語に訳しとるで。{link}で翻訳に協力したってやー。" +i18nInfo: "Misskeyは有志によっていろんな言語に翻訳されとるで。{link}で翻訳に協力したってやー。" manageAccessTokens: "アクセストークンの管理" accountInfo: "アカウント情報" notesCount: "ノートの数やで" repliesCount: "返信した数やで" -renotesCount: "リノートした数やで" +renotesCount: "Renoteした数やで" repliedCount: "返信された数やで" -renotedCount: "リノートされた数やで" +renotedCount: "Renoteされた数やで" followingCount: "フォロー数やで" followersCount: "フォロワー数やで" sentReactionsCount: "ツッコんだ数" @@ -759,12 +692,11 @@ no: "あかん" driveFilesCount: "ドライブのファイル数" driveUsage: "ドライブ使用量やで" noCrawle: "クローラーによるインデックスを拒否するで" -noCrawleDescription: "検索エンジンにあんたのユーザーページ、ノート、Pagesとかのコンテンツを登録(インデックス)せんように頼むで。邪魔すんねんやったら帰って〜。" +noCrawleDescription: "検索エンジンにあんたのユーザーページ、ノート、Pagesとかのコンテンツを登録(インデックス)せぇへんように頼むで。" lockedAccountInfo: "フォローを承認制にしとっても、ノートの公開範囲を「フォロワー」にせぇへん限り、誰でもあんたのノートを見れるで。" alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで" loadRawImages: "添付画像のサムネイルをオリジナル画質にするで" disableShowingAnimatedImages: "アニメーション画像を再生せんとくで" -highlightSensitiveMedia: "きわどいことをめっっちゃわかりやすくする" verificationEmailSent: "無事確認のメールを送れたで。メールに書いてあるリンクにアクセスして、設定を完了してなー。" notSet: "未設定" emailVerified: "メールアドレスは確認されたで" @@ -776,10 +708,11 @@ useSystemFont: "システムのデフォルトのフォントを使うで" clips: "クリップ" experimentalFeatures: "おためし機能やで" experimental: "実験的" -thisIsExperimentalFeature: "これは実験的な機能やから、仕様が変わったりちゃんと動かんかったりするかもしれん。" +thisIsExperimentalFeature: "これは実験的な機能やで。仕様が変更になったりちゃんと動かなかったりするかもやで。" developer: "開発者やで" makeExplorable: "アカウントを見つけやすくするで" makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らんくなるで。" +showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示するで" duplicate: "複製" left: "左" center: "真ん中" @@ -793,7 +726,7 @@ onlineUsersCount: "{n}人が起きとるで" nUsers: "{n}ユーザー" nNotes: "{n}ノート" sendErrorReports: "エラーリポートを送る" -sendErrorReportsDescription: "オンにしたら、なんか変なことが起きたとき、詳しいのが全部Misskeyに送られて、ソフトウェアをもっと良うするで。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴なんかが含まれるな。" +sendErrorReportsDescription: "オンにしたら、変なことが起きたときにエラーの詳細がMisskeyに送られて、ソフトウェアの品質向上に使えるようになるで。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴なんかが含まれるで。" myTheme: "マイテーマ" backgroundColor: "背景" accentColor: "アクセント" @@ -805,12 +738,12 @@ value: "値" createdAt: "作成した日" updatedAt: "更新日時" saveConfirm: "保存するで?" -deleteConfirm: "ホンマにほかすで?" +deleteConfirm: "ホンマに削除するで?" invalidValue: "有効な値じゃないみたいやで。" registry: "レジストリ" closeAccount: "アカウントを閉鎖する" -currentVersion: "今のやつ" -latestVersion: "いっちゃん新しいやつ" +currentVersion: "現在のバージョン" +latestVersion: "最新のバージョン" youAreRunningUpToDateClient: "今使ってるクライアントが最新やで!" newVersionOfClientAvailable: "新しいバージョンのクライアントが使えるで。" usageAmount: "使用量" @@ -832,9 +765,9 @@ goBack: "戻る" unlikeConfirm: "いいね解除するんか?" fullView: "フルビュー" quitFullView: "フルビュー解除" -addDescription: "説明を入れるで" -userPagePinTip: "ノートのメニューから「ピン留め」を選んどいたら、ここにノートを置いとけるで。" -notSpecifiedMentionWarning: "宛先にないメンションがあるで" +addDescription: "説明を追加するで" +userPagePinTip: "個々のノートのメニューから「ピン留め」を選んどくと、ここにノートを表示しておけるで。" +notSpecifiedMentionWarning: "宛先に含まれてへんメンションがあるで" info: "情報" userInfo: "ユーザー情報やで" unknown: "不明" @@ -846,7 +779,7 @@ active: "アクティブ" offline: "オフライン" notRecommended: "あんま推奨しやんで" botProtection: "Botプロテクション" -instanceBlocking: "サーバーブロック・サイレンス" +instanceBlocking: "サーバーブロック" selectAccount: "アカウントを選んでなー" switchAccount: "アカウントを変えるで" enabled: "有効" @@ -857,7 +790,6 @@ administration: "管理" accounts: "アカウント" switch: "切り替え" noMaintainerInformationWarning: "管理者情報が設定されてへんで" -noInquiryUrlWarning: "問い合わせ先URLが設定されてへんで。" noBotProtectionWarning: "Botプロテクションが設定されてへんで。" configure: "設定する" postToGallery: "ギャラリーへ投稿" @@ -907,7 +839,7 @@ itsOn: "オンになっとるよ" itsOff: "オフになってるで" on: "オン" off: "オフ" -emailRequiredForSignup: "アカウント作るのにメールアドレスを必須にするで" +emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで" unread: "未読" filter: "フィルタ" controlPanel: "コントロールパネル" @@ -917,12 +849,11 @@ makeReactionsPublicDescription: "あんたがしたツッコミ一覧を誰で classic: "クラシック" muteThread: "スレッドをミュート" unmuteThread: "スレッドのミュートを解除" -followingVisibility: "フォローの公開範囲" -followersVisibility: "フォロワーの公開範囲" +ffVisibility: "つながりの公開範囲" +ffVisibilityDescription: "あんたのフォロー/フォロワー情報の公開範囲を設定できるで。" continueThread: "さらにスレッドを見るで" deleteAccountConfirm: "アカウントを消すで?ええんか?" -incorrectPassword: "パスワードがちゃうわ。" -incorrectTotp: "ワンタイムパスワードが間違っとるか、期限が切れとるみたいやな。" +incorrectPassword: "パスワードがちゃうで。" voteConfirm: "「{choice}」に投票するんか?" hide: "隠す" useDrawerReactionPickerForMobile: "ケータイとかのときドロワーで表示するで" @@ -947,14 +878,11 @@ oneHour: "1時間" oneDay: "1日" oneWeek: "1週間" oneMonth: "1ヶ月" -threeMonths: "3ヶ月" -oneYear: "1年" -threeDays: "3日" reflectMayTakeTime: "反映されるまで時間がかかることがあるで" failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…" rateLimitExceeded: "レート制限が超えたみたいやで" -cropImage: "画像切り取り" -cropImageAsk: "画像を切り取ってもええか?" +cropImage: "画像のクロップ" +cropImageAsk: "画像をクロップしてもええか?" cropYes: "切り抜いたる" cropNo: "切り抜かへん" file: "ファイル" @@ -965,18 +893,18 @@ thereIsUnresolvedAbuseReportWarning: "未対応の通報があるみたいやで recommended: "推奨" check: "チェック" driveCapOverrideLabel: "このユーザーのドライブ容量上限を変更するで" -driveCapOverrideCaption: "0以下にしたら解除されるで。" -requireAdminForView: "これ見たいんなら管理者じゃないとアカンわ。" +driveCapOverrideCaption: "0以下を指定すると解除されるで。" +requireAdminForView: "これを見るには管理者アカウントでログインしとらなあかんで。" isSystemAccount: "システムが自動で作成・管理しとるアカウントやで。" -typeToConfirm: "これやるんなら {x} って入力してなー" +typeToConfirm: "この操作をやるんなら {x} と入力してなー" deleteAccount: "アカウント削除するで" document: "ドキュメント" numberOfPageCache: "ページ、どんだけキャッシュすんの?" -numberOfPageCacheDescription: "増やすと使いやすくなるけど、負荷とメモリ使用量が増えてくで。一長一短やな。" +numberOfPageCacheDescription: "増やすと使いやすくなる、負荷とメモリ使用量が増えてくで。一長一短やな。" logoutConfirm: "ログアウトしまっか?" lastActiveDate: "最後に使った日時" statusbar: "ステータスバー" -pleaseSelect: "選んだってやー" +pleaseSelect: "選択したってやー" reverse: "反転" colored: "色付き" refreshInterval: "更新間隔" @@ -985,28 +913,28 @@ type: "タイプ" speed: "速度" slow: "遅い" fast: "速い" -sensitiveMediaDetection: "きわどいやつの検出" -localOnly: "ローカルだけ" -remoteOnly: "リモートだけ" +sensitiveMediaDetection: "センシティブなメディアの検出" +localOnly: "ローカルのみ" +remoteOnly: "リモートのみ" failedToUpload: "アップロードに失敗してもうたわ…" -cannotUploadBecauseInappropriate: "きわどい内容を含むかもしれへんって言われたからアップロードできへんわ。" -cannotUploadBecauseNoFreeSpace: "ドライブがもうパンパンやからアップロードできへんわ。" +cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたからアップロードできへんわ。" +cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いからアップロードできへんわ。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルが思うたよりも大きいさかいアップロードできへんでこれ。" beta: "ベータ" -enableAutoSensitive: "自動できわどいか判断する" +enableAutoSensitive: "自動NSFW判定" enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。" -activeEmailValidationDescription: "ユーザーのメアドのバリデーションを、捨てアドかどうかとか、ちゃんと通信できるかとかを見るで。切ったら単に文字列として合っとるかどうかだけ見るわ。" +activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかとかを判定して積極的に行うで。オフにすると単に文字列として正しいかどうかだけチェックするで。" navbar: "ナビゲーションバー" shuffle: "シャッフルするで" account: "アカウント" -move: "移すで" +move: "移動するで" pushNotification: "プッシュ通知" subscribePushNotification: "プッシュ通知をオンにするで" unsubscribePushNotification: "プッシュ通知を止めるで" pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで" pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に対応してないみたいやで。" sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで" -sendPushNotificationReadMessageCaption: "あんたの端末の電池使う量が増えるかもしれん。" +sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。" windowMaximize: "最大化" windowMinimize: "最小化" windowRestore: "元に戻す" @@ -1016,31 +944,29 @@ tools: "ツール" cannotLoad: "読み込めへんで" numberOfProfileView: "プロフィール表示回数" like: "ええやん!" -unlike: "いいねやめる" +unlike: "いいねを解除" numberOfLikes: "いいね数" show: "表示" neverShow: "今後表示しない" remindMeLater: "また後で" didYouLikeMisskey: "Misskey気に入ってくれた?" -pleaseDonate: "Misskeyは{host}が使うとる無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。" -correspondingSourceIsAvailable: "{anchor}" +pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。" roles: "ロール" role: "ロール" noRole: "ロールはありまへん" normalUser: "一般ユーザー" undefined: "未定義" assign: "アサイン" -unassign: "アサインやめる" +unassign: "アサインを解除" color: "色" manageCustomEmojis: "カスタム絵文字の管理" -manageAvatarDecorations: "アバターを飾るモンの管理" youCannotCreateAnymore: "これ以上作れなさそうやわ" -cannotPerformTemporary: "ちょっといまは使えへんで" -cannotPerformTemporaryDescription: "操作し過ぎてちょっと今は使えへんくしとるで。ちょっと待ってからもっかいやってや。" +cannotPerformTemporary: "一時的に利用できへんで" +cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。" invalidParamError: "パラメータがエラー言うとりますわ" -invalidParamErrorDescription: "リクエストパラメータが変やわ。だいたいはバグやねんけど、もしかしたら入れた文字が多すぎるとかかもしれんから確認してや〜" +invalidParamErrorDescription: "リクエストパラメータに問題があんねん。普通はバグやねんけど、もしかすると入力した文字数が多すぎるとかの可能性もあるから確認してや〜" permissionDeniedError: "操作が拒否されてもうた。" -permissionDeniedErrorDescription: "このアカウントはこれやったらアカンって。" +permissionDeniedErrorDescription: "自分のアカウントにはこの操作を行う権限があらへんねん" preset: "プリセット" selectFromPresets: "プリセットから選ぶ" achievements: "実績" @@ -1050,16 +976,15 @@ thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。" thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingCancel: "やめとく" thisPostMayBeAnnoyingIgnore: "このまま投稿" -collapseRenotes: "見たことあるリノートは飛ばして表示するで" -collapseRenotesDescription: "リアクションやリノートをしたことがあるノートをたたんで表示するで。" +collapseRenotes: "見たことあるRenoteは飛ばして表示するで" internalServerError: "サーバー内部エラー" -internalServerErrorDescription: "サーバーでなんか変なこと起こっとるわ。" -copyErrorInfo: "エラー情報をコピるで" +internalServerErrorDescription: "サーバー内部でよう分からんエラーやわ" +copyErrorInfo: "エラー情報をコピー" joinThisServer: "このサーバーに登録するわ" exploreOtherServers: "他のサーバー見てみる" letsLookAtTimeline: "タイムライン見てみーや" disableFederationConfirm: "連合なしにしとくか?" -disableFederationConfirmWarn: "連合なしにしても投稿が非公開になるわけちゃうで。大体の場合は連合なしにする必要はないで。" +disableFederationConfirmWarn: "連合なしにしても投稿は非公開にはならへんで。大体の場合は連合なしにする必要はないで。" disableFederationOk: "連合なしにしとく" invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。" emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ" @@ -1068,19 +993,14 @@ cannotBeChangedLater: "後からは変えられへんで。" reactionAcceptance: "ツッコミの受け入れ" likeOnly: "いいねだけ" likeOnlyForRemote: "リモートからはいいねだけな" -nonSensitiveOnly: "いつ見ても大丈夫なやつだけ" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "いつ見ても大丈夫なやつだけ (リモートはいいねだけ)" +nonSensitiveOnly: "センシティブじゃないやつだけ" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "センシティブじゃないやつだけ (リモートはいいねだけ)" rolesAssignedToMe: "自分に割り当てられたロール" resetPasswordConfirm: "パスワード作り直すんでええな?" sensitiveWords: "けったいな単語" sensitiveWordsDescription: "設定した単語が入っとるノートの公開範囲をホームにしたるわ。改行で区切ったら複数設定できるで。" sensitiveWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。" -prohibitedWords: "禁止ワード" -prohibitedWordsDescription: "設定した言葉が含まれるノートを投稿しようとしたら、エラーが出るようにするで。改行で区切って複数設定できるで。" -prohibitedWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。" -hiddenTags: "見えてへんハッシュタグ" -hiddenTagsDescription: "設定したタグを最近流行りのとこに見えんようにすんで。複数設定するときは改行で区切ってな。" -notesSearchNotAvailable: "なんかノート探せへん。" +notesSearchNotAvailable: "ノート検索は使われへんで。" license: "ライセンス" unfavoriteConfirm: "ほんまに気に入らんの?" myClips: "自分のクリップ" @@ -1090,25 +1010,21 @@ retryAllQueuesConfirmTitle: "もっかいやってみるか?" retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。" enableChartsForRemoteUser: "リモートユーザーのチャートを作る" enableChartsForFederatedInstances: "リモートサーバーのチャートを作る" -enableStatsForFederatedInstances: "リモートサーバの情報を取得" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" -reactionsDisplaySize: "ツッコミの表示のでかさ" -limitWidthOfReaction: "ツッコミの最大横幅を制限して、ちっさく表示するで" +largeNoteReactions: "ノートのツッコミを大きする" noteIdOrUrl: "ノートIDかURL" video: "動画" videos: "動画" -audio: "音声" -audioFiles: "音声" dataSaver: "データケチケチ" accountMigration: "アカウントのお引っ越し" accountMoved: "このユーザーはさらのアカウントに引っ越したで:" -accountMovedShort: "このアカウントは引っ越し済みや" +accountMovedShort: "このアカウントは移行されとるで" operationForbidden: "この操作はできまへん" -forceShowAds: "いっつも広告を映す" +forceShowAds: "常に広告を表示しとく" addMemo: "メモを足す" editMemo: "メモをいらう" reactionsList: "ツッコミ一覧" -renotesList: "リノート一覧" +renotesList: "Renote一覧" notificationDisplay: "通知見せる" leftTop: "左上" rightTop: "右上" @@ -1120,14 +1036,12 @@ horizontal: "横" position: "位置" serverRules: "サーバールール" pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、下に書いてること確認してな。" -pleaseAgreeAllToContinue: "続けるんやったら、全部にチェック入れとかなアカンで。" +pleaseAgreeAllToContinue: "続けるんやったら、全ての「せやな」にチェック入れてる必要があるで。" continue: "続けるで" preservedUsernames: "予約ユーザー名" preservedUsernamesDescription: "予約しとくユーザー名を行ごとに挙げるで。ここで指定されたユーザー名はアカウント作るときに使えへんくなるけど、管理者は例外や。あと、もうあるアカウントも例外やな。" createNoteFromTheFile: "このファイル使うてノート作るで" archive: "アーカイブ" -archived: "アーカイブ済み" -unarchive: "アーカイブ解除" channelArchiveConfirmTitle: "{name}をアーカイブしてええか?" channelArchiveConfirmDescription: "アーカイブしたら、チャンネル一覧とか検索結果からなくなるし、新しく書き込みもできへんなるで。" thisChannelArchived: "このチャンネル、アーカイブされとるで。" @@ -1138,9 +1052,6 @@ preventAiLearning: "生成AIの学習に使わんといて" preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したノートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。" options: "オプション" specifyUser: "ユーザー指定" -lookupConfirm: "照会するけどええか?" -openTagPageConfirm: "ハッシュタグのページを開くんか?" -specifyHost: "ホスト指定" failedToPreviewUrl: "プレビューできへん" update: "更新" rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール" @@ -1151,343 +1062,42 @@ changeReactionConfirm: "ツッコミを別のに変えるか?" later: "あとで" goToMisskey: "Misskeyへ" additionalEmojiDictionary: "絵文字の追加辞書" -installed: "インストールしとる" -branding: "ブランディング" +installed: "インストール済み" +branding: "あ" enableServerMachineStats: "サーバーのマシン情報見せびらかすで" enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" -turnOffToImprovePerformance: "オフにしたらえらい軽うなるで。" -createInviteCode: "招待コード作る" -createWithOptions: "オプション決めて作る" -createCount: "作った数" -inviteCodeCreated: "招待コード作ったで" -inviteLimitExceeded: "招待コード作りすぎやで。" -createLimitRemaining: "作れる招待コードは残り {limit} 個や" -inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作れるで。" -expirationDate: "有効期限" -noExpirationDate: "期限なし" -inviteCodeUsedAt: "招待コードが使われた時" -registeredUserUsingInviteCode: "招待コードを使うた人" -waitingForMailAuth: "メール認証待ち" -inviteCodeCreator: "招待コードを作った人" -usedAt: "使った時" -unused: "つこてへん" -used: "もうつこてる" -expired: "期限切れ" -doYouAgree: "ええんか?" -beSureToReadThisAsItIsImportant: "重要やから絶対読んでや。" -iHaveReadXCarefullyAndAgree: "「{x}」の内容をよう読んで、同意するで。" -dialog: "ダイアログ" -icon: "アイコン" -forYou: "あんたへ" -currentAnnouncements: "現在のお知らせやで" -pastAnnouncements: "過去のお知らせやで" -youHaveUnreadAnnouncements: "あんたまだこのお知らせ読んどらんやろ。" -useSecurityKey: "ブラウザまたはデバイスの言う通りに、セキュリティキーまたはパスキーを使ってや。" -replies: "返事" -renotes: "リノート" -loadReplies: "返信を見るで" -loadConversation: "会話を見るで" -pinnedList: "ピン留めしはったリスト" -keepScreenOn: "デバイスの画面を常にオンにすんで" -verifiedLink: "このリンク先の所有者ってわかったわ。" -notifyNotes: "投稿を通知" -unnotifyNotes: "投稿の通知やめる" -authentication: "認証" -authenticationRequiredToContinue: "続けるんなら認証してや。" -dateAndTime: "日時" -showRenotes: "リノート出す" -edited: "いじったやつ" -notificationRecieveConfig: "通知もらうかの設定" -mutualFollow: "お互いフォローしてんで" -followingOrFollower: "フォロー中またはフォロワー" -fileAttachedOnly: "ファイルのっけてあるやつだけ" -showRepliesToOthersInTimeline: "タイムラインに他の人への返信とかも入れるで" -hideRepliesToOthersInTimeline: "タイムラインに他の人への返信とかは入れへん" -showRepliesToOthersInTimelineAll: "タイムラインに今フォローしとる人全員の返信入れるで" -hideRepliesToOthersInTimelineAll: "タイムラインに今フォローしとる人の返信入れへん" -confirmShowRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れるか?" -confirmHideRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れへんのか?" -externalServices: "他のサイトのサービス" -sourceCode: "ソースコード" -sourceCodeIsNotYetProvided: "ソースコードはまだ提供されてへんで。問題の修正について管理者に問い合わせてみ。" -repositoryUrl: "リポジトリURL" -repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入するで。Misskeyをそのまんま(ソースコードにいかなる変更も加えずに)使っとる場合は https://github.com/misskey-dev/misskey と記入するで。" -repositoryUrlOrTarballRequired: "リポジトリを公開してへんなら、代わりにtarballを提供する必要があるで。詳細は.config/example.ymlを参照してな。" -feedback: "フィードバック" -feedbackUrl: "フィードバックURL" -impressum: "運営者の情報" -impressumUrl: "運営者の情報URL" -impressumDescription: "ドイツとかの一部んところではな、表示が義務付けられてんねん(Impressum)。" -privacyPolicy: "プライバシーポリシー" -privacyPolicyUrl: "プライバシーポリシーURL" -tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" -avatarDecorations: "アイコンデコレーション" -attach: "のっける" -detach: "取る" -detachAll: "全部とる" -angle: "角度" -flip: "反転" -showAvatarDecorations: "アイコンのデコレーション映す" -releaseToRefresh: "離したらリロード" -refreshing: "リロードしとる" -pullDownToRefresh: "引っ張ってリロードするで" -useGroupedNotifications: "通知をグループ分けして出すで" -signupPendingError: "メアド確認してたらなんか変なことなったわ。リンクの期限切れてるかもしれん。" -cwNotationRequired: "「内容を隠す」んやったら注釈書かなアカンで。" -doReaction: "ツッコむで" -code: "コード" -reloadRequiredToApplySettings: "設定を見るんにはリロードが必要やで。" -remainingN: "残り:{n}" -overwriteContentConfirm: "今の内容に上書きされるけどいい?" -seasonalScreenEffect: "季節にあった画面の動き" -decorate: "デコる" -addMfmFunction: "装飾つける" -enableQuickAddMfmFunction: "ややこしいMFMのピッカーを出す" -bubbleGame: "バブルゲーム" -sfx: "効果音" -soundWillBePlayed: "サウンドが再生されるで" -showReplay: "リプレイ見る" -replay: "リプレイ" -replaying: "リプレイ中" -endReplay: "リプレイを終了" -copyReplayData: "リプレイデータをコピー" -ranking: "ランキング" -lastNDays: "直近{n}日" -backToTitle: "タイトルへ" -hemisphere: "住んでる地域" -withSensitive: "センシティブなファイルを含むノートを表示" -userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" -enableHorizontalSwipe: "スワイプしてタブを切り替える" -loading: "読み込み中" -surrender: "やめとく" -gameRetry: "もういっちょ" -notUsePleaseLeaveBlank: "使用せえへん場合は空欄にしてや" -useTotp: "ワンタイムパスワードを使う" -useBackupCode: "バックアップコードを使う" -launchApp: "アプリを起動" -useNativeUIForVideoAudioPlayer: "動画・音声の再生にブラウザのUIを使用する" -keepOriginalFilename: "オリジナルのファイル名を保持" -keepOriginalFilenameDescription: "この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられるで。" -noDescription: "説明文はあらへんで" -alwaysConfirmFollow: "フォローの際常に確認する" -inquiry: "問い合わせ" -tryAgain: "もう一度試しいや。" -confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する" -sensitiveMediaRevealConfirm: "センシティブなメディアやで。表示するんか?" -createdLists: "作成したリスト" -createdAntennas: "作成したアンテナ" -fromX: "{x}から" -genEmbedCode: "埋め込みコードを作る" -noteOfThisUser: "このユーザーのノート全部" -clipNoteLimitExceeded: "これ以上このクリップにノート追加でけへんわ。" -performance: "パフォーマンス" -modified: "変更あり" -discard: "やめる" -thereAreNChanges: "{n}個の変更があるみたいや" -signinWithPasskey: "パスキーでログイン" -unknownWebAuthnKey: "登録されてへんパスキーやな。" -passkeyVerificationFailed: "パスキーの検証に失敗したで。" -passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証は成功したんやけど、パスワードレスログインが無効になっとるわ。" -messageToFollower: "フォロワーへのメッセージ" -target: "対象" -testCaptchaWarning: "CAPTCHAのテストを目的としてるで。絶対に本番環境で使わんといてな。絶対やで。" -prohibitedWordsForNameOfUser: "禁止ワード(ユーザー名)" -prohibitedWordsForNameOfUserDescription: "このリストの中にある文字列がユーザー名に入っとったら、その名前に変更できひんようになるで。モデレーター権限があるユーザーは除外や。" -yourNameContainsProhibitedWords: "その名前は禁止した文字列が含まれとるで" -yourNameContainsProhibitedWordsDescription: "その名前は禁止した文字列が含まれとるわ。どうしてもって言うなら、サーバー管理者に言うしかないで。" -thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者が、表示にログインが要るって設定してるで" -lockdown: "ロックダウン" -pleaseSelectAccount: "アカウント選んでや" -availableRoles: "使えるロール" -acknowledgeNotesAndEnable: "注意事項をわかった上でオンにする。" -federationSpecified: "このサーバーはホワイトリスト連合で運用されてるで。管理者が指定したサーバー以外とはやり取りできひんで。" -federationDisabled: "このサーバーは連合が無効化されてるで。他のサーバーのユーザーとやり取りすることはできひんで。" -confirmOnReact: "ツッコむときに確認とる" -reactAreYouSure: "\" {emoji} \" でツッコむ?" -postForm: "投稿フォーム" -information: "情報" -_chat: - invitations: "来てや" - noHistory: "履歴はないわ。" - members: "メンバーはん" - home: "ホーム" - send: "送信" -_settings: - webhook: "Webhook" -_accountSettings: - requireSigninToViewContents: "ログインしてもらってからコンテンツ見てもらう" - requireSigninToViewContentsDescription1: "あなたが作成した全部のノートとかのコンテンツを見れるようにするのにログインがいるようにするで。クローラーにいろいろ収集されるんを防げるかもしれん。" - requireSigninToViewContentsDescription2: "URLプレビュー(OGP)、Webページへの埋め込み、ノートの引用に対応してないサーバーからの表示ができんくなるで。" - requireSigninToViewContentsDescription3: "リモートサーバーに連合されたコンテンツは、これらの制限が適用されんかもしれんで。" - makeNotesFollowersOnlyBefore: "昔のノートをフォロワーだけに見てもらう" - makeNotesFollowersOnlyBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" - makeNotesHiddenBefore: "昔のノートを見れんようにする" - makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" - mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばんかもしれん。" - notesHavePassedSpecifiedPeriod: "決めた時間が経ったノート" - notesOlderThanSpecifiedDateAndTime: "決めた日時より前のノート" -_abuseUserReport: - forward: "転送" - forwardDescription: "匿名のシステムアカウントってことにして、リモートサーバーに通報を転送するで。" - resolve: "解決" - accept: "ええよ" - reject: "あかんよ" - resolveTutorial: "内容がええなら「ええよ」を選ぶんや。肯定的に解決されたことにして記録するで。\n逆に、内容がだめなら「あかんよ」を選びいや。否定的に解決されたって記録しとくで。" -_delivery: - status: "配信状態" - stop: "配信せぇへん" - resume: "配信再開" - _type: - none: "配信しとる" - manuallySuspended: "手動停止中" - goneSuspended: "サーバー削除のため停止中" - autoSuspendedForNotResponding: "サーバー応答せえへんから停止中" -_bubbleGame: - howToPlay: "遊び方" - hold: "ホールド" - _score: - score: "スコア" - scoreYen: "稼いだ金額" - highScore: "ハイスコア" - maxChain: "最大チェーン数" - yen: "{yen}円" - estimatedQty: "{qty}個分" - scoreSweets: "おにぎり {onigiriQtyWithUnit}" - _howToPlay: - section1: "位置を調整してハコにモノを落とすで。" - section2: "同じもんがくっついたら別のやつになって、スコアがもらえるで。" - section3: "モノがハコからあふれたらゲームオーバーや。ハコからあふれんようにしながらモノを融合させてハイスコアを目指しいや!" -_announcement: - forExistingUsers: "もうおるユーザーのみ" - forExistingUsersDescription: "オンにしたらこのお知らせができた時点でおる人らにだけお知らせが行くで。切ったらこの知らせが行ったあとにアカウント作った人にもちゃんとお知らせが行くで。" - needConfirmationToRead: "既読にするんやったら確認してや" - needConfirmationToReadDescription: "オンにしたら、このお知らせを既読にする時に確認するで。ついでに、一括既読しても既読扱いにならへんで。" - end: "お知らせやめる" - tooManyActiveAnnouncementDescription: "お知らせが多すぎてUXが落ちそうや。終わったお知らせはアーカイブに突っ込んだほうがええかも。" - readConfirmTitle: "既読にしてええんやな?" - readConfirmText: "「{title}」はもう読んだから既読にするで。" - shouldNotBeUsedToPresentPermanentInfo: "新規ユーザーのUXを損ねやすいから、お知らせはストック情報やのうてフロー情報の掲示に使った方がええで。" - dialogAnnouncementUxWarn: "ダイアログ形式のお知らせがいっぺんに2コ以上ある場合、UXに良うないことが多いから、使うんは慎重にすんのがおすすめやで。" - silence: "通知せんで" - silenceDescription: "オンにすると、このお知らせは通知されへんし、既読にする必要もなくなるで。" _initialAccountSetting: accountCreated: "アカウント作り終わったで。" letsStartAccountSetup: "アカウントの初期設定をしよか。" - letsFillYourProfile: "最初はあんたのプロフィールを設定するで。" + letsFillYourProfile: "最初はあんたのプロフィールを設定しよか。" profileSetting: "プロフィール設定" privacySetting: "プライバシー設定" theseSettingsCanEditLater: "この設定はあとから変えれるで。" youCanEditMoreSettingsInSettingsPageLater: "これ以外にもいろんな設定を「設定」ページからできるで。後で確認してみてな。" followUsers: "タイムラインを構築するために、気になるユーザーをフォローしてみ。" pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をあんたのデバイスで受け取れるで。" - initialAccountSettingCompleted: "初期設定終わりや!" + initialAccountSettingCompleted: "初期設定が終わったで。" haveFun: "{name}、楽しんでな~" - youCanContinueTutorial: "こんまま{name}(Misskey)の使い方のチュートリアルにも行けるけど、ここでやめてすぐに使い始めてもええで。" - startTutorial: "チュートリアルはじめる" + ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。" skipAreYouSure: "初期設定飛ばすか?" laterAreYouSure: "初期設定あとでやり直すん?" -_initialTutorial: - launchTutorial: "チュートリアル見るで" - title: "チュートリアルやで" - wellDone: "やるやん" - skipAreYouSure: "チュートリアルやめるか?" - _landing: - title: "チュートリアルによう来たな" - description: "ここでは、Misskeyのカンタンな使い方とか機能を確かめれんで。" - _note: - title: "ノートってなんや?" - description: "Misskeyでの投稿は「ノート」って呼ばれてんで。ノートは順々にタイムラインに載ってて、リアルタイムで新しくなってってんで。" - reply: "返信もできるで。返信の返信もできるから、スレッドっぽく会話をそのまま続けれもするで。" - renote: "そのノートを自分のタイムラインに流して共有できるで。テキスト入れて引用してもええな。" - reaction: "ツッコミをつけることもできるで。細かいことは次のページや。" - menu: "ノートの詳細を出したり、リンクをコピーしたり、いろいろできんねん。" - _reaction: - title: "ツッコミってなんや?" - description: "ノートには「ツッコミ」できんねん。「いいね」とか何言っとるかわからんし、簡単に表現できるのはええことやん?" - letsTryReacting: "ノートの「+」ボタンでツッコめるわ。試しに下のノートにツッコんでみ。" - reactToContinue: "ツッコんだら進めるようになるで。" - reactNotification: "あんたのノートが誰かにツッコまれたら、すぐ通知するで。" - reactDone: "「ー」ボタンでツッコミやめれるで。" - _timeline: - title: "タイムラインのしくみ" - description1: "Misskeyには、いろいろタイムラインがあんで(ただ、サーバーによっては無効化されてるところもあるな)。" - home: "あんたがフォローしてるアカウントの投稿が見れんねん。" - local: "このサーバーの中におる全員の投稿が見れるで。" - social: "ホームタイムラインの投稿もローカルタイムラインのも一緒に見れるで。" - global: "繋がってる他の全サーバーからの投稿が見れるで。" - description2: "それぞれのタイムラインは、いつでも画面上で切り替えられんねん。覚えとき。" - description3: "その他にも、リストタイムラインとかチャンネルタイムラインとかがあんねん。詳しいのは{link}を見とき。" - _postNote: - title: "ノートの投稿設定" - description1: "Misskeyにノートを投稿するとき、いろんなオプションが付けれるで。投稿画面はこんな感じや。" - _visibility: - description: "ノートを見れる相手を制限できるわ。" - public: "みんなに見せるで。" - home: "ホームタイムラインにだけ見せるで。フォロワーとか、プロフィールを見に来た人、リノートからも見れるから、実質は全員見れるけどな。あんまし広がりにくいってことや。" - followers: "フォロワーにだけ見せるで。自分以外はリノートできへんし、フォロワー以外は絶対に見れへん。" - direct: "指定した人にだけ公開されて、ついでに通知も送るで。ダイレクトメールの代わりとして使ってな。" - doNotSendConfidencialOnDirect1: "機密情報を送るときは十分注意せえよ。" - doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容が見れるから、信用できへんサーバーのひとにダイレクト投稿するときには、めっちゃ用心しとくんやで。" - localOnly: "他のサーバーに投稿せえへんくなるで。他の公開範囲とか一切無視して、他のサーバーの人らはこの設定がされたノートは絶対に見れへん。" - _cw: - title: "内容隠し(CW)" - description: "本文のかわりに「注釈」に書いた内容だけ見せるで。「続き見して!」を押すと本文も見れんねん。" - _exampleNote: - cw: "飯テロ注意" - note: "チョコドーナツめっちゃ美味かったわ🍩😋" - useCases: "サーバーのガイドラインに決められとるノートに使うたり、ネタバレとかきわどい内容を自分で隠したりするとき用やな。" - _howToMakeAttachmentsSensitive: - title: "のっけたファイルをセンシティブにするんは?" - description: "サーバーのガイドラインに書いてあったり、そのままおっぴろげとくのはあんま良うないファイルには「センシティブ」っちゅう設定をつけるんや。" - tryThisFile: "試しに、これにのっけてある画像をセンシティブにしてみいや!" - _exampleNote: - note: "納豆のフタ開けるときにやらかしてもうた…" - method: "のっけたファイルをセンシティブにするときは、そのファイルを押してメニュー開けて、「ちょっとこれはアカン」を押すんよ。" - sensitiveSucceeded: "ファイルをのっけるときは、サーバーの言うこと聞いてちゃんと設定するんやで。" - doItToContinue: "画像をちゃんと設定したら先に進めるで。" - _done: - title: "チュートリアル終わり!おつかれさん🎉" - description: "ここで紹介したのは全部の中のちょび~っとだけや。もっと使い方知りたいんやったら、{link}を見ときや。" -_timelineDescription: - home: "ホームタイムラインは、あんたがフォローしとるアカウントの投稿だけ見れるで。" - local: "ローカルタイムラインは、このサーバーにおる全員の投稿を見れるで。" - social: "ソーシャルタイムラインは、ホームタイムラインの投稿もローカルタイムラインのも一緒に見れるで。" - global: "グローバルタイムラインは、繋がっとる他のサーバーの投稿、全部ひっくるめて見れんで。" _serverRules: - description: "新規登録前に見せる、サーバーのカンタンなルールを決めるで。内容は使うための決め事の要約がええと思うわ。" -_serverSettings: - iconUrl: "アイコン画像のURL" - appIconDescription: "{host}がアプリとして表示してるんやつをアイコンを指定すんで。" - appIconUsageExample: "例えば、PWAとか、スマホのホームにブックマークしたときとか" - appIconStyleRecommendation: "円か角丸に切り取られることがあるさかい、塗り潰した余白のある背景があるものがおすすめや。" - appIconResolutionMustBe: "解像度は絶対{resolution}じゃないとアカン。" - manifestJsonOverride: "manifest.jsonのオーバーライド" - shortName: "略称" - shortNameDescription: "サーバーの名前が長ったらしい時に、代わりに出すあだ名。" - fanoutTimelineDescription: "入れると、おのおのタイムラインを取得するときにめちゃめちゃ動きが良うなって、データベースが軽くなるわ。でも、Redisのメモリ使う量が増えるから注意な。サーバーのメモリが足りんときとか、動きが変なときは切れるで。" - fanoutTimelineDbFallback: "データベースにフォールバックする" - fanoutTimelineDbFallbackDescription: "有効にしたら、タイムラインがキャッシュん中に入ってないときにDBにもっかい問い合わせるフォールバック処理ってのをやっとくで。切ったらフォールバック処理をやらんからサーバーはもっと軽くなんねんけど、タイムラインの取得範囲がちょっと減るで。" - reactionsBufferingDescription: "有効にしたら、リアクション作るときのパフォーマンスがすっごい上がって、データベースへの負荷が減るで。代わりに、Redisのメモリ使用は増えるで。" - inquiryUrl: "問い合わせ先URL" - inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定するで。" - openRegistration: "アカウントの作成をオープンにする" - openRegistrationWarning: "登録を解放するのはリスクが伴うで。サーバーをいっつも監視して、なんか起きたらすぐに対応できるんやったら、オンにしてもええと思う。" - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターがおらんかったら、スパムを防ぐためにこの設定は勝手に切られるで。" + description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。" _accountMigration: moveFrom: "別のアカウントからこのアカウントに引っ越す" moveFromSub: "別のアカウントへエイリアスを作る" - moveFromLabel: "引っ越しする前のアカウント #{n}" - moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引っ越ししたいんなら、ここでエイリアスを作っとかなアカンで。\n引っ越す前のアカウントをこんな感じに入力してや: @username@server.example.com\n入力欄空っぽやったら消しとくで(おすすめはせえへん)。" + moveFromLabel: "引っ越し元のアカウント:" + moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したかったら、ここでエイリアスを作っとく必要があるで。必ずお引っ越しを実行する前に作っとかなあかんで!引っ越し元のアカウントをこんな風に入力してくれへんか?:@person@instance.com" moveTo: "このアカウントをさらのアカウントに引っ越すで" - moveToLabel: "引っ越し先のアカウント:" - moveCannotBeUndone: "アカウント引っ越したらもう戻せへん。" + moveToLabel: "引っ越し先のアカウント:" + moveCannotBeUndone: "アカウントを移行すると、取り消すことはできへんくなります。" moveAccountDescription: "おニューのアカウントに移行すんで。\n ・フォロワーがおニューの方を勝手にフォローすんで。\n ・このアカウントからのフォローはまるまる全部解除されんで。\n ・このアカウントでノート作れへんようになるで。\n\nフォロワーの移行は勝手にこっちでやっとくけど、フォローの移行は自分でしてや。移行前にこのアカウントでフォローエクスポートして、移行したあとすぐにおニューのところでインポートしてくれな。\nリストとかミュート、あとブロックもおんなじや。自分で移行してな。\n\n(この説明はこのサーバー、つまりMisskey v13.12.0から後の仕様や。Mastodonとか他のActivityPubソフトやとちょっと挙動が違うこともあんで。)" - moveAccountHowTo: "アカウントの引っ越しには、まず引っ越し先のアカウントで自分のアカウントに対しエイリアスを作ってな。\nエイリアス作ったら、引っ越し先のアカウントをこんな感じに入れてや: @username@server.example.com" - startMigration: "引っ越す" + moveAccountHowTo: "アカウントの引っ越しには、まず引っ越し先のアカウントで自分のアカウントに対しエイリアスを作成しなはれや。\nエイリアス作成した後、引っ越し先のアカウントを次のように入力してくれへんか?:@username@server.example.com" + startMigration: "引っ越しする" migrationConfirm: "ほんまにこのアカウントを {account} に引っ越すんか?一回引っ越してもうたら取り消されへんし、二度とこのアカウントを元に戻されへんくなるで。\nそれと、引っ越し先のアカウントでエイリアスが作れたかちゃ~んと確認しーや?" - movedAndCannotBeUndone: "\nアカウントはもう引っ越し済みや。\nこれはもう戻せへん。" + movedAndCannotBeUndone: "\nアカウントはもう引っ越されてます。\n引っ越しを取り消すことはできまへん。" postMigrationNote: "このアカウントからのフォロー解除は移行操作から丸一日経ったら実行されんで。\nこのアカウントのフォロー・フォロワー数はどっちも0や。フォローの解除はされへんから、あんたのフォロワーはこのアカウントのフォロワー向けの投稿をこの後も見れるで。" - movedTo: "引っ越し先のアカウント:" + movedTo: "引っ越し先のアカウント:" _achievements: earnedAt: "貰った日ぃ" _types: @@ -1511,10 +1121,10 @@ _achievements: title: "箕面の滝からノート" description: "ノートを5,000回投稿した" _notes10000: - title: "えげつないノート" + title: "スーパーノート" description: "ノートを10,000回投稿した" _notes20000: - title: "もっとノートよこせ!" + title: "ニードモアノート" description: "ノートを20,000回投稿した" _notes30000: title: "ノートノートノート" @@ -1611,7 +1221,7 @@ _achievements: title: "はじめてのフォロー" description: "初めてフォローした" _following10: - title: "すたこらさっさ" + title: "ついてく、ついてく" description: "フォローが10人超えた" _following50: title: "友達ぎょうさん" @@ -1664,10 +1274,10 @@ _achievements: description: "クライアント付けてから1時間経ってもうたで。" _noteDeletedWithin1min: title: "*おおっと*" - description: "投稿してから1分以内にその投稿をほかした" + description: "投稿してから1分以内にその投稿を消した" _postedAtLateNight: title: "夜行性" - description: "真夜中にノートを投稿した" + description: "深夜にノートを投稿した" flavor: "そろそろ寝よか" _postedAt0min0sec: title: "時報" @@ -1684,7 +1294,7 @@ _achievements: description: "サーバーのチャートを表示した" _outputHelloWorldOnScratchpad: title: "Hello, world!" - description: "スクラッチパッドで hello world を出力した" + description: "スクラッチパッドで hello worldを出力した" _open3windows: title: "マド開けすぎ" description: "ウィンドウを3つ以上開いた状態にした" @@ -1693,7 +1303,7 @@ _achievements: description: "ドライブのフォルダを再帰的な入れ子にしようとした" _reactWithoutRead: title: "ちゃんと読んだんか?" - description: "100文字以上のノートに投稿3秒以内にツッコんだ" + description: "100文字以上のテキストを含むノートに投稿されてから3秒以内にツッコんだ" _clickedClickHere: title: "ここをクリック" description: "ここをクリックした" @@ -1702,7 +1312,7 @@ _achievements: description: "10秒ごとに0.005%の確率で獲得" _setNameToSyuilo: title: "神様コンプレックス" - description: "名前を syuilo にした" + description: "名前を syuilo に設定した" _passedSinceAccountCreated1: title: "一周年" description: "アカウント作成から1年経過した" @@ -1727,19 +1337,6 @@ _achievements: title: "Brain Diver" description: "Brain Diverへのリンクを投稿したった" flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "心配性" - description: "通知のテストしすぎやって" - _tutorialCompleted: - title: "Misskeyひよっこ講座 修了証" - description: "チュートリアル全部やった" - _bubbleGameExplodingHead: - title: "🤯" - description: "バブルゲームで最も大きいモノを出した" - _bubbleGameDoubleExplodingHead: - title: "ダブル🤯" - description: "バブルゲームで最も大きいモノを2つ同時に出した" - flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて" _role: new: "ロールの作成" edit: "ロールの編集" @@ -1750,76 +1347,55 @@ _role: assignTarget: "アサイン" descriptionOfAssignTarget: "マニュアルは誰がこのロールに含まれてるかを手動で管理するで。\nコンディショナルは条件を設定して、それに合うユーザーが自動で含まれるようになるで。" manual: "マニュアル" - manualRoles: "マニュアルロール" conditional: "コンディショナル" - conditionalRoles: "コンディショナルロール" condition: "条件" isConditionalRole: "これはコンディショナルロールやで" isPublic: "ロールを公開" - descriptionOfIsPublic: "プロフィールでこのロールが出されるで。" + descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができるで。そんで、ユーザーのプロフィールでこのロールが表示されるで。" options: "オプション" policies: "ポリシー" baseRole: "ベースロール" - useBaseValue: "ベースロールの値使う" - chooseRoleToAssign: "アサインするロール選ぶ" + useBaseValue: "ベースロールの値を使用" + chooseRoleToAssign: "アサインするロールを選択" iconUrl: "アイコン画像のURL" asBadge: "バッジとして見せる" descriptionOfAsBadge: "オンにすると、ユーザー名の横んとこにロールのアイコンが表示されるで。" - isExplorable: "ユーザーを見つけやすくしたる" - descriptionOfIsExplorable: "オンにしたらロールの面子一覧が「みつける」で公開されるし、ロールのタイムラインが使えるようになるで。" + isExplorable: "ロールタイムラインを公開するで〜" + descriptionOfIsExplorable: "オンにしたらロールのタイムラインを公開するで〜。でもロールの公開をオフにしたら公開されへんよ。" displayOrder: "表示順" descriptionOfDisplayOrder: "数がでかいほど、UI上で先に表示されるで。" - canEditMembersByModerator: "モデレーターがメンバーいじるのを許す" - descriptionOfCanEditMembersByModerator: "オンにすると、管理者だけやなくてモデレーターもこのロールにユーザーを入れたり抜いたりできるで。オフにすると管理者だけしかやれへんくなるで。" + canEditMembersByModerator: "モデレーターのメンバー編集を許可" + descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。" priority: "優先度" _priority: low: "低い" - middle: "中くらい" + middle: "中" high: "高い" _options: - gtlAvailable: "グローバルタイムライン見る" - ltlAvailable: "ローカルタイムライン見る" - canPublicNote: "パブリック投稿できるか" - mentionMax: "ノート内の最大メンション数" - canInvite: "サーバー招待コード作る" - inviteLimit: "招待コード作れる数" - inviteLimitCycle: "招待コードの作れる間隔" - inviteExpirationTime: "招待コードの期限" + gtlAvailable: "グローバルタイムラインの閲覧" + ltlAvailable: "ローカルタイムラインの閲覧" + canPublicNote: "パブリック投稿の許可" + canInvite: "サーバー招待コードの発行" canManageCustomEmojis: "カスタム絵文字の管理" - canManageAvatarDecorations: "アバターを飾るモンの管理" driveCapacity: "ドライブ容量" alwaysMarkNsfw: "勝手にファイルにNSFWをくっつける" - canUpdateBioMedia: "アイコンとバナーの更新を許可" - pinMax: "ノートピン留めできる数" - antennaMax: "アンテナ作れる数" + pinMax: "ノートのピン留めの最大数" + antennaMax: "アンテナの作成可能数" wordMuteMax: "ワードミュートの最大文字数" - webhookMax: "Webhook作れる数" - clipMax: "クリップ作れる数" - noteEachClipsMax: "クリップの中にノート作れる数" - userListMax: "ユーザーリスト作れる数" + webhookMax: "Webhockの作成可能数" + clipMax: "クリップの作成可能数" + noteEachClipsMax: "クリップ内のノートの最大数" + userListMax: "ユーザーリストの作成可能数" userEachUserListsMax: "ユーザーリスト内のユーザーの最大数" rateLimitFactor: "レートリミット" descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。" - canHideAds: "広告映さへん" - canSearchNotes: "ノート探せるかどうか" - canUseTranslator: "翻訳使えるかどうか" - avatarDecorationLimit: "アイコンデコのいっちばんつけれる数" - canImportAntennas: "アンテナのインポートを許す" - canImportBlocking: "ブロックのインポートを許す" - canImportFollowing: "フォローのインポートを許す" - canImportMuting: "ミュートのインポートを許す" - canImportUserLists: "リストのインポートを許す" + canHideAds: "広告を表示させへん" + canSearchNotes: "ノート検索を使わすかどうか" _condition: - roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" isRemote: "リモートユーザー" - isCat: "猫ユーザー" - isBot: "botユーザー" - isSuspended: "サスペンド済みユーザー" - isLocked: "鍵アカウントユーザー" - isExplorable: "「アカウントを見つけやすくする」が有効なユーザー" - createdLessThan: "アカウント作ってから~以内" - createdMoreThan: "アカウント作ってから~経過" + createdLessThan: "アカウント作成から~以内" + createdMoreThan: "アカウント作成から~経過" followersLessThanOrEq: "フォロワー数が~以下" followersMoreThanOrEq: "フォロワー数が~以上" followingLessThanOrEq: "フォロー数が~以下" @@ -1828,46 +1404,40 @@ _role: notesMoreThanOrEq: "投稿を~以上しとる" and: "~かつ~" or: "~または~" - not: "~じゃない" + not: "~ではない" _sensitiveMediaDetection: - description: "機械学習で自動できわどいメディアを検出して、運営しやすくするで。でもサーバーが少し重くなってまうわ。" + description: "機械学習を使って自動でセンシティブなメディアを検出して、モデレーションに役立てることができるで。サーバーの負荷が少し増えてまうなあ。" sensitivity: "検出感度やで" sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減るで。感度を高くすると、検知漏れ(偽陰性)が減るで。" - setSensitiveFlagAutomatically: "センシティブフラグを設定するで" - setSensitiveFlagAutomaticallyDescription: "この設定切っても内部的には判定結果はそのままや。" + setSensitiveFlagAutomatically: "NSFWフラグを設定するで" + setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されるで。" analyzeVideos: "動画の解析をオンにするで" - analyzeVideosDescription: "画像だけじゃなくて動画も解析するようにするで。サーバーがちょっと重くなるで。" + analyzeVideosDescription: "画像に加えて動画も解析するようにするで。鯖の負荷が少し増えるで。" _emailUnavailable: - used: "もう使われとるわ" + used: "もう使われとるで" format: "形式がおかしいで" - disposable: "ずーっと使えるアドレスじゃないみたいや" - mx: "正しいメールサーバーじゃないっぽいわ" - smtp: "メールサーバーがうんともすんとも言わへん" - banned: "このメールアドレスはあかん" + disposable: "永久に使えるアドレスじゃないみたいやで" + mx: "正しいメールサーバーじゃない見たいやで" + smtp: "メールサーバーが応答してないみたいや" _ffVisibility: public: "公開" followers: "フォロワーだけに公開" private: "非公開" _signup: - almostThere: "ほぼ終わったようなもんや" + almostThere: "ほぼ完了やで" emailAddressInfo: "あんたが使っとるメアドを入力してなー。入れたメアドが公開されることはないで。" - emailSent: "さっき入れたメアド({email})宛に確認メールを送ったで。メールに書かれたリンク押してアカウント作るの終わらしてな。\nメールの認証リンクの期限は30分や。" + emailSent: "さっき入れたメールアドレス({email})宛に確認のメールが送られたで。メールに書かれたリンクにアクセスすれば、アカウントの作成が完了や!" _accountDelete: accountDelete: "アカウントの削除" - mayTakeTime: "アカウント消すんはサーバーが重いんやって。やから作ったコンテンツとか上げたファイルの数が多いと消し終わるまでに時間がかかるかもしれへん。" - sendEmail: "アカウントの消し終わるときは、登録してたメアドに通知するで。" - requestAccountDelete: "アカウント削除頼む" + mayTakeTime: "アカウントの削除は負荷がかかる処理やねんて。やから作ったコンテンツの数や上げたファイルの数が多いと削除が終わるまでに時間がかかることがあるんやって。" + sendEmail: "アカウントの削除が終わるときは、登録してたメールアドレス宛に通知を送るで。" + requestAccountDelete: "アカウント削除をリクエスト" started: "削除処理が始まったで。" - inProgress: "今消しよるで" + inProgress: "削除が進んでるで" _ad: back: "戻る" - reduceFrequencyOfThisAd: "この広告ちょっとうざったらしいわ" + reduceFrequencyOfThisAd: "この広告の表示頻度を下げるで" hide: "表示せん" - timezoneinfo: "曜日はサーバーのタイムゾーンを元に決めるで。" - adsSettings: "広告配信設定" - notesPerOneAd: "リアタイ更新中に広告を出す間隔(ノートの個数な)" - setZeroToDisable: "0でリアタイ更新時の広告配信を無効にすんで" - adsTooClose: "広告を出す間隔がめっちゃ短いから、ユーザー体験がめちゃめちゃ悪くなるかもしれへん。" _forgotPassword: enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。" ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。" @@ -1886,8 +1456,6 @@ _plugin: install: "プラグインのインストール" installWarn: "信頼できへんプラグインはインストールせんとってな" manage: "プラグインの管理" - viewSource: "ソース見る" - viewLog: "ログを表示" _preferencesBackups: list: "作ったバックアップ" saveNew: "新しく保存" @@ -1902,8 +1470,8 @@ _preferencesBackups: deleteConfirm: "{name}を消すん?" renameConfirm: "「{old}」を「{new}」に変えるん?" noBackups: "バックアップはないで。「新しく保存」ってとこでこのクライアント設定を鯖に保存できるで。" - createdAt: "作った日時: {date} {time}" - updatedAt: "更新日時: {date} {time}" + createdAt: "作った日時:{date}{time}" + updatedAt: "更新日時:{date}{time}" cannotLoad: "読み込みできへん..." invalidFile: "ファイル形式が違うで?" _registry: @@ -1917,38 +1485,30 @@ _aboutMisskey: contributors: "主な貢献者" allContributors: "全ての貢献者" source: "ソースコード" - original: "オリジナル" - thisIsModifiedVersion: "{name}はオリジナルのMisskeyをいじったバージョンをつこうてるで。" translation: "Misskeyを翻訳" donate: "Misskeyに寄付" morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰" patrons: "支援者" - projectMembers: "" -_displayOfSensitiveMedia: - respect: "きわどいのは見とうない" - ignore: "きわどいのも見たい" - force: "常にメディアを隠すで" _instanceTicker: none: "表示せん" - remote: "リモートユーザーに見せる" - always: "いつでも見せる" + remote: "リモートユーザーに表示" + always: "常に表示" _serverDisconnectedBehavior: reload: "自動でリロード" dialog: "ダイアログで警告" quiet: "控えめに警告" _channel: - create: "チャンネル作る" - edit: "チャンネルいじる" + create: "チャンネルを作る" + edit: "チャンネルを編集" setBanner: "バナーを設定" removeBanner: "バナーを削除" featured: "トレンド" - owned: "管理しとる" + owned: "管理中" following: "フォロー中やで" - usersCount: "{n}人が参加しとる" + usersCount: "{n}人が参加中やで" notesCount: "{n}こ投稿があるで" nameAndDescription: "名前と説明" nameOnly: "名前だけ" - allowRenoteToExternal: "チャンネルの外にリノートできるようにする" _menuDisplay: sideFull: "横" sideIcon: "横(アイコン)" @@ -1958,6 +1518,11 @@ _wordMute: muteWords: "ミュートするワード" muteWordsDescription: "スペースで区切るとAND指定になって、改行で区切るとOR指定になるで。" muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になるで。" + softDescription: "指定した条件のノートをタイムラインから隠すで。" + hardDescription: "指定した条件のノートをタイムラインに追加しないようにするで。追加せーへんかったかったノートは、条件を変えても除外されたままになるで。" + soft: "ソフト" + hard: "ハード" + mutedNotes: "ミュートされたノート" _instanceMute: instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートにするで。" instanceMuteDescription2: "改行で区切って設定するんやで" @@ -1974,7 +1539,7 @@ _theme: builtinThemes: "標準のテーマ" alreadyInstalled: "そのテーマはもうインストールされとるで?" invalid: "テーマの形式が間違ってるみたいや" - make: "テーマ作る" + make: "テーマを作る" base: "ベース" addConstant: "定数を追加" constant: "定数" @@ -2004,6 +1569,7 @@ _theme: header: "ヘッダー" navBg: "サイドバーの背景" navFg: "サイドバーの文字" + navHoverFg: "サイドバー文字(ホバー)" navActive: "サイドバー文字(アクティブ)" navIndicator: "サイドバーのインジケーター" link: "リンク" @@ -2020,27 +1586,30 @@ _theme: infoFg: "情報の文字" infoWarnBg: "警告の背景" infoWarnFg: "警告の文字" + cwBg: "CW ボタンの背景" + cwFg: "CW ボタンの文字" + cwHoverBg: "CW ボタンの背景 (ホバー)" toastBg: "通知トーストの背景" toastFg: "通知トーストの文字" buttonBg: "ボタンの背景" buttonHoverBg: "ボタンの背景 (ホバー)" inputBorder: "入力ボックスの縁取り" + listItemHoverBg: "リスト項目の背景 (ホバー)" + driveFolderBg: "ドライブフォルダーの背景" + wallpaperOverlay: "壁紙のオーバーレイ" badge: "バッジ" messageBg: "チャットの背景" + accentDarken: "アクセント (暗め)" + accentLighten: "アクセント (明るめ)" fgHighlighted: "強調されとる文字" _sfx: note: "ノート" noteMy: "ノート(自分)" notification: "通知" - reaction: "ツッコミ選んどるとき" -_soundSettings: - driveFile: "ドライブん中の音使う" - driveFileWarn: "ドライブん中のファイル選びや" - driveFileTypeWarn: "このファイルは対応しとらへん" - driveFileTypeWarnDescription: "音声ファイルを選びや" - driveFileDurationWarn: "音が長すぎるわ" - driveFileDurationWarnDescription: "長い音使うたらMisskey使うのに良うないかもしれへんで。それでもええか?" - driveFileError: "音声が読み込めへんかったで。設定を変更せえや" + chat: "チャット" + chatBg: "チャット(バックグラウンド)" + antenna: "アンテナ受信" + channel: "チャンネル通知" _ago: future: "未来" justNow: "ついさっき" @@ -2052,32 +1621,36 @@ _ago: monthsAgo: "{n}ヶ月前" yearsAgo: "{n}年前" invalid: "あらへん" -_timeIn: - seconds: "{n}秒後" - minutes: "{n}分後" - hours: "{n}時間後" - days: "{n}日後" - weeks: "{n}週間後" - months: "{n}ヶ月後" - years: "{n}年後" _time: second: "秒" minute: "分" hour: "時間" day: "日" +_timelineTutorial: + title: "Misskeyってなんや?" + step1_1: "これは「タイムライン」や。{name}に投稿された「ノート」が順番に表示されるで。" + step1_2: "タイムラインには何個か種類があってな、例えば「ホームタイムライン」だったらあんたのフォローしてる人のノート、「ローカルタイムライン」には{name}全部のノートが流れてくるで。" + step2_1: "試しに、何かノートを投稿してみ。画面の鉛筆マークのボタンでフォームが開くで。" + step2_2: "最初のノートは、自己紹介とか「{name}始めてみたんや」とかがええと思うで。" + step3_1: "投稿できた?" + step3_2: "あんたのノートがタイムラインに出てきたら成功や。" + step4_1: "ノートには、「ツッコミ」を付けれるで。" + step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶんやで。" _2fa: alreadyRegistered: "もう設定終わっとるわ。" registerTOTP: "認証アプリの設定はじめる" + passwordToTOTP: "パスワードを入れてーや" step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。" step2: "次に、ここにあるQRコードをアプリでスキャンしてな~。" - step2Uri: "デスクトップアプリを使う時は次のURIを入れるで" + step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。" + step2Url: "デスクトップアプリやったら次のURLを入力してや:" step3Title: "確認コードを入れてーや" - step3: "アプリに映っとる確認コード(トークン)を入れて終わりや。" - setupCompleted: "設定が終わったで。" - step4: "これからログインするときも、同じようにコードを入れるんや。" + step3: "アプリに表示されているトークンを入力して終わりや。" + step4: "これからログインするときも、同じようにトークンを入力するんやで" securityKeyNotSupported: "今使とるブラウザはセキュリティキーに対応してへんのやってさ。" registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するんやったら、まず認証アプリを設定してーな。" securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーか端末の指紋認証やPINを使ってログインするように設定できるで。" + chromePasskeyNotSupported: "Chromeのパスキーは今んとこ対応してないねん。" registerSecurityKey: "セキュリティキー・パスキーを登録するわ" securityKeyName: "キーの名前を入れてーや" tapSecurityKey: "ブラウザが言うこと聞いて、セキュリティキーとかパスキー登録しといでや" @@ -2085,15 +1658,9 @@ _2fa: removeKeyConfirm: "{name}を消すん?" whyTOTPOnlyRenew: "セキュリティキーが登録されとったら、認証アプリの設定は解除できへんで。" renewTOTP: "認証アプリをもっかい設定" - renewTOTPConfirm: "今までの認証アプリの確認コードは使えんくなるけどええか?" + renewTOTPConfirm: "今までの人称アプリの確認コードは使えんくなるけどええか?" renewTOTPOk: "もっかい設定する" renewTOTPCancel: "やめとく" - checkBackupCodesBeforeCloseThisWizard: "このウィザードを閉じる前に、したのバックアップコードを確認しいや。" - backupCodes: "バックアップコード" - backupCodesDescription: "認証アプリが使用できんなった場合、以下のバックアップコードを使ってアカウントにアクセスできるで。これらのコードは必ず安全な場所に置いときや。各コードは一回だけ使用できるで。" - backupCodeUsedWarning: "バックアップコードが使用されたで。認証アプリが使えなくなってるん場合、なるべく早く認証アプリを再設定しや。" - backupCodesExhaustedWarning: "バックアップコードが全て使用されたで。認証アプリを利用できん場合、これ以上アカウントにアクセスできなくなるで。認証アプリを再登録しや。" - moreDetailedGuideHere: "詳細なガイドはこちら" _permissions: "read:account": "アカウントの情報を見るで" "write:account": "アカウントの情報を変更するで" @@ -2127,59 +1694,6 @@ _permissions: "write:gallery": "ギャラリーを操作するで" "read:gallery-likes": "ギャラリーのいいねを見るで" "write:gallery-likes": "ギャラリーのいいねを操作するで" - "read:flash": "Playを見る" - "write:flash": "Playを操作する" - "read:flash-likes": "Playのええやん!を見る" - "write:flash-likes": "Playのええやん!を見る" - "read:admin:abuse-user-reports": "ユーザーからの通報を見る" - "write:admin:delete-account": "ユーザーアカウント消す" - "write:admin:delete-all-files-of-a-user": "ユーザーのファイル全部ほかす" - "read:admin:index-stats": "データベースインデックスの情報見る" - "read:admin:table-stats": "データベーステーブルの情報見る" - "read:admin:user-ips": "ユーザーのIPアドレスを見る" - "read:admin:meta": "インスタンスのメタデータ見る" - "write:admin:reset-password": "ユーザーのパスワードをリセット" - "write:admin:resolve-abuse-user-report": "ユーザーからの通報を解決する" - "write:admin:send-email": "メール送る" - "read:admin:server-info": "サーバーの情報見る" - "read:admin:show-moderation-log": "モデレーションログ見る" - "read:admin:show-user": "ユーザーのプライベートな情報見る" - "write:admin:suspend-user": "ユーザーを凍結" - "write:admin:unset-user-avatar": "ユーザーのアバターを削除" - "write:admin:unset-user-banner": "ユーザーのバナーを削除" - "write:admin:unsuspend-user": "ユーザーの凍結解除" - "write:admin:meta": "インスタンスのメタデータいじる" - "write:admin:user-note": "モデレーションノートいじる" - "write:admin:roles": "ロールをいじる" - "read:admin:roles": "ロール見る" - "write:admin:relays": "リレーいじる" - "read:admin:relays": "リレー見る" - "write:admin:invite-codes": "招待コードいじる" - "read:admin:invite-codes": "招待コード見る" - "write:admin:announcements": "お知らせいじる" - "read:admin:announcements": "お知らせ見る" - "write:admin:avatar-decorations": "アバターデコレーションをいじる" - "read:admin:avatar-decorations": "アバターデコレーション見る" - "write:admin:federation": "連合の情報いじる" - "write:admin:account": "ユーザーアカウントいじる" - "read:admin:account": "ユーザーの情報見る" - "write:admin:emoji": "絵文字いじる" - "read:admin:emoji": "絵文字見る" - "write:admin:queue": "ジョブキューいじる" - "read:admin:queue": "ジョブキューの情報見る" - "write:admin:promo": "プロモーションノートいじる" - "write:admin:drive": "ユーザーのドライブいじる" - "read:admin:drive": "ユーザーのドライブの情報見る" - "read:admin:stream": "管理者用のWebsocket API使う" - "write:admin:ad": "広告いじる" - "read:admin:ad": "広告見る" - "write:invite-codes": "招待コード作る" - "read:invite-codes": "招待コード取得" - "write:clip-favorite": "クリップのいいねいじる" - "read:clip-favorite": "クリップのいいね見る" - "read:federation": "連合の情報取得" - "write:report-abuse": "違反報告" - "write:chat": "チャットを操作するで" _auth: shareAccessTitle: "アプリへのアクセス許してやったらどうや" shareAccess: "「{name}」がアカウントにアクセスすることを許可してええか?" @@ -2188,17 +1702,13 @@ _auth: permissionAsk: "このアプリは次の権限を要求しとるで" pleaseGoBack: "アプリケーションに戻ってええよ" callback: "アプリケーションに戻っとるで" - accepted: "アクセスを許可したで" denied: "アクセスを拒否ったで" - scopeUser: "以下のユーザーとしていじってるで" pleaseLogin: "アプリにアクセスさせるんやったら、ログインしてや。" - byClickingYouWillBeRedirectedToThisUrl: "アクセスを許したら、自動で下のURLに遷移するで" _antennaSources: all: "みんなのノート" homeTimeline: "フォローしとるユーザーのノート" - users: "選んだ一人か複数のユーザーのノート" + users: "選らんだ一人か複数のユーザーのノート" userList: "選んだリストのユーザーのノート" - userBlacklist: "選んだ一人か複数のユーザーを除いた全てのノート" _weekday: sunday: "日曜日" monday: "月曜日" @@ -2237,7 +1747,6 @@ _widgets: _userList: chooseList: "リストを選ぶ" clicker: "クリッカー" - birthdayFollowings: "今日誕生日のツレ" _cw: hide: "隠す" show: "続き見して!" @@ -2299,22 +1808,15 @@ _profile: metadataContent: "内容" changeAvatar: "アバター画像を変更するで" changeBanner: "バナー画像を変更するで" - verifiedLinkDescription: "内容をURLに設定すると、リンク先のwebサイトに自分のプロフのリンクが含まれてる場合に所有者確認済みアイコンを表示させることができるで。" - avatarDecorationMax: "最大{max}つまでデコつけれんで" - followedMessage: "フォローされたら返すメッセージ" - followedMessageDescription: "フォローされたときに相手に返す短めのメッセージを決めれるで。" - followedMessageDescriptionForLockedAccount: "フォローが承認制なら、フォローリクエストをOKしたときに見せるで。" _exportOrImport: allNotes: "全てのノート" favoritedNotes: "お気に入りにしたノート" - clips: "クリップ" followingList: "フォロー" muteList: "ミュート" blockingList: "ブロック" userLists: "リスト" excludeMutingUsers: "ミュートしてるユーザーは入れんとくわ" excludeInactiveUsers: "使われてなさそうなアカウントは入れんとくわ" - withReplies: "インポートした人による返信をTLに含むようにすんで。" _charts: federation: "連合" apRequest: "リクエスト" @@ -2361,11 +1863,13 @@ _play: title: "タイトル" script: "スクリプト" summary: "説明" - visibilityDescription: "非公開に設定するとプロフィールに表示されへんくなるけど、URLを知っとる人は引き続きアクセスできるで。" _pages: newPage: "ページを作る" editPage: "ページの編集" readPage: "ソースを表示中" + created: "ページを作成したで" + updated: "ページを更新したで" + deleted: "ページを削除したで" pageSetting: "ページ設定" nameAlreadyExists: "指定されたページURLはもうあるみたいや" invalidNameTitle: "正しくないページURLみたいやで" @@ -2393,7 +1897,6 @@ _pages: eyeCatchingImageSet: "アイキャッチ画像を設定" eyeCatchingImageRemove: "アイキャッチ画像を削除" chooseBlock: "ブロックを追加" - enterSectionTitle: "セクションタイトルを入れる" selectType: "種類を選択" contentBlocks: "コンテンツ" inputBlocks: "入力" @@ -2404,8 +1907,6 @@ _pages: section: "セクション" image: "画像" button: "ボタン" - dynamic: "動的ブロック" - dynamicDescription: "このブロックは廃止されとるで。今後は{play}を利用してや。" note: "ノート埋め込み" _note: id: "ノートID" @@ -2420,56 +1921,35 @@ _notification: youGotMention: "{name}からのメンション" youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" - youRenoted: "{name}がリノートしたみたいやで" + youRenoted: "{name}がRenoteしたみたいやで" youWereFollowed: "フォローされたで" youReceivedFollowRequest: "フォロー許可してほしいみたいやな" yourFollowRequestAccepted: "フォローさせてもろたで" pollEnded: "アンケートの結果が出たみたいや" - newNote: "さらの投稿" unreadAntennaNote: "アンテナ {name}" - roleAssigned: "ロールが付与されたで" emptyPushNotificationMessage: "プッシュ通知の更新をしといたで" achievementEarned: "実績を獲得しとるで" - testNotification: "通知テスト" - checkNotificationBehavior: "通知の表示を確かめるで" - sendTestNotification: "テスト通知を送信するで" - notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで" - reactedBySomeUsers: "{n}人がツッコんだで" - likedBySomeUsers: "{n}人がいいねしたで" - renotedBySomeUsers: "{n}人がリノートしたで" - followedBySomeUsers: "{n}人にフォローされたで" - flushNotification: "通知の履歴をリセットする" - exportOfXCompleted: "{x}のエクスポートが終わったわ" - login: "ログインしとったで" - createToken: "アクセストークンが作成されたで" - createTokenDescription: "心当たりないんやったら「{text}」でアクセストークンを削除してやって。" _types: all: "すべて" - note: "あんたらの新規投稿" follow: "フォロー" mention: "メンション" reply: "リプライ" - renote: "リノート" + renote: "Renote" quote: "引用" reaction: "ツッコミ" pollEnded: "アンケートが終了したで" receiveFollowRequest: "フォロー許可してほしいみたいやで" followRequestAccepted: "フォローが受理されたで" - roleAssigned: "ロールが付与された" achievementEarned: "実績の獲得" - exportCompleted: "エクスポート終わった" - login: "ログイン" - test: "通知テスト" app: "連携アプリからの通知や" _actions: followBack: "フォローバック" reply: "返事" - renote: "リノート" + renote: "Renote" _deck: alwaysShowMainColumn: "いつもメインカラムを表示" columnAlign: "カラムの寄せ" addColumn: "カラムを追加" - newNoteNotificationSettings: "新着ノート通知の設定" configureColumn: "カラムの設定" swapLeft: "左に移動" swapRight: "右に移動" @@ -2483,9 +1963,6 @@ _deck: introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょ!" introduction2: "画面の右にある + を押して、いつでもカラムを追加できるで。" widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選んでウィジェットを追加してなー" - useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示" - usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となるで" - flexible: "幅を自動調整" _columns: main: "メイン" widgets: "ウィジェット" @@ -2508,343 +1985,15 @@ _drivecleaner: orderByCreatedAtAsc: "追加日の古い順" _webhookSettings: createWebhook: "Webhookをつくる" - modifyWebhook: "Webhookを編集" name: "名前" secret: "シークレット" - trigger: "トリガー" + events: "Webhookを投げるタイミング" active: "有効" _events: follow: "フォローしたとき~!" followed: "フォローもらったとき~!" note: "ノートを投稿したとき~!" reply: "返信があるとき~!" - renote: "リノートされるとき~!" - reaction: "ツッコまれたとき~!" + renote: "Renoteされるとき~!" + reaction: "ツッコミがあるとき~!" mention: "メンションがあるとき~!" - _systemEvents: - abuseReport: "ユーザーから通報があったとき" - abuseReportResolved: "ユーザーからの通報を処理したとき" - userCreated: "ユーザーが作成されたとき" - inactiveModeratorsWarning: "モデレーターがしばらくおらんかったとき" - inactiveModeratorsInvitationOnlyChanged: "モデレーターがしばらくおらんかったから、システムが招待制に変えたとき" - deleteConfirm: "ほんまにWebhookをほかしてもええんか?" - testRemarks: "スイッチ右のボタンを押すとダミーデータを使ったテスト用Webhookを送れるで。" -_abuseReport: - _notificationRecipient: - createRecipient: "通報の通知先を追加" - modifyRecipient: "通報の通知先を編集" - recipientType: "通知先の種類" - _recipientType: - mail: "メール" - webhook: "Webhook" - _captions: - mail: "モデレーター権限を持つユーザーのメアドに通知を送るで(通報を受けた時のみ)" - webhook: "指定したSystemWebhookに通知を送るで(通報を受けた時と通報を解決した時にそれぞれ発信)" - keywords: "キーワード" - notifiedUser: "通知先ユーザー" - notifiedWebhook: "使用するWebhook" - deleteConfirm: "通知先を削除してもええか?" -_moderationLogTypes: - createRole: "ロールを追加すんで" - deleteRole: "ロールほかす" - updateRole: "ロールの更新すんで" - assignRole: "ロールへアサイン" - unassignRole: "ロールのアサインほかす" - suspend: "凍結" - unsuspend: "凍結解除" - addCustomEmoji: "自由な絵文字追加されたで" - updateCustomEmoji: "自由な絵文字更新されたで" - deleteCustomEmoji: "自由な絵文字消されたで" - updateServerSettings: "サーバー設定更新すんねん" - updateUserNote: "モデレーションノート更新" - deleteDriveFile: "ファイルをほかす" - deleteNote: "ノートを削除" - createGlobalAnnouncement: "みんなへの通告を作成したで" - createUserAnnouncement: "あんたらへの通告を作成したで" - updateGlobalAnnouncement: "みんなへの通告更新したったで" - updateUserAnnouncement: "あんたらへの通告更新したったで" - deleteGlobalAnnouncement: "みんなへの通告消したったで" - deleteUserAnnouncement: "あんたらへのお知らせを削除" - resetPassword: "パスワードをリセット" - suspendRemoteInstance: "リモートサーバーを止めんで" - unsuspendRemoteInstance: "リモートサーバーを再開すんで" - updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新" - markSensitiveDriveFile: "ファイルをセンシティブ付与" - unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" - resolveAbuseReport: "苦情を解決" - forwardAbuseReport: "通報を転送" - updateAbuseReportNote: "通報のモデレーションノート更新" - createInvitation: "招待コード作る" - createAd: "広告を作んで" - deleteAd: "広告ほかす" - updateAd: "広告を更新" - createAvatarDecoration: "アイコンデコレーションを作成" - updateAvatarDecoration: "アイコンデコレーションを更新" - deleteAvatarDecoration: "アイコンデコレーションを削除" - unsetUserAvatar: "この子のアイコン元に戻す" - unsetUserBanner: "この子のバナー元に戻す" - createSystemWebhook: "SystemWebhookを作成" - updateSystemWebhook: "SystemWebhookを更新" - deleteSystemWebhook: "SystemWebhookを削除" - createAbuseReportNotificationRecipient: "通報の通知先作る" - updateAbuseReportNotificationRecipient: "通報の通知先更新" - deleteAbuseReportNotificationRecipient: "通報の通知先消す" - deleteAccount: "アカウント消す" - deletePage: "ページ消す" - deleteFlash: "Playをほかす" - deleteGalleryPost: "ギャラリーの投稿をほかす" -_fileViewer: - title: "ファイルの詳しい情報" - type: "ファイルの種類" - size: "ファイルのでかさ" - url: "URL" - uploadedAt: "追加した日" - attachedNotes: "ファイルがついてきてるノート" - thisPageCanBeSeenFromTheAuthor: "このページはこのファイルをアップした人しか見れへんねん。" -_externalResourceInstaller: - title: "ほかのサイトからインストール" - checkVendorBeforeInstall: "配ってるとこが信頼できるか確認した上でインストールしてな。" - _plugin: - title: "このプラグイン、インストールする?" - _theme: - title: "このテーマインストールする?" - _meta: - base: "" - _vendorInfo: - title: "" - endpoint: "" - hashVerify: "" - _errors: - _invalidParams: - title: "" - description: "" - _resourceTypeNotSupported: - title: "" - description: "" - _failedToFetch: - title: "" - fetchErrorDescription: "他のサイトに繋がらんかったわ。もっかいやってもダメやったら、サイトの管理してる人に言っといて。" - parseErrorDescription: "他のサイトから持ってきたデータ、よう分からんかったわ。サイトの管理してる人に言っといて。" - _hashUnmatched: - title: "ちゃんとしたデータが持ってこれんかったわ" - description: "もらったデータがなんかおかしいっぽいわ。ちょっと危ないからインストールはできへん。サイト管理してる人に言っといてな。" - _pluginParseFailed: - title: "AiScriptエラー起こしてもうたねん" - description: "データは取得できたものの、AiScript解析時にエラーがあったから読み込めへんかってん。すまんが、プラグインを作った人に問い合わせてくれへん?ごめんな。エラーの詳細はJavaScriptコンソール読んでな。" - _pluginInstallFailed: - title: "プラグインのインストール失敗してもた" - description: "プラグインのインストール中に問題発生してもた、もう1度試してな。エラーの詳細はJavaScriptのコンソール見てや。" - _themeParseFailed: - title: "テーマ解析エラー" - description: "データは取れたんやが、テーマファイル読み込んどる時にエラーがあったから読み込めへんかったわ。すまんけど、テーマ作った人に言うてくれへん?ごめんな。エラーの詳細はJavaScriptコンソール読んでな。" - _themeInstallFailed: - title: "テーマインストールに失敗してもた" - description: "なんかテーマインストールできんかったわ。もう一回試してな。細かいのはJavaScriptのコンソール見てや。" -_dataSaver: - _media: - title: "メディアの読み込み" - description: "絵・動画が自動で読まれるのをふせぐわ。隠れてる絵・動画はタップするとひょっこりはんしてくれんで。" - _avatar: - title: "アイコンの絵" - description: "アイコン画像のアニメが止まるで。普通の画像よりもデータ量がでかいから、もっと通信量を節約できるねん。" - _code: - title: "コードハイライト" - description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。" -_hemisphere: - N: "北半球" - S: "南半球" - caption: "一部のクライアント設定で、季節を判定するのに使用するで。" -_reversi: - reversi: "リバーシ" - gameSettings: "対局の設定" - chooseBoard: "ボードを選択" - blackOrWhite: "先行/後攻" - blackIs: "{name}が黒(先行)" - rules: "ルール" - thisGameIsStartedSoon: "対局、そろそろ開始されるで。" - waitingForOther: "相手の準備が完了するのを待ってんで。" - waitingForMe: "あんさんの準備が完了すんのを待ってんで" - waitingBoth: "準備してなー" - ready: "準備完了" - cancelReady: "準備を再開" - opponentTurn: "相手のターンやで" - myTurn: "あんさんのターンや" - turnOf: "{name}のターンやで" - pastTurnOf: "{name}のターン" - surrender: "投了" - surrendered: "投了により" - timeout: "時間切れ" - drawn: "引き分け" - won: "{name}の勝ち" - black: "黒" - white: "白" - total: "合計" - turnCount: "{count}ターン目" - myGames: "自分の対局" - allGames: "みんなの対局" - ended: "終了" - playing: "対局中" - isLlotheo: "石の少ない方が勝ち(ロセオ)" - loopedMap: "ループマップ" - canPutEverywhere: "どこでも置けるモード" - timeLimitForEachTurn: "1ターンの時間制限" - freeMatch: "フリーマッチ" - lookingForPlayer: "対戦相手を探してるで" - gameCanceled: "対局がキャンセルされたわ" - shareToTlTheGameWhenStart: "初めの時に対局をタイムラインに投稿するで" - iStartedAGame: "対局し始めたで! #MisskeyReversi" - opponentHasSettingsChanged: "相手が設定変えたで" - allowIrregularRules: "変則許可 (完全フリー)" - disallowIrregularRules: "変則なし" - showBoardLabels: "盤面に行・列番号を表示" - useAvatarAsStone: "石をアイコンにする" -_offlineScreen: - title: "オフライン - サーバーに接続できひんで" - header: "サーバーに接続できへんわ" -_urlPreviewSetting: - title: "URLプレビューの設定" - enable: "URLプレビューを有効にする" - timeout: "プレビュー取得時のタイムアウト(ms)" - timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されへんで。" - maximumContentLength: "Content-Lengthの最大値(byte)" - maximumContentLengthDescription: "Content-Lengthがこの値を超えた場合、プレビューは生成されへんで。" - requireContentLength: "Content-Lengthが取得できた場合のみプレビューを生成" - requireContentLengthDescription: "相手サーバがContent-Lengthを返さない場合、プレビューは生成されへんで。" - userAgent: "User-Agent" - userAgentDescription: "プレビュー取得時に使用されるUser-Agentを設定するで。空欄の場合、デフォルトのUser-Agentが使用されるで。" - summaryProxy: "プレビューを生成するプロキシのエンドポイント" - summaryProxyDescription: "Misskey本体やなく、サマリープロキシを使用してプレビューを生成するで。" - summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されるで。プロキシ側がこれらをサポートせえへんときは、設定値は無視されるで。" -_mediaControls: - pip: "ピクチャインピクチャ" - playbackRate: "再生速度" - loop: "ループ再生" -_contextMenu: - title: "コンテキストメニュー" - app: "アプリ" - appWithShift: "Shiftキーでアプリ" - native: "ブラウザのUI" -_gridComponent: - _error: - requiredValue: "この値は必須項目やで" - columnTypeNotSupport: "正規表現によるバリデーションはtype:textのカラムだけサポートしてるで" - patternNotMatch: "この値は{pattern}のパターンに一致しいひんで" - notUnique: "この値は一意でなあかんで" -_roleSelectDialog: - notSelected: "選択されとらんで" -_customEmojisManager: - _gridCommon: - copySelectionRows: "選択行をコピーするで" - copySelectionRanges: "選択範囲をコピーするで" - deleteSelectionRows: "選択行を削除するで" - deleteSelectionRanges: "選択範囲の値をクリアするで" - searchSettings: "検索設定" - searchSettingCaption: "検索条件を詳しく設定するで。" - searchLimit: "表示件数" - sortOrder: "並び順" - registrationLogs: "登録ログ" - registrationLogsCaption: "絵文字更新・削除時のログが表示されるで。更新・削除操作をしたり、ページを遷移・リロードしたら消えるから気ぃつけてな。" - alertEmojisRegisterFailedDescription: "絵文字の更新・削除に失敗したで。詳細は登録ログを確認してな。" - _logs: - showSuccessLogSwitch: "成功ログを表示するで" - failureLogNothing: "失敗ログはあらへん。" - logNothing: "失敗ログはあらへん。" - _remote: - selectionRowDetail: "選択行の詳細やで" - importSelectionRows: "選択行をインポートするで" - importSelectionRangesRows: "選択範囲の行をインポートするで" - importEmojisButton: "チェックされた絵文字をインポートするで" - confirmImportEmojisTitle: "絵文字のインポートするで" - confirmImportEmojisDescription: "リモートから受信した{count}個の絵文字をインポートするで。絵文字のライセンスには十分気ぃつけてな。実行してもええか?" - _local: - tabTitleList: "登録済み絵文字一覧" - tabTitleRegister: "絵文字の登録" - _list: - emojisNothing: "登録された絵文字はないで。" - markAsDeleteTargetRows: "選択行を削除対象にするで" - markAsDeleteTargetRanges: "選択範囲の行を削除対象にするで" - alertUpdateEmojisNothingDescription: "変更された絵文字はないで。" - alertDeleteEmojisNothingDescription: "削除対象の絵文字はないで。" - confirmMovePage: "ページを移動してもええんか?" - confirmChangeView: "表示を変更してもええんか?" - confirmUpdateEmojisDescription: "{count}個の絵文字を更新するで。実行してもええか?" - confirmDeleteEmojisDescription: "チェックがつけられた{count}個の絵文字を削除するで。ほんまにええか?" - confirmResetDescription: "今までやった変更が全部リセットされるで。" - confirmMovePageDesciption: "このページの絵文字に変更が加えられてるで。\n保存せずページを移動してまうと、このページで加えた変更が全てパーになるで。" - dialogSelectRoleTitle: "絵文字に設定されたロールで検索" - _register: - uploadSettingTitle: "アップロード設定" - uploadSettingDescription: "この画面で絵文字アップロードするときの動きを設定できるで。" - directoryToCategoryLabel: "ディレクトリ名を\"category\"に入力する" - directoryToCategoryCaption: "ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を\"category\"に入力します。" - confirmRegisterEmojisDescription: "リストに表示されてる絵文字を新たなカスタム絵文字として登録するで。ほんまにええか? (サーバーがしんどくなるから、一回で登録できる絵文字は{count}件までやで)" - confirmClearEmojisDescription: "編集内容をほかして、リストに表示されている絵文字をクリアするで。ほんまにええか?" - confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードするで。ほんまにええか?" -_embedCodeGen: - title: "埋め込みコードをカスタム" - header: "ヘッダー出す" - autoload: "勝手に続きを読み込む(非推奨)" - maxHeight: "高さの最大値" - maxHeightDescription: "0は最大値を指定せえへんけど、ウィジェットが伸び続けるから絶対1以上にしといてや。" - maxHeightWarn: "高さの最大値が無効になっとるで。意図してへん変更なら、普通の値に戻してや。" - previewIsNotActual: "プレビュー画面で出せる範囲をはみ出したから、ホンマの表示とはちゃうとおもうで。" - rounded: "角丸める" - border: "外枠に枠線つける" - applyToPreview: "プレビューに反映" - generateCode: "埋め込みコード作る" - codeGenerated: "コード作ったで" - codeGeneratedDescription: "作ったコードはウェブサイトに貼っつけて使ってや。" -_selfXssPrevention: - warning: "警告" - title: "「この画面になんか貼り付けろ」は全部詐欺やで。" - description1: "ここになんかはつっつけると、悪いユーザーにアカウント乗っ取られたり、個人情報盗まれたりするかもやで" - description2: "はっつけようとしてるものがなんなんかわからんのやったら、%c今すぐ作業やめてウィンドウを閉じて。" - description3: "詳しくはこれを見て。{link}" -_followRequest: - recieved: "もらった申請" - sent: "送った申請" -_remoteLookupErrors: - _federationNotAllowed: - title: "このサーバーと通信できん" - description: "このサーバーとの通信は無効化されてるか、このサーバーをブロックしてるんか、ブロックされてるかもしれん。\nサーバー管理者に問い合わせてや。" - _uriInvalid: - title: "URIがおかしいで" - description: "入力されたURIに問題があるで。URIに使えん文字を入れてないから確かめて。" - _requestFailed: - title: "リクエスト失敗してもうたで" - description: "このサーバーとの通信に失敗してもうたわ。相手サーバーがダウンしてるかもしれん。あと、おかしいURIとか、ありえんURIを入れてないか確かめて。" - _responseInvalid: - title: "レスポンスがおかしいで" - description: "このサーバーと通信することはできたけど、もらったデータがおかしかったで。" - _noSuchObject: - title: "見つからへんね" - description: "求められたリソースが見つからんかったで。URIをもっかい確かめてや。" -_captcha: - verify: "CAPTCHAしばいたって" - testSiteKeyMessage: "サイトキーとシークレットキーにテスト用の値を入力することでプレビューを確認できるで。\n詳細は下記ページを確認してな。" - _error: - _requestFailed: - title: "CAPTCHAのリクエストに失敗してもうた" - text: "しばらく後で実行するか、設定をもっかい確認してや。" - _verificationFailed: - title: "CAPTCHAのリクエストに失敗してもうた" - text: "設定がほんまに合ってるかもっかい確認してや。" - _unknown: - title: "CAPTCHAエラー" - text: "思いもせんかったエラーが起きたわ。" -_bootErrors: - title: "読み込みに失敗したで" - serverError: "少し待ってからリロードしてもまだ問題が解決されんのやったら、以下のError IDを添えてサーバー管理者に連絡して。" - solution: "以下のことやったら解決するかもやで。" - solution1: "ブラウザとかOSを最新バージョンに更新する" - solution2: "アドブロッカーを無効にする" - solution3: "ブラウザのキャッシュをクリアする" - solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する" - otherOption: "ほかのオプション" - otherOption1: "クライアント設定とキャッシュをほかす" - otherOption2: "簡易クライアントを起動" - otherOption3: "修復ツールを起動" -_search: - searchScopeAll: "みんな" - searchScopeLocal: "ローカル" - searchScopeUser: "ユーザー指定" diff --git a/locales/jbo-EN.yml b/locales/jbo-EN.yml index d4fea291d7..ed97d539c0 100644 --- a/locales/jbo-EN.yml +++ b/locales/jbo-EN.yml @@ -1,3 +1 @@ --- -_lang_: "la .lojban." -headlineMisskey: "lo se tcana noi jorne fi loi notci" diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml index d4aa36fa70..18fd8f5a58 100644 --- a/locales/kab-KAB.yml +++ b/locales/kab-KAB.yml @@ -56,7 +56,6 @@ accounts: "Imiḍan" searchByGoogle: "Nadi" file: "Ifuyla" account: "Imiḍan" -replies: "Err" _email: _follow: title: "Yeṭṭafaṛ-ik·em-id" @@ -104,7 +103,3 @@ _deck: _columns: notifications: "Ilɣuyen" list: "Tibdarin" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Imayl" diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index 222599572a..ef66f3fbd2 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -61,7 +61,6 @@ smtpPass: "ಗುಪ್ತಪದ" user: "ಬಳಕೆದಾರ" searchByGoogle: "ಹುಡುಕು" file: "ಕಡತಗಳು" -replies: "ಉತ್ತರಿಸು" _email: _follow: title: "ಹಿಂಬಾಲಿಸಿದರು" @@ -77,8 +76,6 @@ _profile: username: "ಬಳಕೆಹೆಸರು" _notification: youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರು" - _types: - login: "ಪ್ರವೇಶ" _actions: reply: "ಉತ್ತರಿಸು" _deck: diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml deleted file mode 100644 index 361d90d8fa..0000000000 --- a/locales/ko-GS.yml +++ /dev/null @@ -1,850 +0,0 @@ ---- -_lang_: "한국어(경상)" -headlineMisskey: "노트로 이언 네트워크" -introMisskey: "어서 오이소! Misskey넌 오픈소스 분산헹 마이크로 블로그 서비스입니다.\n‘노트’럴 맨걸어서 지검 일나넌 일얼 노누던가 내 이바구럴 남한데 서 보이소.📡\n‘리액션’ 기넝서 남으 노트에 억수로 빠리게 답할 수 잇십니다.👍\n새롭운 세게럴 탐험해 보입시다.🚀" -poweredByMisskeyDescription: "{name} 서버넌 오픈소스 플랫폼 Misskey으 서버 가운데 하나입니다." -monthAndDay: "{month}월 {day}일" -search: "찾기" -notifications: "알림" -username: "사용자 이럼" -password: "비밀번호" -forgotPassword: "비밀번호럴 잊엇뿟십니꺼?" -fetchingAsApObject: "연합서 찾아보고 잇어예" -ok: "예" -gotIt: "알것어예" -cancel: "아이예" -noThankYou: "뎃어예" -enterUsername: "사용자 이럼 서기" -renotedBy: "{user}님이 리노트햇어예" -noNotes: "노트가 어ᇝ십니다" -noNotifications: "알림이 어ᇝ십니다" -instance: "서버" -settings: "설정" -notificationSettings: "알림 설정" -basicSettings: "기본 설정" -otherSettings: "다린 설정" -openInWindow: "창서 옐기" -profile: "프로필" -timeline: "타임라인" -noAccountDescription: "자기소개가 어ᇝ십니다" -login: "로그인" -loggingIn: "로그인하고 잇어예" -logout: "로그아웃" -signup: "가입하기" -uploading: "올리고 잇어예" -save: "저장하기" -users: "사용자" -addUser: "사용자 옇기" -favorite: "질겨찾기" -favorites: "질겨찾기" -unfavorite: "질겨찾기서 어ᇝ애기" -favorited: "질겨찾기에 담앗십니다." -alreadyFavorited: "벌시로 질겨찾기에 담기 잇십니다." -cantFavorite: "질겨찾기에 몬 담앗십니다." -pin: "프로필에 붙이기" -unpin: "프로필서 띠기" -copyContent: "내용 복사하기" -copyLink: "링크 복사하기" -copyLinkRenote: "리노트 링크 복사" -delete: "내삐리기" -deleteAndEdit: "내삐리고 새로 적기" -deleteAndEditConfirm: "요 노트럴 뭉캐고 새로 적십니꺼? 요 노트서 리액션하고 리노트, 답하기도 말캉 뭉캐집니다." -addToList: "리스트에 옇기" -addToAntenna: "안테나에 옇기" -sendMessage: "메시지 보내기" -copyRSS: "알에스에스 복사하기" -copyUsername: "사용자 이럼 복사하기" -copyUserId: "사용자 아이디 복사하기" -copyNoteId: "노트 아이디 복사하기" -copyFileId: "파일 아이디 복사하기" -copyFolderId: "폴더 아이디 복사하기" -copyProfileUrl: "프로필 주소 복사하기" -searchUser: "사용자 찾기" -reply: "답하기" -loadMore: "더 볼래예" -showMore: "더 볼래예" -showLess: "꺼기" -youGotNewFollower: "새 팔로워가 잇십니다" -receiveFollowRequest: "팔로잉 요청이 잇십니다" -followRequestAccepted: "팔로잉이 받아딜이젓십니다" -mention: "멘션" -mentions: "받언 멘션" -directNotes: "쪽지 서기" -importAndExport: "가오기하고 내가기" -import: "가오기" -export: "내가기" -files: "파일" -download: "내리받기" -driveFileDeleteConfirm: "‘{name}’ 파일얼 뭉캡니꺼? 요 파일얼 서넌 콘텐츠도 뭉캐집니다." -unfollowConfirm: "{name}님얼 고마 팔로잉합니꺼?" -exportRequested: "내가기 요청얼 햇십니다. 시간이 쪼매 걸릴 깁니다. 요청이 껕나모 ‘드라이브’에 옇십니다." -importRequested: "가오기 요청얼 햇십니다. 시간이 쪼매 걸릴 깁니다." -lists: "리스트" -noLists: "리스트가 어ᇝ십니다" -note: "노트" -notes: "노트" -following: "팔로잉" -followers: "팔로워" -followsYou: "내럴 팔로잉합니다" -createList: "리스트 맨걸기" -manageLists: "리스트 간리하기" -error: "우짭니꺼" -somethingHappened: "먼가 일낫십니다" -retry: "다시 하기" -pageLoadError: "하멘 부리오기가 아이뎁니다." -pageLoadErrorDescription: "네트워크나 브라우저 캐시 때문일 깁니다. 캐시럴 뭉캐던가 쪼매 잇다 새로 해 주이소." -serverIsDead: "서버가 대답얼 아이합니다. 쪼매 잇다 새로 해 주이소." -youShouldUpgradeClient: "요 하멘얼 볼라먼 새로 곤치던가 새 버전으 클라이언트럴 받아 서 보이소." -enterListName: "리스트 이럼 서기" -privacy: "개인 정보" -makeFollowManuallyApprove: "팔로잉얼 하나석 받아딜이기" -defaultNoteVisibility: "기본 공개 범위" -follow: "팔로우" -followRequest: "팔로우 요청하기" -followRequests: "팔로우 요청" -unfollow: "팔로우 무루기" -followRequestPending: "팔로우 수락 지둘림" -enterEmoji: "이모지 서기" -renote: "리노트" -unrenote: "리노트 무루기" -renoted: "리노트럴 햇십니다." -cantRenote: "요 걸언 리노트럴 몬 합니다." -cantReRenote: "리노트넌 지럴 리노트 몬 합니다." -quote: "따오기" -inChannelRenote: "채널 안 리노트" -inChannelQuote: "채널 안 따오기" -pinnedNote: "붙인 노트" -pinned: "프로필에 붙이기" -you: "나" -clickToShow: "누질라서 보기" -sensitive: "수ᇚ힛섭니다" -add: "옇기" -reaction: "반엉" -reactions: "반엉" -reactionSettingDescription2: "꺼시서 두고, 누질라서 뭉캐고, ‘+’럴 누질라서 옇십니다." -rememberNoteVisibility: "공개 범위럴 기억하기" -attachCancel: "붙임 빼기" -deleteFile: "파일 뭉캐기" -markAsSensitive: "수ᇚ힘 설정" -unmarkAsSensitive: "수ᇚ힘 무루기" -enterFileName: "파일 이럼 서기" -mute: "수ᇚ후기" -unmute: "수ᇚ훈 거 무루기" -renoteMute: "리노트 수ᇚ후기" -renoteUnmute: "리노트 수ᇚ훈 거 무루기" -block: "차단하기" -unblock: "차단 무루기" -suspend: "얼우기" -unsuspend: "얼우기 풀기" -blockConfirm: "차단합니꺼?" -unblockConfirm: "차단얼 무룹니꺼?" -suspendConfirm: "얼웁니꺼?" -unsuspendConfirm: "얼운 거 풉니꺼?" -selectList: "리스트 개리기" -editList: "리스트 적기" -selectChannel: "채널 개리기" -selectAntenna: "안테나 개리기" -editAntenna: "안테나 적기" -selectWidget: "위젯 개리기" -editWidgets: "위젯 적기" -editWidgetsExit: "고마 적기" -customEmojis: "사용자 지정 이모지" -emoji: "이모지" -emojis: "이모지" -emojiName: "이모지 이럼" -emojiUrl: "이모지 주소" -addEmoji: "이모지 옇기" -settingGuide: "개않언 설정" -cacheRemoteFiles: "웬겍 파일 캐시하기" -cacheRemoteFilesDescription: "요 설정얼 키모 웬겍 파일얼 요 서버으 스토리지에 캐시합니다. 미디어가 사게 비이지먼 서버으 스토리지럴 마이 섭니다. 웬겍 사용자가 얼매나 캐시럴 둘 긴가넌 고 옉할으 드라이브 크기 제한마중 다립니다. 요 제한얼 넘구모 엣날 파일버터 캐시서 뭉캐지서 링크가 뎁니다. 요 설정얼 꺼모 웬겍 파일언 첨버터 링크가 뎁니다. 이미지으 섬네일얼 맨걸던 사용자으 개인 정보럴 징키던 할라먼 default.yml서 proxyRemoteFiles럴 ture로 하입시다." -youCanCleanRemoteFilesCache: "파일 간리으 🗑️ 모냥얼 누질리모 캐시럴 말캉 뭉캘 수 잇십니다." -cacheRemoteSensitiveFiles: "웬겍으 수ᇚ힌 파일얼 캐시하기" -cacheRemoteSensitiveFilesDescription: "요 설정얼 꺼모 웬겍 수ᇚ힌 파일이 캐시하지 아이하고 바리 링크합니다." -flagAsBot: "자동 게정입니다" -flagAsBotDescription: "요 게정얼 프로그램서 설라먼 키야 합니다. 키모 다런 개발자가 반엉얼 끋어ᇝ이 데풀이하지 몬 하게 도아 줄 수 잇고 Misskey으 시스템서 자동 게정이 뎁니다." -flagAsCat: "애웅애웅애웅애웅!" -flagAsCatDescription: "애옹?" -flagShowTimelineReplies: "타임라인서 노트으 답하기 보기" -flagShowTimelineRepliesDescription: "키모 타임라인서 다런 사용자덜으 답하기도 봅니다." -autoAcceptFollowed: "팔로잉하넌 사용자으 팔로잉 요청 바리 받아딜이기" -addAccount: "게정 옇기" -reloadAccountsList: "게정 리스트으 정보 새로 바꾸기" -loginFailed: "로그인이 아이뎁니다." -showOnRemote: "웬겍서 보기" -general: "일반" -wallpaper: "벡지" -setWallpaper: "벡지 설정" -removeWallpaper: "벡지 뭉캐기" -searchWith: "찾기: {q}" -youHaveNoLists: "리스트가 어ᇝ십니다" -followConfirm: "{name}님얼 팔로잉합니꺼?" -proxyAccount: "프락시 게정" -proxyAccountDescription: "프락시 게정언 턱벨한 조겐서 웬겍 팔로잉얼 하넌 게정입니다. 사용자가 웬겍 사용자럴 리스트에 옇얼 때 리스트에 옇언 사용자럴 누도 팔로잉 아이하모 할동이 서버로 아이 오니께 요 게정이 아인 프락시 게정얼 팔로잉하게 합니다." -host: "호스트 이럼" -selectUser: "사용자 개리기" -recipient: "받넌 사람" -annotation: "주석" -federation: "옌합" -instances: "서버" -registeredAt: "첫 발겐" -latestRequestReceivedAt: "막죽에 받언 요청" -latestStatus: "막죽 상태" -storageUsage: "스토리지 사용량" -charts: "차트" -perHour: "한 시간마중" -perDay: "하리마중" -stopActivityDelivery: "할동 고마 보내기" -blockThisInstance: "요 서버 차단하기" -silenceThisInstance: "서버 수ᇚ후기" -operations: "동작" -software: "소프트웨어" -version: "버전" -metadata: "메타데이터" -withNFiles: "파일 {n}개" -monitor: "모니터" -jobQueue: "작업 대기옐" -cpuAndMemory: "시피유하고 메모리" -network: "네트워크" -disk: "디스크" -instanceInfo: "서버 정보" -statistics: "통게" -clearQueue: "대기옐 비우기" -clearQueueConfirmTitle: "대기옐얼 비웁니꺼?" -clearQueueConfirmText: "대기옐에 잇넌 걸얼 아이 보냅니다. 흐이 요 동작언 할 필요가 어ᇝ십니다." -clearCachedFiles: "캐시 비우기" -clearCachedFilesConfirm: "캐시한 웬겍 파일얼 말캉 뭉캡니꺼?" -blockedInstances: "차단한 서버" -blockedInstancesDescription: "차단할라넌 서버으 호스트럴 줄 바꿈해서로 비이 줍니다. 차단한 서버넌 요 서버하고 교류 몬 합니다." -silencedInstances: "수ᇚ훈 서버" -silencedInstancesDescription: "수ᇚ훌라넌 서버으 호스트럴 줄 바꿈해서로 비이 줍니다. 수ᇚ훈 서버으 게정언 말캉 ‘수ᇚ후기’가 데서 팔로잉 요청만 데고 팔로워가 아인 로컬 게정서 멘션얼 몬 합니다. 차단한 서버넌 상간 어ᇝ십니다." -muteAndBlock: "수ᇚ훔하고 차단" -mutedUsers: "수ᇚ훈 사용자" -blockedUsers: "차단한 사용자" -noUsers: "사용자가 어ᇝ십니다" -editProfile: "프로필 적기" -noteDeleteConfirm: "요 노트럴 뭉캡니꺼?" -pinLimitExceeded: "더 몬 붙입니다" -done: "햇어예" -processing: "처리하고 잇어예" -preview: "미리보기" -default: "기본값" -defaultValueIs: "기본값: {value}" -noCustomEmojis: "이모지가 어ᇝ십니다" -noJobs: "작업이 어ᇝ십니다" -federating: "옌합하고 잇어예" -blocked: "차단햇어예" -suspended: "고만 보내예" -all: "말캉" -subscribing: "구독하고 잇어예" -publishing: "보내고 잇어예" -notResponding: "답이 어ᇝ어예" -instanceFollowing: "서버으 팔로잉" -instanceFollowers: "서버으 팔로워" -instanceUsers: "서버으 사용자" -changePassword: "비밀번호 바꾸기" -security: "보안" -retypedNotMatch: "선 거가 안 맞십니다." -currentPassword: "지검 비밀번호" -newPassword: "새 비밀번호" -newPasswordRetype: "새 비밀번호 다시 서기" -attachFile: "파일 붙이기" -more: "더 볼래예!" -featured: "인기" -usernameOrUserId: "사용자 이럼이나 사용자 아이디" -noSuchUser: "사용자럴 몬 찾앗십니다" -lookup: "찾아보기" -announcements: "공지 걸" -imageUrl: "이미지 주소" -remove: "내삐리기" -removed: "뭉캣십니다" -removeAreYouSure: "‘{x}’(얼)럴 뭉캡니꺼?" -deleteAreYouSure: "‘{x}’(얼)럴 뭉캡니꺼?" -resetAreYouSure: "아시로 데돌립니꺼?" -areYouSure: "갠찮십니꺼?" -saved: "저장햇십니다" -upload: "올리기" -keepOriginalUploading: "온본 두기" -keepOriginalUploadingDescription: "이미지럴 올릴 때 온본얼 고대로 둡니다. 꺼모 올릴 때 브라우저서 웹 공개 이미지럴 맨겁니다." -fromDrive: "드라이브서" -fromUrl: "주소서" -uploadFromUrl: "주소 올리기" -uploadFromUrlDescription: "올리기할라넌 파일으 주소" -uploadFromUrlRequested: "올리기럴 요청햇십니다" -uploadFromUrlMayTakeTime: "올리기가 껕날라먼 시간이 쪼매 걸릴 깁니다." -explore: "살펴보기" -messageRead: "이럿어예" -noMoreHistory: "요카마 옛날 기록이 어ᇝ십니다" -nUsersRead: "{n}멩이 이럿십니다" -agreeTo: "{0}에 동이하기" -agree: "동이합니다" -agreeBelow: "밑으 내용에 동이합니다" -basicNotesBeforeCreateAccount: "주이할 내용" -termsOfService: "이용 약간" -start: "시작하기" -home: "덜머리" -remoteUserCaution: "웬겍 사용자넌 정보가 학실하지 아이할 수 잇십니다." -activity: "할동" -images: "이미지" -image: "이미지" -birthday: "생일" -yearsOld: "{age}살" -registeredDate: "맨건 날" -location: "장소" -theme: "테마" -themeForLightMode: "볽엄 모드서 설 테마" -themeForDarkMode: "어덥엄 모드서 설 테마" -light: "볽엄" -dark: "어덥엄" -lightThemes: "볽언 테마" -darkThemes: "어덥언 테마" -syncDeviceDarkMode: "디바이스 쪽 어덥엄 모드하고 같구로 마추기" -drive: "드라이브" -fileName: "파일 이럼" -selectFile: "파일 개리기" -selectFiles: "파일 개리기" -selectFolder: "폴더 개리기" -selectFolders: "폴더 개리기" -renameFile: "파일 이럼 바꾸기" -folderName: "폴더 이럼" -createFolder: "폴더 맨걸기" -renameFolder: "폴더 이럼 바꾸기" -deleteFolder: "폴더 뭉캐기" -folder: "폴더" -addFile: "파일 옇기" -emptyDrive: "드라이브가 비잇십니다" -emptyFolder: "폴더가 비잇십니다" -unableToDelete: "몬 뭉캡니다" -inputNewFileName: "새 파일 이럼얼 서 보이소" -inputNewDescription: "새 설멩얼 서 보이소" -inputNewFolderName: "새 폴더 이럼얼 서 보이소" -circularReferenceFolder: "엚길 폴더으 아래 폴더입니다." -hasChildFilesOrFolders: "요 폴더넌 아이 비잇어니께 몬 뭉캡니다." -copyUrl: "주소 복사하기" -rename: "이럼 바꾸기" -avatar: "아바타" -banner: "배너" -displayOfSensitiveMedia: "수ᇚ힌 옝상물 보기" -whenServerDisconnected: "서버하고 옌겔이 껂기모" -disconnectedFromServer: "서버하고 옌겔이 껂깃십니다" -reload: "새로곤침" -doNothing: "무시하기" -reloadConfirm: "새로곤침합니꺼?" -watch: "간심 갖기" -unwatch: "간심 고마 갖기" -accept: "받기" -reject: "아이 받기" -normal: "일반" -instanceName: "서버 이럼" -instanceDescription: "서버 소개" -maintainerName: "간리자 이럼" -maintainerEmail: "간리자 전자우펜" -tosUrl: "이용 약간 주소" -thisYear: "올개" -thisMonth: "요달" -today: "오올" -dayX: "{day}일" -monthX: "{month}월" -yearX: "{year}년" -pages: "바닥" -integration: "옌겔" -connectService: "옌겔하기" -disconnectService: "껂기" -enableLocalTimeline: "로컬 타임라인 키기" -enableGlobalTimeline: "글로벌 타임라인 키기" -disablingTimelinesInfo: "요 타임라인얼 꺼도 간리자하고 중재자넌 고대로 설 수 잇십니다." -registration: "맨걸기" -invite: "초대하기" -driveCapacityPerLocalAccount: "로컬 사용자 하나마중 드라이브 커기" -driveCapacityPerRemoteAccount: "웬겍 사용자 하나마중 드라이브 커기" -inMb: "메가바이트 단이" -bannerUrl: "배너 이미지 주소" -backgroundImageUrl: "배겡 이미지 주소" -basicInfo: "기본 정보" -pinnedUsers: "붙인 사용자" -pinnedUsersDescription: "‘살펴보기’서 붙일라넌 사용자럴 줄 바꿈해서로 적십니다." -pinnedPages: "붙인 바닥" -pinnedPagesDescription: "서버으 대문서 붙일라넌 바닥으 겡로럴 줄 바꿈해서로 적십니다." -pinnedClipId: "붙일 클립으 아이디" -pinnedNotes: "붙인 노트" -hcaptcha: "에이치캡차" -enableHcaptcha: "에이치캡차 키기" -hcaptchaSiteKey: "사이트키" -hcaptchaSecretKey: "시크릿키" -mcaptchaSiteKey: "사이트키" -mcaptchaSecretKey: "시크릿키" -recaptcha: "리캡차" -enableRecaptcha: "리캡차 키기" -recaptchaSiteKey: "사이트키" -recaptchaSecretKey: "시크릿키" -turnstile: "턴스타일" -enableTurnstile: "턴스타일 키기" -turnstileSiteKey: "사이트키" -turnstileSecretKey: "시크릿키" -avoidMultiCaptchaConfirm: "오만 캡차럴 서모 간섭이 잇얼 깁니다. 다린 캡차를 껍니꺼? ‘아이예’럴 누질리모 오만 캡차럴 키 둘 수도 잇십니다." -antennas: "안테나" -manageAntennas: "안테나 간리" -name: "이럼" -antennaSource: "받얼 소스" -antennaKeywords: "받얼 검색어" -antennaExcludeKeywords: "수ᇚ훌 검색어" -antennaKeywordsDescription: "띠어서기럴 하모 ‘거라고’가 데고 줄 바꿈얼 하모 ‘아이먼’이 뎁니다" -notifyAntenna: "새 노트럴 알리기" -withFileAntenna: "파일이 붙언 노트마" -enableServiceworker: "브라우저서 알림 포시럴 키기" -antennaUsersDescription: "사용자 이럼얼 줄 바꿈해서로 섭니다" -caseSensitive: "대소문자럴 구벨하기" -withReplies: "답하기도 옇기" -connectedTo: "요 게정하고 옌겔데어 잇십니다" -notesAndReplies: "걸하고 답걸" -withFiles: "파일에 붙이기" -silence: "수ᇚ후기" -silenceConfirm: "수ᇚ훕니꺼?" -unsilence: "수ᇚ후기 어ᇝ애기" -unsilenceConfirm: "수ᇚ후기럴 어ᇝ앱니꺼?" -popularUsers: "소문난 사용자" -recentlyUpdatedUsers: "얼마 전에 걸 선 사용자" -recentlyRegisteredUsers: "얼마 전에 맨건 사용자" -recentlyDiscoveredUsers: "얼마 전에 찾언 사용자" -exploreUsersCount: "사용자 {count}멩이 잇십니다." -exploreFediverse: "옌합우주 탐험하기" -popularTags: "소문난 태그" -userList: "리스트" -about: "정보" -aboutMisskey: "Misskey넌예" -administrator: "간리자" -token: "학인 기호" -2fa: "두 단게 정멩" -setupOf2fa: "두 단게 정멩 설정" -totp: "정멩 앱" -totpDescription: "정멩 앱서 단헤용 비밀번호 서기" -moderator: "중재자" -moderation: "중재" -moderationNote: "중재 노트" -addModerationNote: "중재 노트 옇기" -moderationLogs: "중재 일지" -nUsersMentioned: "{n}멩이 이바구하고 잇어예" -securityKeyAndPasskey: "보안키·패스키" -securityKey: "보안키" -lastUsed: "마지막 쓰임" -lastUsedAt: "마지막 쓰임: {t}" -unregister: "맨걸기 무루기" -passwordLessLogin: "비밀번호 어ᇝ이 로그인" -passwordLessLoginDescription: "비밀번호 어ᇝ이 보안 키나 패스 키만 서서 로그인합니다." -resetPassword: "비밀번호 재설정" -newPasswordIs: "새 비밀번호넌 ‘{password}’입니다" -reduceUiAnimation: "화면 움직임 효과들을 수ᇚ후기" -share: "노누기" -notFound: "몬 찾앗십니다" -notFoundDescription: "선 주소에 맞넌 페이지가 어ᇝ십니다." -uploadFolder: "기본 올리기 위치" -markAsReadAllNotifications: "모던 알림얼 읽엄 포시" -markAsReadAllUnreadNotes: "모던 걸얼 읽엄 포시" -markAsReadAllTalkMessages: "모던 대화 읽엄 포시" -help: "도움말" -inputMessageHere: "옇다 메시지럴 서이소" -close: "꺼기" -invites: "초대하기" -members: "구성원" -transfer: "넘구기" -title: "제목" -text: "걸" -enable: "키기" -next: "다엄" -retype: "다시 서기" -noteOf: "{user}님으 노트" -quoteAttached: "따옴" -quoteQuestion: "따와가 작성하겠십니까?" -onlyOneFileCanBeAttached: "메시지엔 파일 하나까제밖에 몬 넣십니다" -invitations: "초대하기" -invitationCode: "초대장" -checking: "학인하고 잇십니다" -tooShort: "억수로 짜립니다" -tooLong: "억수로 집니다" -passwordMatched: "맞십니다" -passwordNotMatched: "안 맞십니다" -signinWith: "{x} 서 로그인" -signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소." -or: "아니면" -language: "언어" -uiLanguage: "UI 표시 언어" -aboutX: "{x}에 대해서" -emojiStyle: "이모지 모양" -native: "기본" -showNoteActionsOnlyHover: "마우스 올맀을 때만 노트 액션 버턴 보이기" -noHistory: "기록이 없십니다" -signinHistory: "로그인 기록" -enableAdvancedMfm: "복잡한 MFM 키기" -enableAnimatedMfm: "정신사나운 MFM 키기" -doing: "잠만예" -category: "카테고리" -tags: "태그" -docSource: "요 문서의 원본" -createAccount: "게정 맨걸기" -existingAccount: "원래 게정" -regenerate: "엎고 다시 맨걸기" -fontSize: "글자 크기" -mediaListWithOneImageAppearance: "사진 하나짜리 미디어 목록의 높이" -limitTo: "{x}로 제한" -noFollowRequests: "지둘리는 팔로우 요청이 없십니다" -openImageInNewTab: "새 탭서 사진 열기" -dashboard: "대시보드" -local: "로컬" -remote: "웬겍" -total: "합계" -weekOverWeekChanges: "저번주보다" -dayOverDayChanges: "어제보다" -appearance: "모냥" -clientSettings: "클라이언트 설정" -accountSettings: "게정 설정" -promotion: "선전" -promote: "선전하기" -numberOfDays: "며칠동안" -hideThisNote: "요 노트를 수ᇚ후기" -showFeaturedNotesInTimeline: "타임라인에다 추천 노트 보이기" -objectStorage: "오브젝트 스토리지" -useObjectStorage: "오브젝트 스토리지 키기" -objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "오브젝트 (미디어) 참조 링크 만들 때 쓰는 URL임다. CDN 내지 프락시를 쓴다 카멘은 그 URL을 갖다 늫고, 아이면 써먹을 서비스네 가이드를 봐봐가 공개적으로 접근할 수 있는 주소를 여 넣어 주이소. 그니께, 내가 AWS S3을 쓴다 카면은 'https://.s3.amazonaws.com', GCS를 쓴다 카면 'https://storage.googleapis.com/' 처럼 쓰믄 되입니더." -objectStorageBucket: "Bucket" -objectStorageBucketDesc: "설 서비스으 버킷 이럼얼 서 주이소." -objectStoragePrefix: "Prefix" -objectStoragePrefixDesc: "요 Prefix 디렉토리 안에다가 파일이 들어감다." -objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "AWS S3넌 비아 두고 다런 것언 거 서비스으 엔드포인트럴 서 주이소. ‘’나 ‘:’맨치로 섭니다." -objectStorageRegion: "Region" -objectStorageRegionDesc: "‘xx-east-1’맨치로 리전 이럼얼 서 주이소. 설 서비스에 리전 개넴이 어ᇝ어먼 ‘us-east-1’라고 해 두이소. 에이더블유에스 설정 파일이나 환겡 벤수가 이ᇇ어면 비아 두이소." -objectStorageUseSSL: "SSL 쓰기" -objectStorageUseSSLDesc: "API 호출할 때 HTTPS 안 쓸거면은 꺼 두이소" -objectStorageUseProxy: "연결에 프락시 사용" -objectStorageUseProxyDesc: "오브젝트 스토리지 API 호출에 프락시 안 쓸 거면 꺼 두이소" -objectStorageSetPublicRead: "업로드할 때 'public-read' 설정하기" -s3ForcePathStyleDesc: "s3ForcePathStyle을 키면, 바께쓰 이름을 URL의 호스트명 말고 경로의 일부로써 취급합니다. 셀프 호스트 Minio 같은 걸 굴릴라믄 켜놔야 될 수도 있십니다." -serverLogs: "서버 로그" -deleteAll: "말캉 뭉캐기" -showFixedPostForm: "타임라인 우에 글 작성 칸 박기" -showFixedPostFormInChannel: "채널 타임라인 우에 글 작성 칸 박기" -withRepliesByDefaultForNewlyFollowed: "팔로우 할 때 기본적으로 답걸도 타임라인에 나오게 하기" -newNoteRecived: "새 노트 있어예" -sounds: "소리" -sound: "소리" -listen: "듣기" -none: "어ᇝ엄" -showInPage: "바닥서 보기" -popout: "새 창 열기" -volume: "음량" -masterVolume: "대빵 음량" -notUseSound: "음소거하기" -useSoundOnlyWhenActive: "Misskey가 활성화되어 있을 때만 소리 내기" -details: "자세히" -chooseEmoji: "이모지 개리기" -unableToProcess: "작업 다 몬 했십니다" -recentUsed: "최근 쓴 놈" -install: "설치" -uninstall: "삭제" -installedApps: "설치된 애플리케이션" -nothing: "어ᇝ어예" -installedDate: "설치한 날" -lastUsedDate: "마지막 사용" -state: "상태" -sort: "정렬하기" -ascendingOrder: "작은 순" -descendingOrder: "큰 순" -scratchpad: "스크래치 패드" -scratchpadDescription: "스크래치 패드는 AiScript를 끼적거리는 창입니더. Misskey랑 갖다 이리저리 상호작용하는 코드를 서가 굴리멘은 그 결과도 바로 확인할 수 있십니다." -output: "출력" -script: "스크립트" -disablePagesScript: "온갖 바닥서 AiScript를 쓰지 않음" -updateRemoteUser: "원겍 사용자 근황 알아오기" -unsetUserAvatar: "아바타 치우기" -unsetUserAvatarConfirm: "아바타 갖다 치울까예?" -unsetUserBanner: "배너 치우기" -unsetUserBannerConfirm: "배너 갖다 치울까예?" -deleteAllFiles: "파일 말캉 뭉캐기" -deleteAllFilesConfirm: "파일을 싸그리 다 뭉캐삐릴까예?" -removeAllFollowing: "팔로잉 말캉 무루기" -removeAllFollowingDescription: "{host} 서버랑 걸어놓은 모든 팔로잉을 무룹니다. 고 서버가 아예 없어지삐맀든가, 그런 경우에 하이소." -userSuspended: "요 게정은... 얼어 있십니다." -userSilenced: "요 게정은... 수ᇚ혀 있십니다." -relays: "릴레이" -addRelay: "릴레이 옇기" -addedRelays: "옇은 릴레이" -deletedNote: "뭉캔 걸" -enableInfiniteScroll: "알아서 더 보기" -useCw: "내용 수ᇚ후기" -description: "설멩" -describeFile: "캡션 옇기" -enterFileDescription: "캡션 서기" -author: "맨던 사람" -manage: "간리" -large: "커게" -medium: "엔갆게" -small: "쪼맪게" -emailServer: "전자우펜 서버" -email: "전자우펜" -emailAddress: "전자우펜 주소" -smtpHost: "호스트 이럼" -smtpPort: "포트" -smtpUser: "사용자 이럼" -smtpPass: "비밀번호" -display: "보기" -create: "맨걸기" -abuseReports: "신고하기" -reportAbuse: "신고하기" -reportAbuseRenote: "리노트 신고하기" -reportAbuseOf: "{name}님얼 신고하기" -reporter: "신고한 사람" -reporteeOrigin: "신고덴 사람" -reporterOrigin: "신고한 곳" -waitingFor: "{x}(얼)럴 지달리고 잇십니다" -random: "무작이" -system: "시스템" -clip: "클립 맨걸기" -createNew: "새로 맨걸기" -notesCount: "노트 수" -renotesCount: "리노트한 수" -renotedCount: "리노트덴 수" -followingCount: "팔로우 수" -followersCount: "팔로워 수" -noteFavoritesCount: "질겨찾기한 노트 수" -clips: "클립 맨걸기" -clearCache: "캐시 비우기" -nUsers: "{n} 사용자" -typingUsers: "{users} 님이 서고 잇어예" -unlikeConfirm: "좋네예럴 무룹니꺼?" -info: "정보" -selectAccount: "계정 개리기" -user: "사용자" -administration: "간리" -middle: "엔갆게" -translatedFrom: "{x}서 번옉" -on: "킴" -off: "껌" -hide: "수ᇚ후기" -clickToFinishEmailVerification: "[{ok}]럴 누질라서 전자우펜 정멩얼 껕내이소." -searchByGoogle: "찾기" -tenMinutes: "십 분" -oneHour: "한 시간" -oneDay: "하리" -oneWeek: "한 주" -oneMonth: "한 달" -file: "파일" -typeToConfirm: "게속할라먼 {x}럴 누질라 주이소" -pleaseSelect: "개리 주이소" -remoteOnly: "웬겍만" -tools: "도구" -like: "좋네예!" -unlike: "좋네예 무루기" -numberOfLikes: "좋네예 수" -show: "보기" -roles: "옉할" -role: "옉할" -noRole: "옉할이 어ᇝ십니다" -thisPostMayBeAnnoyingCancel: "아이예" -likeOnly: "좋네예마" -hiddenTags: "수ᇚ훈 해시태그" -myClips: "내 클립" -preservedUsernames: "예약 사용자 이럼" -specifyUser: "사용자 지정" -icon: "아바타" -replies: "답하기" -renotes: "리노트" -attach: "옇기" -surrender: "아이예" -information: "정보" -_chat: - invitations: "초대하기" - noHistory: "기록이 없십니다" - members: "구성원" - home: "덜머리" -_delivery: - stop: "고만 보내예" - _type: - none: "보내고 잇어예" -_initialAccountSetting: - startTutorial: "길라잡이 하기" -_initialTutorial: - launchTutorial: "길라잡이 보기" - title: "길라잡이" - skipAreYouSure: "길라잡이럴 껕냅니까?" - _landing: - title: "길라잡이에 어서 오이소" - _done: - title: "길라잡이가 껕낫십니다!🎉" -_achievements: - _types: - _notes1: - description: "첫 노트럴 섯어예" - _notes10: - description: "노트럴 10번 섰어예" - _notes100: - description: "노트럴 100번 섰어예" - _notes500: - description: "노트럴 500번 섰어예" - _notes1000: - description: "노트럴 1,000번 섰어예" - _notes5000: - description: "노트럴 5,000번 섰어예" - _notes10000: - description: "노트럴 10,000번 섰어예" - _notes20000: - description: "노트럴 20,000번 섰어예" - _notes30000: - description: "노트럴 30,000번 섰어예" - _notes40000: - description: "노트럴 40,000번 섰어예" - _notes50000: - description: "노트럴 50,000번 섰어예" - _notes60000: - description: "노트럴 60,000번 섰어예" - _notes70000: - description: "노트럴 70,000번 섰어예" - _notes80000: - description: "노트럴 80,000번 섰어예" - _notes90000: - description: "노트럴 90,000번 섰어예" - _notes100000: - description: "노트럴 100,000번 섰어예" - _noteClipped1: - description: "첫 노트럴 클립햇어예" - _noteFavorited1: - description: "첫 노트럴 질겨찾기에 담앗어예" - _myNoteFavorited1: - description: "다런 사람이 내 노트럴 질겨찾기에 담앗십니다" - _iLoveMisskey: - description: "“I ❤ #Misskey”럴 섰어예" - _postedAt0min0sec: - description: "0분 0초에 노트를 섰어예" - _tutorialCompleted: - description: "길라잡이럴 껕냇십니다" -_role: - displayOrder: "보기 순서" - _priority: - middle: "엔갆게" - _options: - canHideAds: "강고 수ᇚ후기" - _condition: - isRemote: "웬겍 사용자" - isCat: "갱이 사용자" - isBot: "자동 사용자" -_gallery: - my: "내 걸" - liked: "좋네예한 걸" - like: "좋네예!" - unlike: "좋네예 무루기" -_email: - _follow: - title: "새 팔로워가 잇십니다" -_serverDisconnectedBehavior: - reload: "알아서 새로곤침" -_channel: - removeBanner: "배너 뭉캐기" - usersCount: "{n}명 참여" - notesCount: "노트 {n}개" -_menuDisplay: - hide: "수ᇚ후기" -_theme: - description: "설멩" - keys: - mention: "멘션" - renote: "리노트" -_sfx: - note: "새 노트" - notification: "알림" - reaction: "리액션 개리기" -_2fa: - step3Title: "학인 기호럴 서기" - renewTOTPCancel: "뎃어예" -_permissions: - "read:favorites": "질겨찾기 보기" - "write:favorites": "질겨찾기 곤치기" -_widgets: - profile: "프로필" - instanceInfo: "서버 정보" - notifications: "알림" - timeline: "타임라인" - activity: "할동" - federation: "옌합" - jobQueue: "작업 대기옐" - _userList: - chooseList: "리스트 개리기" -_cw: - hide: "수ᇚ후기" - show: "더 볼래예" - chars: "걸자 {count}개" - files: "파일 {count}개" -_visibility: - home: "덜머리" - followers: "팔로워" -_postForm: - _placeholders: - e: "옇다 서 주이소" -_profile: - name: "이럼" - username: "사용자 이럼" -_exportOrImport: - favoritedNotes: "질겨찾기한 노트" - clips: "클립 맨걸기" - followingList: "팔로잉" - muteList: "수ᇚ후기" - blockingList: "차단하기" - userLists: "리스트" -_charts: - federation: "옌합" -_timelines: - home: "덜머리" -_play: - my: "내 플레이" - script: "스크립트" - summary: "설멩" -_pages: - like: "좋네예" - unlike: "좋네예 무루기" - my: "내 페이지" - blocks: - image: "이미지" - _note: - id: "노트 아이디" -_notification: - youWereFollowed: "새 팔로워가 잇십니다" - newNote: "새 걸" - _types: - follow: "팔로잉" - mention: "멘션" - renote: "리노트" - quote: "따오기" - reaction: "반엉" - login: "로그인" - _actions: - reply: "답하기" - renote: "리노트" -_deck: - _columns: - notifications: "알림" - tl: "타임라인" - antenna: "안테나" - list: "리스트" - mentions: "받언 멘션" -_webhookSettings: - name: "이럼" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "전자우펜" -_moderationLogTypes: - suspend: "얼우기" - deleteNote: "노트 뭉캐기" - deleteUserAnnouncement: "사용자 공지 걸 뭉캐기" - resetPassword: "비밀번호 재설정" - resolveAbuseReport: "신고 해겔하기" -_reversi: - reversi: "리버시" - chooseBoard: "보드 개리기" - black: "꺼멍" - white: "허영" - total: "합게" -_remoteLookupErrors: - _noSuchObject: - title: "몬 찾앗십니다" -_search: - searchScopeAll: "말캉" - searchScopeUser: "사용자 지정" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index c7d36b4a01..faad1175cb 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -2,24 +2,20 @@ _lang_: "한국어" headlineMisskey: "노트로 연결되는 네트워크" introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로 블로그 서비스입니다.\n'노트'를 작성해서 지금 일어나고 있는 일을 공유하거나, 당신만의 이야기를 모두에게 발신하세요📡\n'리액션' 기능으로 친구의 노트에 총알같이 반응을 추가할 수도 있습니다👍\n새로운 세계를 탐험해 보세요🚀" -poweredByMisskeyDescription: "{name} 서버는 오픈소스 플랫폼 Misskey의 서버 가운데 하나입니다." +poweredByMisskeyDescription: "{name}은(는) 오픈소스 플랫폼 Misskey를 사용한 서버 가운데 하나입니다." monthAndDay: "{month}월 {day}일" search: "검색" -reset: "초기화" notifications: "알림" username: "유저명" password: "비밀번호" -initialPasswordForSetup: "초기 설정용 비밀번호" -initialPasswordIsIncorrect: "초기 설정용 비밀번호가 올바르지 않습니다." -initialPasswordForSetupDescription: "Misskey를 직접 설치하는 경우, 설정 파일에 입력해둔 비밀번호를 사용하세요.\nMisskey 설치를 도와주는 호스팅 서비스 등을 사용하는 경우, 서비스 제공자로부터 받은 비밀번호를 사용하세요.\n비밀번호를 따로 설정하지 않은 경우, 아무것도 입력하지 않아도 됩니다." forgotPassword: "비밀번호 재설정" -fetchingAsApObject: "연합에서 찾아보는 중" +fetchingAsApObject: "연합에서 조회 중" ok: "확인" gotIt: "알겠어요" cancel: "취소" noThankYou: "나중에" enterUsername: "유저명 입력" -renotedBy: "{user}님이 리노트" +renotedBy: "{user}님의 리노트" noNotes: "노트가 없습니다" noNotifications: "표시할 알림이 없습니다" instance: "서버" @@ -42,30 +38,23 @@ addUser: "유저 추가" favorite: "즐겨찾기" favorites: "즐겨찾기" unfavorite: "즐겨찾기에서 제거" -favorited: "즐겨찾기에 등록했습니다." -alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다." +favorited: "즐겨찾기에 등록했습니다" +alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다" cantFavorite: "즐겨찾기에 등록하지 못했습니다." pin: "프로필에 고정" unpin: "프로필에서 고정 해제" copyContent: "내용 복사" copyLink: "링크 복사" -copyRemoteLink: "리모트 서버의 링크로 복사하기" -copyLinkRenote: "리노트 링크 복사" delete: "삭제" deleteAndEdit: "삭제 후 편집" deleteAndEditConfirm: "이 노트를 삭제한 뒤 다시 편집하시겠습니까? 이 노트에 대한 리액션, 리노트, 답글 또한 모두 삭제됩니다." addToList: "리스트에 추가" -addToAntenna: "안테나에 추가" sendMessage: "메시지 보내기" copyRSS: "RSS 복사" copyUsername: "유저명 복사" copyUserId: "유저 ID 복사" copyNoteId: "노트 ID 복사" -copyFileId: "파일 ID 복사" -copyFolderId: "폴더 ID 복사" -copyProfileUrl: "프로필 URL 복사" -searchUser: "유저 검색" -searchThisUsersNotes: "유저의 노트를 검색" +searchUser: "사용자 검색" reply: "답글" loadMore: "더 보기" showMore: "더 보기" @@ -81,7 +70,7 @@ import: "가져오기" export: "내보내기" files: "파일" download: "다운로드" -driveFileDeleteConfirm: "‘{name}’ 파일을 삭제하시겠습니까? 이 파일을 사용하는 일부 콘텐츠도 삭제됩니다." +driveFileDeleteConfirm: "파일 \"{name}\" 을 삭제하시겠습니까? 이 파일이 첨부된 노트도 함께 삭제됩니다." unfollowConfirm: "{name}님을 언팔로우하시겠습니까?" exportRequested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 \"드라이브\"에 추가됩니다." importRequested: "가져오기를 요청하였습니다. 이 작업에는 시간이 걸릴 수 있습니다." @@ -91,7 +80,7 @@ note: "노트" notes: "노트" following: "팔로잉" followers: "팔로워" -followsYou: "나를 팔로우 합니다" +followsYou: "당신을 팔로우합니다" createList: "리스트 만들기" manageLists: "리스트 관리" error: "오류" @@ -99,7 +88,7 @@ somethingHappened: "오류가 발생했습니다" retry: "다시 시도" pageLoadError: "페이지를 불러오지 못했습니다." pageLoadErrorDescription: "네트워크 연결 또는 브라우저 캐시로 인해 발생했을 가능성이 높습니다. 캐시를 삭제하거나, 잠시 후 다시 시도해 주세요." -serverIsDead: "서버가 응답하지 않습니다. 잠시 후 다시 시도해 주세요." +serverIsDead: "서버로부터 응답이 없습니다. 잠시 후 다시 시도해주세요." youShouldUpgradeClient: "이 페이지를 표시하려면 새로고침하여 새로운 버전의 클라이언트를 이용해 주십시오." enterListName: "리스트 이름을 입력" privacy: "프라이버시" @@ -114,38 +103,29 @@ enterEmoji: "이모지 입력" renote: "리노트" unrenote: "리노트 취소" renoted: "리노트했습니다" -renotedToX: "{name}명이 리노트했습니다." cantRenote: "이 게시물은 리노트 할 수 없습니다." cantReRenote: "리노트를 리노트 할 수 없습니다." quote: "인용" inChannelRenote: "채널 내 리노트" inChannelQuote: "채널 내 인용" -renoteToChannel: "채널에 리노트" -renoteToOtherChannel: "다른 채널에 리노트" -pinnedNote: "고정된 노트" -pinned: "고정하기" -you: "나" +pinnedNote: "고정해놓은 노트" +pinned: "프로필에 고정" +you: "당신" clickToShow: "클릭하여 보기" -sensitive: "열람 주의" +sensitive: "열람주의" add: "추가" reaction: "리액션" reactions: "리액션" -emojiPicker: "이모지 선택기" -pinnedEmojisForReactionSettingDescription: "리액션을 할 때 이모지 선택기 상단에 표시할 이모지를 설정할 수 있습니다." -pinnedEmojisSettingDescription: "이모지를 입력할 때 이모지 선택기 상단에 표시할 이모지를 설정할 수 있습니다." -emojiPickerDisplay: "선택기 표시" -overwriteFromPinnedEmojisForReaction: "리액션 설정을 덮어쓰기" -overwriteFromPinnedEmojis: "일반 설정을 덮어쓰기" +reactionSetting: "선택기에 표시할 리액션" reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다." rememberNoteVisibility: "공개 범위를 기억하기" attachCancel: "첨부 취소" -deleteFile: "파일 삭제" markAsSensitive: "열람주의로 설정" unmarkAsSensitive: "열람주의 해제" enterFileName: "파일명을 입력" mute: "뮤트" unmute: "뮤트 해제" -renoteMute: "리노트 뮤트" +renoteMute: "리노트를 뮤트" renoteUnmute: "리노트 뮤트 해제" block: "차단" unblock: "차단 해제" @@ -160,7 +140,6 @@ editList: "리스트 편집" selectChannel: "채널 선택" selectAntenna: "안테나 선택" editAntenna: "안테나 편집" -createAntenna: "안테나 만들기" selectWidget: "위젯 선택" editWidgets: "위젯 편집" editWidgetsExit: "편집 종료" @@ -172,28 +151,21 @@ emojiUrl: "이모지 URL" addEmoji: "이모지 추가" settingGuide: "추천 설정" cacheRemoteFiles: "리모트 파일을 캐시" -cacheRemoteFilesDescription: "이 설정을 활성화하면 리모트 파일을 이 서버의 스토리지에 캐시합니다. 미디어의 표시가 빨라지지만, 서버의 저장 용량을 크게 소모합니다. 리모트 유저의 미디어를 얼마나 보관할 지는 역할의 드라이브 용량 제한에 따라 결정되며, 정해진 용량을 넘길 경우 오래된 파일부터 차례대로 삭제한 뒤 링크로 전환합니다. \n비활성화하면 리모트 파일을 직접 링크하며, 이 경우 이미지 썸네일 생성 및 유저 프라이버시 보호를 위해 default.yml에서 proxyRemoteFiles를 true로 설정하는 것을 권장합니다." -youCanCleanRemoteFilesCache: "파일 관리 화면의 🗑️ 버튼을 눌러 모든 캐시를 삭제할 수 있습니다." -cacheRemoteSensitiveFiles: "리모트의 민감한 파일을 캐시" -cacheRemoteSensitiveFilesDescription: "이 설정을 비활성화하면 리모트의 민감한 파일은 캐시하지 않고 리모트에서 직접 가져오도록 합니다." +cacheRemoteFilesDescription: "이 설정을 해지하면 리모트 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다." flagAsBot: "나는 봇입니다" flagAsBotDescription: "이 계정을 자동화된 수단으로 운용할 경우에 활성화해 주세요. 이 플래그를 활성화하면, 다른 봇이 이를 참고하여 봇 끼리의 무한 연쇄 반응을 회피하거나, 이 계정의 시스템 상에서의 취급이 Bot 운영에 최적화되는 등의 변화가 생깁니다." -flagAsCat: "미야아아아오오오오오오오오오옹!!!!!!!" -flagAsCatDescription: "야옹?(이 계정이 고양이라면 눌러 주세요.)" +flagAsCat: "나는 고양이다냥" +flagAsCatDescription: "이 계정이 고양이라면 활성화 해주세요." flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기" flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다." autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락" addAccount: "계정 추가" -reloadAccountsList: "계정 목록 새로고침" +reloadAccountsList: "계정 리스트 정보 갱신" loginFailed: "로그인에 실패했습니다" showOnRemote: "리모트에서 보기" -continueOnRemote: "리모트에서 계속" -chooseServerOnMisskeyHub: "Misskey Hub에서 서버 찾아보기" -specifyServerHost: "서버 도메인 직접 지정" -inputHostName: "도메인을 입력하세요" general: "일반" wallpaper: "배경" -setWallpaper: "배경 설정" +setWallpaper: "배경화면 설정" removeWallpaper: "배경 제거" searchWith: "검색: {q}" youHaveNoLists: "리스트가 없습니다" @@ -201,7 +173,6 @@ followConfirm: "{name}님을 팔로우 하시겠습니까?" proxyAccount: "프록시 계정" proxyAccountDescription: "프록시 계정은 특정 조건 하에서 유저의 리모트 팔로우를 대행하는 계정입니다. 예를 들면, 유저가 리모트 유저를 리스트에 넣었을 때, 리스트에 들어간 유저를 아무도 팔로우한 적이 없다면 액티비티가 서버로 배달되지 않기 때문에, 대신 프록시 계정이 해당 유저를 팔로우하도록 합니다." host: "호스트" -selectSelf: "본인을 선택" selectUser: "유저 선택" recipient: "수신인" annotation: "내용에 대한 주석" @@ -216,11 +187,8 @@ perHour: "1시간마다" perDay: "1일마다" stopActivityDelivery: "액티비티 보내지 않기" blockThisInstance: "이 서버를 차단" -silenceThisInstance: "서버를 사일런스" -mediaSilenceThisInstance: "서버의 미디어를 사일런스" operations: "작업" software: "소프트웨어" -softwareName: "소프트웨어 이름" version: "버전" metadata: "메타데이터" withNFiles: "{n}개의 파일" @@ -233,17 +201,11 @@ instanceInfo: "서버 정보" statistics: "통계" clearQueue: "대기열 비우기" clearQueueConfirmTitle: "대기열을 비우시겠습니까?" -clearQueueConfirmText: "대기열에 남아 있는 노트는 더 이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다." +clearQueueConfirmText: "대기열에 남아 있는 노트는 더이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다." clearCachedFiles: "캐시 비우기" clearCachedFilesConfirm: "캐시된 리모트 파일을 모두 삭제하시겠습니까?" blockedInstances: "차단된 서버" blockedInstancesDescription: "차단하려는 서버의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다." -silencedInstances: "사일런스한 서버" -silencedInstancesDescription: "사일런스하려는 서버의 호스트명을 한 줄에 하나씩 입력합니다. 사일런스된 서버에 소속된 유저는 모두 '사일런스'된 상태로 취급되며, 이 서버로부터의 팔로우가 프로필 설정과 무관하게 승인제로 변경되고, 팔로워가 아닌 로컬 유저에게는 멘션할 수 없게 됩니다. 정지된 서버에는 적용되지 않습니다." -mediaSilencedInstances: "미디어를 사일런스한 서버" -mediaSilencedInstancesDescription: "미디어를 사일런스 하려는 서버의 호스트를 한 줄에 하나씩 입력합니다. 미디어가 사일런스된 서버의 유저가 업로드한 파일은 모두 민감한 미디어로 처리되며, 커스텀 이모지를 사용할 수 없게 됩니다. 또한, 차단한 인스턴스에는 적용되지 않습니다." -federationAllowedHosts: "연합을 허가하는 서버" -federationAllowedHostsDescription: "연합을 허가하는 서버의 호스트를 엔터로 구분해서 설정합니다." muteAndBlock: "뮤트 및 차단" mutedUsers: "뮤트한 유저" blockedUsers: "차단한 유저" @@ -251,6 +213,7 @@ noUsers: "아무도 없습니다" editProfile: "프로필 수정" noteDeleteConfirm: "이 노트를 삭제하시겠습니까?" pinLimitExceeded: "더 이상 고정할 수 없습니다." +intro: "Misskey의 설치가 완료되었습니다! 관리자 계정을 생성해주세요." done: "완료" processing: "처리중" preview: "미리보기" @@ -273,22 +236,22 @@ security: "보안" retypedNotMatch: "입력이 일치하지 않습니다." currentPassword: "현재 비밀번호" newPassword: "새 비밀번호" -newPasswordRetype: "새 비밀번호(재입력)" +newPasswordRetype: "새 비밀번호 (재입력)" attachFile: "파일 첨부" -more: "더 보기!" -featured: "유행" +more: "더보기" +featured: "하이라이트" usernameOrUserId: "유저명이나 ID" noSuchUser: "유저를 찾을 수 없습니다" -lookup: "찾아보기" +lookup: "조회" announcements: "공지사항" imageUrl: "이미지 URL" remove: "삭제" -removed: "삭제했습니다" +removed: "삭제하였습니다" removeAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?" deleteAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?" resetAreYouSure: "초기화 하시겠습니까?" -areYouSure: "계속 진행하시겠습니까?" -saved: "저장했습니다" +saved: "저장하였습니다" +messaging: "대화" upload: "업로드" keepOriginalUploading: "원본 이미지를 유지" keepOriginalUploadingDescription: "이미지를 업로드할 때에 원본을 그대로 유지합니다. 비활성화하면 업로드할 때 브라우저에서 웹 공개용 이미지를 생성합니다." @@ -298,11 +261,10 @@ uploadFromUrl: "URL 업로드" uploadFromUrlDescription: "업로드하려는 파일의 URL" uploadFromUrlRequested: "업로드를 요청했습니다" uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 수 있습니다." -uploadNFiles: "{n}개의 파일을 업로" -explore: "둘러보기" +explore: "발견하기" messageRead: "읽음" noMoreHistory: "이것보다 과거의 기록이 없습니다" -startChat: "채팅을 시작하기" +startMessaging: "대화 시작하기" nUsersRead: "{n}명이 읽음" agreeTo: "{0}에 동의" agree: "동의합니다" @@ -327,22 +289,18 @@ dark: "다크" lightThemes: "밝은 테마" darkThemes: "어두운 테마" syncDeviceDarkMode: "디바이스의 다크 모드 설정과 동기화" -switchDarkModeManuallyWhenSyncEnabledConfirm: "'{x}'가 켜져 있습니다. 동기화를 끄고 수동으로 모드를 변경하겠습니까?" drive: "드라이브" fileName: "파일명" selectFile: "파일 선택" selectFiles: "파일 선택" selectFolder: "폴더 선택" selectFolders: "폴더 선택" -fileNotSelected: "파일을 선택하지 않았습니다" renameFile: "파일 이름 변경" -folderName: "폴더 이름" +folderName: "폴더명" createFolder: "폴더 만들기" renameFolder: "폴더 이름 바꾸기" deleteFolder: "폴더 삭제" -folder: "폴더" addFile: "파일 추가" -showFile: "파일 표시하기" emptyDrive: "드라이브가 비어 있습니다" emptyFolder: "폴더가 비어 있습니다" unableToDelete: "삭제할 수 없습니다" @@ -355,24 +313,23 @@ copyUrl: "URL 복사" rename: "이름 변경" avatar: "아바타" banner: "배너" -displayOfSensitiveMedia: "민감한 미디어 표시" whenServerDisconnected: "서버와의 접속이 끊겼을 때" disconnectedFromServer: "서버와의 연결이 끊어졌습니다" reload: "새로고침" doNothing: "무시하기" reloadConfirm: "새로고침 하시겠습니까?" -watch: "관심 갖기" -unwatch: "관심 해제하기" -accept: "수락하기" -reject: "거절하기" -normal: "일반" +watch: "지켜보기" +unwatch: "지켜보기 해제" +accept: "허가" +reject: "거부" +normal: "정상" instanceName: "서버 이름" instanceDescription: "서버 소개" maintainerName: "관리자 이름" maintainerEmail: "관리자 이메일" tosUrl: "이용약관 URL" thisYear: "올해" -thisMonth: "이달" +thisMonth: "이번 달" today: "오늘" dayX: "{day}일" monthX: "{month}월" @@ -385,28 +342,25 @@ enableLocalTimeline: "로컬 타임라인 활성화" enableGlobalTimeline: "글로벌 타임라인 활성화" disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리자 및 모더레이터는 계속 사용할 수 있습니다." registration: "등록" +enableRegistration: "신규 회원가입을 활성화" invite: "초대" driveCapacityPerLocalAccount: "로컬 유저 한 명당 드라이브 용량" -driveCapacityPerRemoteAccount: "리모트 유저별 드라이브 용량" +driveCapacityPerRemoteAccount: "리모트 유저 한 명당 드라이브 용량" inMb: "메가바이트 단위" +iconUrl: "아이콘 URL" bannerUrl: "배너 이미지 URL" backgroundImageUrl: "배경 이미지 URL" basicInfo: "기본 정보" -pinnedUsers: "고정한 유저" +pinnedUsers: "고정된 유저" pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다." pinnedPages: "고정한 페이지" pinnedPagesDescription: "서버의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다." pinnedClipId: "고정할 클립의 ID" -pinnedNotes: "고정된 노트" +pinnedNotes: "고정해놓은 노트" hcaptcha: "hCaptcha" enableHcaptcha: "hCaptcha 활성화" hcaptchaSiteKey: "사이트 키" hcaptchaSecretKey: "시크릿 키" -mcaptcha: "mCaptcha" -enableMcaptcha: "mCaptcha 활성화" -mcaptchaSiteKey: "사이트 키" -mcaptchaSecretKey: "시크릿 키" -mcaptchaInstanceUrl: "mCaptcha 인스턴스 URL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHA 활성화" recaptchaSiteKey: "사이트 키" @@ -422,11 +376,9 @@ name: "이름" antennaSource: "받을 소스" antennaKeywords: "받을 키워드" antennaExcludeKeywords: "제외할 키워드" -antennaExcludeBots: "봇 계정 제외" antennaKeywordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다" notifyAntenna: "새로운 노트를 알림" withFileAntenna: "파일이 첨부된 노트만" -excludeNotesInSensitiveChannel: "민감한 채널의 노트 제외" enableServiceworker: "ServiceWorker 사용" antennaUsersDescription: "유저명을 한 줄에 한 명씩 적습니다" caseSensitive: "대소문자를 구분" @@ -439,9 +391,9 @@ silenceConfirm: "이 계정을 사일런스로 설정하시겠습니까?" unsilence: "사일런스 해제" unsilenceConfirm: "이 계정의 사일런스를 해제하시겠습니까?" popularUsers: "인기 유저" -recentlyUpdatedUsers: "최근에 활동한 유저" -recentlyRegisteredUsers: "최근에 가입한 유저" -recentlyDiscoveredUsers: "최근에 발견한 유저" +recentlyUpdatedUsers: "최근 활동한 유저" +recentlyRegisteredUsers: "최근 가입한 유저" +recentlyDiscoveredUsers: "최근 발견한 유저" exploreUsersCount: "{count}명의 유저가 있습니다" exploreFediverse: "연합우주를 탐색" popularTags: "인기 태그" @@ -451,23 +403,18 @@ aboutMisskey: "Misskey에 대하여" administrator: "관리자" token: "토큰" 2fa: "2단계 인증" -setupOf2fa: "2단계 인증 설정" totp: "인증 앱" totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력" moderator: "모더레이터" -moderation: "조정" -moderationNote: "조정 기록" -moderationNoteDescription: "모더레이터 역할을 가진 유저만 보이는 메모를 적을 수 있습니다." -addModerationNote: "조정 기록 추가하기" -moderationLogs: "모더레이션 로그" +moderation: "모더레이션" nUsersMentioned: "{n}명이 언급함" -securityKeyAndPasskey: "보안 키 또는 패스키" +securityKeyAndPasskey: "보안 키 또는 패스 키" securityKey: "보안 키" lastUsed: "마지막 사용" lastUsedAt: "마지막 사용: {t}" unregister: "등록 해제" passwordLessLogin: "비밀번호 없이 로그인" -passwordLessLoginDescription: "비밀번호 없이 보안 키 또는 패스키만 사용해서 로그인합니다." +passwordLessLoginDescription: "비밀번호를 사용하지 않고 보안 키 또는 패스 키 등으로만 로그인합니다." resetPassword: "비밀번호 재설정" newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다" reduceUiAnimation: "UI의 애니메이션을 줄이기" @@ -475,6 +422,7 @@ share: "공유" notFound: "찾을 수 없습니다" notFoundDescription: "지정한 URL에 해당하는 페이지가 존재하지 않습니다." uploadFolder: "기본 업로드 위치" +cacheClear: "캐시 지우기" markAsReadAllNotifications: "모든 알림을 읽은 상태로 표시" markAsReadAllUnreadNotes: "모든 글을 읽은 상태로 표시" markAsReadAllTalkMessages: "모든 대화를 읽은 상태로 표시" @@ -491,11 +439,11 @@ next: "다음" retype: "다시 입력" noteOf: "{user}의 노트" quoteAttached: "인용함" -quoteQuestion: "인용해서 첨부하시겠습니까?" -attachAsFileQuestion: "붙여넣으려는 글이 너무 깁니다. 텍스트 파일로 첨부하시겠습니까?" +quoteQuestion: "인용해서 작성하시겠습니까?" +noMessagesYet: "아직 대화가 없습니다" +newMessageExists: "새 메시지가 있습니다" onlyOneFileCanBeAttached: "메시지에 첨부할 수 있는 파일은 하나까지입니다" -signinRequired: "진행하기 전에 로그인을 해 주세요" -signinOrContinueOnRemote: "계속하려면 사용하는 서버로 이동하거나 이 서버에 로그인해야 합니다." +signinRequired: "로그인 해주세요" invitations: "초대" invitationCode: "초대 코드" checking: "확인하는 중입니다" @@ -510,19 +458,15 @@ strongPassword: "강한 비밀번호" passwordMatched: "일치합니다" passwordNotMatched: "일치하지 않습니다" signinWith: "{x}로 로그인" -signinFailed: "로그인할 수 없습니다. 유저 이름과 비밀번호를 확인해 주십시오." +signinFailed: "로그인할 수 없습니다. 사용자명과 비밀번호를 확인하여 주십시오." or: "혹은" language: "언어" uiLanguage: "UI 표시 언어" aboutX: "{x}에 대하여" emojiStyle: "이모지 스타일" -native: "기본" -menuStyle: "메뉴 스타일" -style: "스타일" -drawer: "서랍" -popup: "팝업" -showNoteActionsOnlyHover: "마우스가 올라간 때에만 노트 동작 버튼을 표시하기" -showReactionsCount: "노트의 리액션 수를 표시하기" +native: "네이티브" +disableDrawer: "드로어 메뉴를 사용하지 않기" +showNoteActionsOnlyHover: "노트 액션 버튼을 마우스를 올렸을 때에만 표시" noHistory: "기록이 없습니다" signinHistory: "로그인 기록" enableAdvancedMfm: "고급 MFM을 활성화" @@ -548,7 +492,7 @@ dayOverDayChanges: "어제보다" appearance: "모양" clientSettings: "클라이언트 설정" accountSettings: "계정 설정" -promotion: "홍보" +promotion: "프로모션" promote: "프로모션하기" numberOfDays: "며칠동안" hideThisNote: "이 노트를 숨기기" @@ -558,13 +502,13 @@ useObjectStorage: "오브젝트 스토리지를 사용" objectStorageBaseUrl: "Base URL" objectStorageBaseUrlDesc: "오브젝트 (미디어) 참조 URL 을 만들 때 사용되는 URL입니다. CDN 또는 프록시를 사용하는 경우 그 URL을 지정하고, 그 외의 경우 사용할 서비스의 가이드에 따라 공개적으로 액세스 할 수 있는 주소를 지정해 주세요. 예를 들어, AWS S3의 경우 'https://.s3.amazonaws.com', GCS등의 경우 'https://storage.googleapis.com/' 와 같이 지정합니다." objectStorageBucket: "Bucket" -objectStorageBucketDesc: "사용하는 서비스의 bucket 이름을 지정해 주세요." +objectStorageBucketDesc: "사용 서비스의 bucket명을 지정해주세요." objectStoragePrefix: "Prefix" objectStoragePrefixDesc: "이 Prefix 의 디렉토리 아래에 파일이 저장됩니다." objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "AWS S3는 비워 두고 다른 서비스는 각 서비스의 endpoint를 설정해 주세요. ‘’ 혹은 ‘:’처럼 지정합니다." +objectStorageEndpointDesc: "AWS S3의 경우 공란, 다른 서비스의 경우 각 서비스의 가이드에 맞게 endpoint를 설정해주세요. '' 혹은 ':' 와 같이 지정합니다." objectStorageRegion: "Region" -objectStorageRegionDesc: "‘xx-east-1’처럼 region을 지정해 주세요. 사용하는 서비스에 region 개념이 없으면 ‘us-east-1’처럼 설정해 주세요. AWS 설정 파일이나 환경 변수가 있으면 비워 주세요." +objectStorageRegionDesc: "'xx-east-1'와 같이 region을 지정해 주세요. 사용하는 서비스에 region 개념이 없는 경우 'us-east-1'으로 설정해 주세요. AWS 설정 파일 또는 환경 변수를 참조할 경우에는 비워주세요." objectStorageUseSSL: "SSL 사용" objectStorageUseSSLDesc: "API 호출시 HTTPS 를 사용하지 않는 경우 OFF 로 설정해 주세요" objectStorageUseProxy: "연결에 프록시를 사용" @@ -573,24 +517,18 @@ objectStorageSetPublicRead: "업로드할 때 'public-read'를 설정하기" s3ForcePathStyleDesc: "s3ForcePathStyle을 활성화하면, 버킷 이름을 URL의 호스트명이 아닌 경로의 일부로써 취급합니다. 셀프 호스트 Minio와 같은 서비스를 사용할 경우 활성화해야 할 수 있습니다." serverLogs: "서버 로그" deleteAll: "모두 삭제" -showFixedPostForm: "타임라인 상단에 글 입력란을 표시" -showFixedPostFormInChannel: "채널 타임라인 상단에 글 입력란을 표시" -withRepliesByDefaultForNewlyFollowed: "팔로우 할 때 기본적으로 답글을 타임라인에 나오게 하기" +showFixedPostForm: "타임라인 상단에 글 작성란을 표시" +showFixedPostFormInChannel: "채널 타임라인 상단에 글 작성란을 표시" newNoteRecived: "새 노트가 있습니다" -newNote: "새로운 노트" sounds: "소리" sound: "소리" -notificationSoundSettings: "알림 설정" listen: "듣기" none: "없음" showInPage: "페이지로 보기" popout: "새 창으로 열기" volume: "음량" masterVolume: "마스터 볼륨" -notUseSound: "음소거 하기" -useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기" details: "자세히" -renoteDetails: "리노트 상세 내용" chooseEmoji: "이모지 선택" unableToProcess: "작업을 완료할 수 없습니다" recentUsed: "최근 사용" @@ -606,21 +544,15 @@ ascendingOrder: "오름차순" descendingOrder: "내림차순" scratchpad: "스크래치 패드" scratchpadDescription: "스크래치 패드는 AiScript 의 테스트 환경을 제공합니다. Misskey 와 상호 작용하는 코드를 작성, 실행 및 결과를 확인할 수 있습니다." -uiInspector: "UI 인스펙터" -uiInspectorDescription: "메모리에 있는 UI 컴포넌트의 인스턴트 목록을 볼 수 있습니다. UI 컴포넌트는 Ui:C: 계열 함수로 만들어집니다." output: "출력" script: "스크립트" disablePagesScript: "Pages 에서 AiScript 를 사용하지 않음" updateRemoteUser: "리모트 유저 정보 갱신" -unsetUserAvatar: "아바타 제거" -unsetUserAvatarConfirm: "아바타를 제거할까요?" -unsetUserBanner: "배너 제거" -unsetUserBannerConfirm: "배너를 제거할까요?" deleteAllFiles: "모든 파일 삭제" deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?" removeAllFollowing: "모든 팔로잉 해제" -removeAllFollowingDescription: "{host} 서버의 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않는 경우 등에 실행해 주세요." -userSuspended: "이 유저는 정지되었습니다." +removeAllFollowingDescription: "{host}(으)로부터 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않게 된 경우 등에 실행해 주세요." +userSuspended: "이 계정은 정지된 상태입니다." userSilenced: "이 계정은 사일런스된 상태입니다." yourAccountSuspendedTitle: "계정이 정지되었습니다" yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오." @@ -639,13 +571,13 @@ addedRelays: "추가된 릴레이" serviceworkerInfo: "푸시 알림을 수행하려면 활성화해야 합니다." deletedNote: "삭제된 노트" invisibleNote: "비공개 노트" -enableInfiniteScroll: "자동으로 더 보기" +enableInfiniteScroll: "자동으로 좀 더 보기" visibility: "공개 범위" poll: "투표" useCw: "내용 숨기기" enablePlayer: "플레이어 열기" disablePlayer: "플레이어 닫기" -expandTweet: "게시물 확장하기" +expandTweet: "트윗 확장하기" themeEditor: "테마 에디터" description: "설명" describeFile: "캡션 추가" @@ -666,7 +598,6 @@ medium: "보통" small: "작게" generateAccessToken: "액세스 토큰 생성" permission: "권한" -adminPermission: "관리자 권한" enableAll: "전체 선택" disableAll: "전체 해제" tokenRequested: "계정 접근 허용" @@ -681,26 +612,20 @@ emailAddress: "메일 주소" smtpConfig: "SMTP 서버 설정" smtpHost: "호스트" smtpPort: "포트" -smtpUser: "유저 이름" +smtpUser: "유저명" smtpPass: "비밀번호" emptyToDisableSmtpAuth: "SMTP 인증을 사용하지 않으려면 공란으로 비워둡니다." smtpSecure: "SMTP 연결에 Implicit SSL/TTS 사용" smtpSecureInfo: "STARTTLS 사용 시에는 해제합니다." testEmail: "이메일 전송 테스트" wordMute: "단어 뮤트" -wordMuteDescription: "정해진 단어가 포함된 노트를 최소화 한 상태로 표시합니다. 최소화 된 노트는 클릭해서 표시할 수 있습니다." -hardWordMute: "하드 단어 뮤트" -showMutedWord: "뮤트한 단어를 표시하기" -hardWordMuteDescription: "정한 단어가 들어간 노트를 숨깁니다. 단어 뮤트와 차이점은 노트가 아예 보이지 않습니다." regexpError: "정규 표현식 오류" regexpErrorDescription: "{tab}단어 뮤트 {line}행의 정규 표현식에 오류가 발생했습니다:" instanceMute: "서버 뮤트" userSaysSomething: "{name}님이 무언가를 말했습니다" -userSaysSomethingAbout: "{name}님이 \"{word}\"를 언급했습니다." makeActive: "활성화" -display: "보기" +display: "표시" copy: "복사" -copiedToClipboard: "클립보드에 복사되었습니다." metrics: "통계" overview: "요약" logs: "로그" @@ -715,28 +640,29 @@ useGlobalSettingDesc: "활성화하면 계정의 알림 설정이 적용됩니 other: "기타" regenerateLoginToken: "로그인 토큰을 재생성" regenerateLoginTokenDescription: "로그인할 때 사용되는 내부 토큰을 재생성합니다. 일반적으로 이 작업을 실행할 필요는 없습니다. 이 기능을 사용하면 이 계정으로 로그인한 모든 기기에서 로그아웃됩니다." -theKeywordWhenSearchingForCustomEmoji: "맞춤 이모티콘을 검색할 때 키워드가 됩니다." setMultipleBySeparatingWithSpace: "공백으로 구분하여 여러 개 설정할 수 있습니다." fileIdOrUrl: "파일 ID 또는 URL" behavior: "동작" sample: "예시" abuseReports: "신고" reportAbuse: "신고" -reportAbuseRenote: "리노트 신고하기" -reportAbuseOf: "{name} 신고하기" -fillAbuseReportDescription: "신고 사유를 자세히 기재해 주세요. 대상 노트나 페이지 등이 있는 경우에는 해당 URL도 기재해 주세요." +reportAbuseOf: "{name}을 신고하기" +fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요. 특정 게시물을 신고할 때에는 게시물의 URL도 포함해 주세요." abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다." reporter: "신고자" reporteeOrigin: "피신고자" reporterOrigin: "신고자" +forwardReport: "리모트 서버에도 신고 내용 보내기" +forwardReportIsAnonymous: "리모트 서버에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다." send: "전송" +abuseMarkAsResolved: "해결됨으로 표시" openInNewTab: "새 탭에서 열기" openInSideView: "사이드뷰로 열기" defaultNavigationBehaviour: "기본 탐색 동작" editTheseSettingsMayBreakAccount: "이 설정을 변경하면 계정이 손상될 수 있습니다." instanceTicker: "노트의 서버 정보" waitingFor: "{x}을(를) 기다리고 있습니다" -random: "무작위" +random: "랜덤" system: "시스템" switchUi: "UI 전환" desktop: "데스크탑" @@ -745,34 +671,32 @@ createNew: "새로 만들기" optional: "옵션" createNewClip: "새 클립 만들기" unclip: "클립 해제" -confirmToUnclipAlreadyClippedNote: "이 노트는 ‘{name}’ 클립을 이미 포함합니다. 클립에서 제외하시겠습니까?" +confirmToUnclipAlreadyClippedNote: "이 노트는 이미 \"{name}\" 클립에 포함되어 있습니다. 클립을 해제하시겠습니까?" public: "공개" -private: "비공개" i18nInfo: "Misskey는 자원봉사자들에 의해 다양한 언어로 번역되고 있습니다. {link}에서 번역에 참가할 수 있습니다." manageAccessTokens: "액세스 토큰 관리" accountInfo: "계정 정보" notesCount: "노트 수" repliesCount: "답글 수" -renotesCount: "리노트 수" +renotesCount: "Renote 수" repliedCount: "받은 답글 수" -renotedCount: "받은 리노트 수" +renotedCount: "받은 Renote 수" followingCount: "팔로우 수" followersCount: "팔로워 수" -sentReactionsCount: "리액션 수" +sentReactionsCount: "보낸 리액션 수" receivedReactionsCount: "받은 리액션 수" -pollVotesCount: "투표 수" -pollVotedCount: "받은 투표 수" +pollVotesCount: "투표한 횟수" +pollVotedCount: "투표받은 횟수" yes: "예" no: "아니오" -driveFilesCount: "드라이브에 있는 파일 수" +driveFilesCount: "드라이브 파일 개수" driveUsage: "드라이브 사용량" noCrawle: "검색엔진의 인덱싱 거부" -noCrawleDescription: "검색엔진에 유저 페이지, 노트, 페이지 등의 콘텐츠를 인덱싱되지 않게 합니다." +noCrawleDescription: "검색엔진에 사용자 페이지, 노트, 페이지 등의 콘텐츠를 인덱싱되지 않게 합니다." lockedAccountInfo: "팔로우를 승인으로 승인받더라도 노트의 공개 범위를 '팔로워'로 하지 않는 한 누구나 당신의 노트를 볼 수 있습니다." alwaysMarkSensitive: "미디어를 항상 열람 주의로 설정" loadRawImages: "첨부한 이미지의 썸네일을 원본화질로 표시" disableShowingAnimatedImages: "움직이는 이미지를 자동으로 재생하지 않음" -highlightSensitiveMedia: "미디어가 민감한 내용이라는 것을 알기 쉽게 표시" verificationEmailSent: "확인 메일을 발송하였습니다. 설정을 완료하려면 메일에 첨부된 링크를 확인해 주세요." notSet: "설정되지 않음" emailVerified: "메일 주소가 확인되었습니다." @@ -786,8 +710,9 @@ experimentalFeatures: "실험실" experimental: "실험실" thisIsExperimentalFeature: "이 기능은 실험적인 기능입니다. 사양이 변경되거나 정상적으로 동작하지 않을 가능성이 있습니다." developer: "개발자" -makeExplorable: "계정을 쉽게 발견하도록 하기" +makeExplorable: "\"발견하기\"에 내 계정 보이기" makeExplorableDescription: "비활성화하면 \"발견하기\"에 나의 계정을 표시하지 않습니다." +showGapBetweenNotesInTimeline: "타임라인의 노트 사이를 띄워서 표시" duplicate: "복제" left: "왼쪽" center: "가운데" @@ -795,7 +720,6 @@ wide: "넓게" narrow: "좁게" reloadToApplySetting: "이 설정을 적용하려면 페이지를 새로고침해야 합니다. 바로 새로고침하시겠습니까?" needReloadToApply: "변경 사항은 새로고침하면 적용됩니다." -needToRestartServerToApply: "변경 사항은 새로고침이 필요합니다." showTitlebar: "타이틀 바를 표시하기" clearCache: "캐시 비우기" onlineUsersCount: "{n}명이 접속 중" @@ -829,10 +753,10 @@ editCode: "코드 수정" apply: "적용" receiveAnnouncementFromInstance: "이 서버의 알림을 이메일로 수신할게요" emailNotification: "메일 알림" -publish: "공개" +publish: "게시" inChannelSearch: "채널에서 검색" useReactionPickerForContextMenu: "우클릭하여 리액션 선택기 열기" -typingUsers: "{users}님이 입력 중" +typingUsers: "{users} 님이 입력하고 있어요.." jumpToSpecifiedDate: "특정 날짜로 이동" showingPastTimeline: "과거의 타임라인을 표시하고 있어요" clear: "지우기" @@ -866,14 +790,13 @@ administration: "관리" accounts: "계정" switch: "전환" noMaintainerInformationWarning: "관리자 정보가 설정되어 있지 않습니다." -noInquiryUrlWarning: "문의처 주소를 설정하지 않았습니다." noBotProtectionWarning: "Bot 방어가 설정되어 있지 않습니다." configure: "설정하기" postToGallery: "갤러리에 업로드" postToHashtag: "이 해시태그에 게시" gallery: "갤러리" -recentPosts: "최근 게시물" -popularPosts: "인기 게시물" +recentPosts: "최근 포스트" +popularPosts: "인기 포스트" shareWithNote: "노트로 공유" ads: "광고" expiration: "기한" @@ -889,7 +812,7 @@ previewNoteText: "본문 미리보기" customCss: "CSS 사용자화" customCssWarn: "이 설정은 기능을 알고 있는 경우에만 사용해야 합니다. 잘못된 값을 입력하면 클라이언트가 정상적으로 작동하지 않을 수 있습니다." global: "글로벌" -squareAvatars: "프로필 아바타를 사각형으로 표시" +squareAvatars: "프로필 아이콘을 사각형으로 표시" sent: "전송" received: "수신" searchResult: "검색 결과" @@ -902,14 +825,14 @@ whatIsNew: "패치 정보 보기" translate: "번역" translatedFrom: "{x}에서 번역" accountDeletionInProgress: "계정 삭제 작업을 진행하고 있습니다" -usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 유저명은 나중에 변경할 수 없습니다." +usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 사용자명은 나중에 변경할 수 없습니다." aiChanMode: "아이 모드" devMode: "개발자 모드" keepCw: "CW 유지하기" pubSub: "Pub/Sub 계정" lastCommunication: "마지막 통신" -resolved: "처리함" -unresolved: "처리되지 않음" +resolved: "해결됨" +unresolved: "해결되지 않음" breakFollow: "팔로워 해제" breakFollowConfirm: "팔로우를 해제하시겠습니까?" itsOn: "켜져 있습니다" @@ -924,18 +847,17 @@ manageAccounts: "계정 관리" makeReactionsPublic: "리액션 목록을 공개하기" makeReactionsPublicDescription: "나의 리액션을 누구나 볼 수 있게 합니다." classic: "클래식" -muteThread: "글타래 뮤트" +muteThread: "이 글타래를 뮤트" unmuteThread: "글타래 뮤트 해제" -followingVisibility: "팔로우의 공개 범위" -followersVisibility: "팔로워의 공개 범위" -continueThread: "글타래 더 보기" +ffVisibility: "내 인맥의 공개 범위" +ffVisibilityDescription: "나의 팔로우와 팔로워 정보에 대한 공개 범위를 설정할 수 있습니다." +continueThread: "이 글타래 이어서 보기" deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? " incorrectPassword: "비밀번호가 올바르지 않습니다." -incorrectTotp: "OTP 번호가 틀렸거나 유효기간이 만료되어 있을 수 있습니다." voteConfirm: "\"{choice}\"에 투표하시겠습니까?" hide: "숨기기" useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시" -welcomeBackWithName: "{name}님, 환영합니다." +welcomeBackWithName: "환영합니다, {name}님" clickToFinishEmailVerification: "[{ok}]를 눌러 이메일 인증을 완료하세요." overridedDeviceKind: "장치 유형" smartphone: "스마트폰" @@ -947,7 +869,7 @@ numberOfColumn: "한 줄에 보일 리액션의 수" searchByGoogle: "검색" instanceDefaultLightTheme: "서버 기본 라이트 테마" instanceDefaultDarkTheme: "서버 기본 다크 테마" -instanceDefaultThemeDescription: "객체 형식({}로 감싼 형태)의 테마 코드를 입력해 주세요." +instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요." mutePeriod: "뮤트할 기간" period: "기간" indefinitely: "무기한" @@ -956,9 +878,6 @@ oneHour: "1시간" oneDay: "1일" oneWeek: "일주일" oneMonth: "1개월" -threeMonths: "3개월" -oneYear: "1년" -threeDays: "3일" reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다." failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다" rateLimitExceeded: "요청 제한 횟수를 초과하였습니다" @@ -983,7 +902,6 @@ document: "문서" numberOfPageCache: "페이지 캐시 수" numberOfPageCacheDescription: "숫자가 클 수록 편리성이 높아지지만, 시스템 자원과 메모리를 더 많이 사용합니다." logoutConfirm: "로그아웃 하시겠습니까?" -logoutWillClearClientData: "로그아웃하면 클라이언트의 설정 데이터가 브라우저에서 지워지게 됩니다. 다시 로그인할 때 설정 데이터를 복원할 수 있도록 하려면 설정 자동 백업을 활성화하세요." lastActiveDate: "마지막 이용" statusbar: "상태바" pleaseSelect: "선택해 주세요" @@ -1002,7 +920,6 @@ failedToUpload: "업로드 실패" cannotUploadBecauseInappropriate: "이 파일은 부적절한 내용을 포함한다고 판단되어 업로드할 수 없습니다." cannotUploadBecauseNoFreeSpace: "드라이브 용량이 부족하여 업로드할 수 없습니다." cannotUploadBecauseExceedsFileSizeLimit: "파일 크기가 너무 크기 때문에 업로드할 수 없습니다." -cannotUploadBecauseUnallowedFileType: "허가되지 않은 유형의 파일이기에 업로드할 수 없습니다." beta: "베타" enableAutoSensitive: "자동 NSFW 탐지" enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 서버 정책에 따라 자동으로 설정될 수 있습니다." @@ -1033,18 +950,16 @@ show: "표시" neverShow: "다시 보지 않기" remindMeLater: "나중에 알림" didYouLikeMisskey: "Misskey가 마음에 드시나요?" -pleaseDonate: "Misskey는 {host} 서버의 무료 소프트웨어입니다. 앞으로도 개발을 이어 나가려면 후원이 절실히 필요합니다!" -correspondingSourceIsAvailable: "소스 코드는 {anchor}에서 받아보실 수 있습니다." +pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!" roles: "역할" role: "역할" noRole: "역할이 없습니다" -normalUser: "일반 유저" +normalUser: "일반 사용자" undefined: "정의되지 않음" assign: "할당" unassign: "할당 취소" color: "색" manageCustomEmojis: "커스텀 이모지 관리" -manageAvatarDecorations: "아바타 꾸미기 관리" youCannotCreateAnymore: "더 이상 생성할 수 없습니다." cannotPerformTemporary: "일시적으로 사용할 수 없음" cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시적으로 사용이 불가합니다. 잠시 후 다시 시도해 주세요." @@ -1062,12 +977,11 @@ thisPostMayBeAnnoyingHome: "홈에 게시" thisPostMayBeAnnoyingCancel: "그만두기" thisPostMayBeAnnoyingIgnore: "이대로 게시" collapseRenotes: "이미 본 리노트를 간략화하기" -collapseRenotesDescription: "리액션이나 리노트를 한 노트를 접어서 표시합니다." internalServerError: "내부 서버 오류" internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다." copyErrorInfo: "오류 정보 복사" joinThisServer: "이 서버에 가입" -exploreOtherServers: "다른 서버 찾기" +exploreOtherServers: "다른 서버 둘러보기" letsLookAtTimeline: "타임라인 구경하기" disableFederationConfirm: "정말로 연합을 끄시겠습니까?" disableFederationConfirmWarn: "연합을 끄더라도 게시물이 비공개로 전환되는 것은 아닙니다. 대부분의 경우 연합을 비활성화할 필요가 없습니다." @@ -1080,17 +994,12 @@ reactionAcceptance: "리액션 수신" likeOnly: "좋아요만 받기" likeOnlyForRemote: "리모트에서는 좋아요만 받기" nonSensitiveOnly: "민감한 이모지를 제외하고 받기" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "민감한 이모지를 제외하고 받기(리모트에서는 좋아요만 받기)" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "민감한 이모지를 제외하고 받기 (리모트에서는 좋아요만 받기)" rolesAssignedToMe: "나에게 할당된 역할" resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?" sensitiveWords: "민감한 단어" sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다." sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다." -prohibitedWords: "금지 단어" -prohibitedWordsDescription: "설정된 단어가 포함되는 노트를 게시하려고 하면, 오류가 발생하도록 합니다. 줄바꿈으로 구분지어 복수 설정할 수 있습니다." -prohibitedWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다." -hiddenTags: "숨긴 해시태그" -hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다." notesSearchNotAvailable: "노트 검색을 이용하실 수 없습니다." license: "라이선스" unfavoriteConfirm: "즐겨찾기를 해제하시겠습니까?" @@ -1101,25 +1010,21 @@ retryAllQueuesConfirmTitle: "지금 다시 시도하시겠습니까?" retryAllQueuesConfirmText: "일시적으로 서버의 부하가 증가할 수 있습니다." enableChartsForRemoteUser: "리모트 유저의 차트를 생성" enableChartsForFederatedInstances: "리모트 서버의 차트를 생성" -enableStatsForFederatedInstances: "리모트 서버 정보 받아오기" showClipButtonInNoteFooter: "노트 동작에 클립을 추가" -reactionsDisplaySize: "리액션 표시 크기" -limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시하기" +largeNoteReactions: "노트의 리액션을 크게 표시" noteIdOrUrl: "노트 ID 및 URL" video: "동영상" videos: "동영상" -audio: "소리" -audioFiles: "소리" dataSaver: "데이터 절약 모드" accountMigration: "계정 이동" -accountMoved: "이 유저는 다음 계정으로 이사했습니다:" +accountMoved: "이 사용자는 다음 계정으로 이사했습니다:" accountMovedShort: "이사한 계정입니다" operationForbidden: "사용할 수 없습니다" forceShowAds: "광고를 항상 표시" addMemo: "메모 추가" editMemo: "메모 편집" reactionsList: "리액션 목록" -renotesList: "리노트 목록" +renotesList: "Renote 목록" notificationDisplay: "알림 표시" leftTop: "왼쪽 상단" rightTop: "오른쪽 상단" @@ -1133,25 +1038,20 @@ serverRules: "서버 규칙" pleaseConfirmBelowBeforeSignup: "이 서버에 가입하기 전에 아래 사항을 확인하여 주십시오." pleaseAgreeAllToContinue: "계속하시려면 모든 항목에 동의하십시오." continue: "계속" -preservedUsernames: "예약한 유저명" -preservedUsernamesDescription: "예약할 유저명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 유저명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다." +preservedUsernames: "예약된 사용자명" +preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다." createNoteFromTheFile: "이 파일로 노트를 작성" archive: "아카이브" -archived: "아카이브 됨" -unarchive: "보관 취소" -channelArchiveConfirmTitle: "{name} 채널을 보존하시겠습니까?" -channelArchiveConfirmDescription: "보존한 채널은 채널 목록과 검색 결과에 표시되지 않으며 새로운 노트도 작성할 수 없습니다." -thisChannelArchived: "이 채널은 보존되었습니다." +channelArchiveConfirmTitle: "{name} 을(를) 아카이브하시겠습니까?" +channelArchiveConfirmDescription: "아카이브한 채널은 채널 목록과 검색 결과에 표시되지 않으며, 채널에 새로운 노트를 작성할 수 없게 됩니다." +thisChannelArchived: "이 채널은 아카이브되었습니다." displayOfNote: "노트 표시" initialAccountSetting: "초기 설정" youFollowing: "팔로잉" preventAiLearning: "기계학습(생성형 AI)으로의 사용을 거부" preventAiLearningDescription: "외부의 문장 생성 AI나 이미지 생성 AI에 대해 제출한 노트나 이미지 등의 콘텐츠를 학습의 대상으로 사용하지 않도록 요구합니다. 다만, 이 요구사항을 지킬 의무는 없기 때문에 학습을 완전히 방지하는 것은 아닙니다." options: "옵션" -specifyUser: "유저 지정" -lookupConfirm: "조회 할까요?" -openTagPageConfirm: "해시태그의 페이지를 열까요?" -specifyHost: "호스트 지정" +specifyUser: "사용자 지정" failedToPreviewUrl: "미리 볼 수 없음" update: "업데이트" rolesThatCanBeUsedThisEmojiAsReaction: "이 이모지를 리액션으로 사용할 수 있는 역할" @@ -1166,367 +1066,6 @@ installed: "설치됨" branding: "브랜딩" enableServerMachineStats: "서버의 머신 사양을 공개하기" enableIdenticonGeneration: "유저마다의 Identicon 생성 유효화" -turnOffToImprovePerformance: "이 기능을 끄면 성능이 향상될 수 있습니다." -createInviteCode: "초대 코드 생성" -createWithOptions: "옵션을 지정하여 생성" -createCount: "초대 수" -inviteCodeCreated: "초대 코드 생성됨" -inviteLimitExceeded: "초대 코드 생성 한도를 초과했습니다." -createLimitRemaining: "초대 한도: {limit}회 남음" -inviteLimitResetCycle: " {time}시간 이내에 최대 {limit}개의 초대 코드를 생성할 수 있습니다." -expirationDate: "만료 날짜" -noExpirationDate: "만료기간 없음" -inviteCodeUsedAt: "다음에 사용된 초대 코드" -registeredUserUsingInviteCode: "초대 코드 사용 대상" -waitingForMailAuth: "이메일 인증 보류 중" -inviteCodeCreator: "초대 코드 생성자" -usedAt: "사용 시각" -unused: "사용되지 않음" -used: "사용됨" -expired: "만료됨" -doYouAgree: "동의하십니까?" -beSureToReadThisAsItIsImportant: "중요하므로 반드시 읽어주십시오." -iHaveReadXCarefullyAndAgree: "\"{x}\"의 내용을 읽고 동의합니다." -dialog: "다이얼로그" -icon: "아바타" -forYou: "나에게" -currentAnnouncements: "현재 공지사항" -pastAnnouncements: "과거 공지사항" -youHaveUnreadAnnouncements: "읽지 않은 공지사항이 있습니다." -useSecurityKey: "브라우저 또는 기기의 안내에 따라 보안 키 또는 패스키를 사용해 주십시오." -replies: "답글" -renotes: "리노트" -loadReplies: "답글 보기" -loadConversation: "대화 보기" -pinnedList: "고정된 리스트" -keepScreenOn: "기기 화면을 항상 켜기" -verifiedLink: "이 링크의 소유자임이 확인되었습니다." -notifyNotes: "새 노트 알림 켜기" -unnotifyNotes: "새 노트 알림 끄기" -authentication: "인증" -authenticationRequiredToContinue: "계속하려면 인증하십시오" -dateAndTime: "일시" -showRenotes: "리노트 보기" -edited: "수정됨" -notificationRecieveConfig: "알림 설정" -mutualFollow: "맞팔로우" -followingOrFollower: "팔로 중이거나 팔로워" -fileAttachedOnly: "미디어를 포함한 노트만" -showRepliesToOthersInTimeline: "타임라인에 다른 사람에게 보내는 답글을 포함" -hideRepliesToOthersInTimeline: "타임라인에 다른 사람에게 보내는 답글을 포함하지 않음" -showRepliesToOthersInTimelineAll: "타임라인에 현재 팔로우 중인 사람 전원의 답글을 포함하게 하기" -hideRepliesToOthersInTimelineAll: "타임라인에 현재 팔로우 중인 사람 전원의 답글이 나오지 않게 하기" -confirmShowRepliesAll: "이 조작은 되돌릴 수 없습니다. 정말로 타임라인에 현재 팔로우 중인 사람 전원의 답글이 나오게 하시겠습니까?" -confirmHideRepliesAll: "이 조작은 되돌릴 수 없습니다. 정말로 타임라인에 현재 팔로우 중인 사람 전원의 답글이 나오지 않게 하시겠습니까?" -externalServices: "외부 서비스" -sourceCode: "소스 코드" -sourceCodeIsNotYetProvided: "소스 코드를 아직 제공하지 않습니다. 이 문제를 해결하려면 관리자에게 문의해 주세요." -repositoryUrl: "저장소 URL" -repositoryUrlDescription: "소스 코드를 공개한 저장소가 있는 경우, 그 URL을 적습니다. Misskey를 원본 그대로 (소스 코드를 어떤 식으로도 변경하지 않고) 쓰고 있는 경우 https://github.com/misskey-dev/misskey 라고 적습니다." -repositoryUrlOrTarballRequired: "저장소를 공개하지 않은 경우 대신 tarball을 제공할 필요가 있습니다. 세부사항은 .config/example.yml을 참조해 주세요." -feedback: "피드백" -feedbackUrl: "피드백 URL" -impressum: "운영자 정보" -impressumUrl: "운영자 정보 URL" -impressumDescription: "독일 등의 일부 나라와 지역에서는 꼭 표시해야 합니다(Impressum)." -privacyPolicy: "개인정보 보호 정책" -privacyPolicyUrl: "개인정보 보호 정책 URL" -tosAndPrivacyPolicy: "약관 및 개인정보 보호 정책" -avatarDecorations: "아바타 장식" -attach: "붙이기" -detach: "빼기" -detachAll: "모두 빼기" -angle: "각도" -flip: "플립" -showAvatarDecorations: "아바타 장식 표시" -releaseToRefresh: "놓아서 새로고침" -refreshing: "새로고침 중" -pullDownToRefresh: "아래로 내려서 새로고침" -useGroupedNotifications: "알림을 그룹화하고 표시" -signupPendingError: "메일 주소 확인중에 문제가 발생했습니다. 링크의 유효기간이 지났을 가능성이 있습니다." -cwNotationRequired: "'내용을 숨기기'를 체크한 경우 주석을 써야 합니다." -doReaction: "리액션 추가" -code: "문자열" -reloadRequiredToApplySettings: "설정을 적용하려면 새로고침을 해야 합니다." -remainingN: "나머지: {n}" -overwriteContentConfirm: "현재 내용을 덮어쓰기 합니다. 계속 진행하시겠습니까?" -seasonalScreenEffect: "계절에 따른 효과 보이기" -decorate: "장식하기" -addMfmFunction: "장식 추가하기" -enableQuickAddMfmFunction: "상급자용 MFM 선택기 표시하기" -bubbleGame: "버블 게임" -sfx: "효과음" -soundWillBePlayed: "소리가 재생됩니다" -showReplay: "리플레이 보기" -replay: "리플레이" -replaying: "리플레이 중" -endReplay: "리플레이 종료" -copyReplayData: "리플레이 데이터를 복사" -ranking: "랭킹" -lastNDays: "최근 {n}일" -backToTitle: "타이틀로 가기" -hemisphere: "거주 지역" -withSensitive: "민감한 파일이 포함된 노트 보기" -userSaysSomethingSensitive: "{name}의 민감한 파일이 포함된 게시물" -enableHorizontalSwipe: "스와이프하여 탭 전환" -loading: "불러오는 중" -surrender: "그만두기" -gameRetry: "다시 시도" -notUsePleaseLeaveBlank: "사용하지 않는 경우 비워두세요." -useTotp: "일회용 비밀번호 사용" -useBackupCode: "백업 코드 사용" -launchApp: "앱 실행" -useNativeUIForVideoAudioPlayer: "브라우저 UI에서 미디어 재생" -keepOriginalFilename: "원본 파일 이름을 유지" -keepOriginalFilenameDescription: "이 설정을 끄면 업로드를 할 때 파일 이름이 자동으로 무작위 문자열로 바뀝니다." -noDescription: "설명문이 없습니다" -alwaysConfirmFollow: "팔로우일 때 항상 확인하기" -inquiry: "문의하기" -tryAgain: "다시 시도해 주세요." -confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인" -sensitiveMediaRevealConfirm: "민감한 미디어입니다. 표시할까요?" -createdLists: "만든 리스트" -createdAntennas: "만든 안테나" -fromX: "{x}에서" -genEmbedCode: "임베디드 코드 만들기" -noteOfThisUser: "이 유저의 노트 목록" -clipNoteLimitExceeded: "더 이상 이 클립에 노트를 추가 할 수 없습니다." -performance: "퍼포먼스" -modified: "변경 있음" -discard: "파기" -thereAreNChanges: "{n}건 변경이 있습니다." -signinWithPasskey: "패스키로 로그인" -unknownWebAuthnKey: "등록되지 않은 패스키입니다." -passkeyVerificationFailed: "패스키 검증을 실패했습니다." -passkeyVerificationSucceededButPasswordlessLoginDisabled: "입력된 패스키는 정상적이나, 비밀번호 없이 로그인 하는 기능이 비활성화 되어있습니다." -messageToFollower: "팔로워에게 보낼 메시지" -target: "대상" -testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. 실제 환경에서는 사용하지 마세요." -prohibitedWordsForNameOfUser: "금지 단어 (유저명)" -prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 유저명에 있는 경우, 일반 유저는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 유저는 제한 대상에서 제외됩니다." -yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다." -yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요." -thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해야 볼 수 있도록 설정되어 있습니다." -lockdown: "잠금" -pleaseSelectAccount: "계정을 선택해주세요." -availableRoles: "사용 가능한 역할" -acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했습니다." -federationSpecified: "이 서버는 화이트 리스트 제도로 운영 중 입니다. 정해진 리모트 서버가 아닌 경우 연합되지 않습니다." -federationDisabled: "이 서버는 연합을 하지 않고 있습니다. 리모트 서버 유저와 통신을 할 수 없습니다." -confirmOnReact: "리액션할 때 확인" -reactAreYouSure: "\" {emoji} \"로 리액션하시겠습니까?" -markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠습니까?" -unmarkAsSensitiveConfirm: "이 미디어의 민감한 미디어 지정을 해제하시겠습니까?" -preferences: "환경설정" -accessibility: "접근성" -preferencesProfile: "설정 프로필" -copyPreferenceId: "설정한 ID를 복사" -resetToDefaultValue: "기본값으로 되돌리기" -overrideByAccount: "계정으로 덮어쓰기" -untitled: "제목 없음" -noName: "이름이 없습니다." -skip: "건너뛰기" -restore: "복원" -syncBetweenDevices: "장치간 동기화" -preferenceSyncConflictTitle: "서버에 설정값이 존재합니다." -preferenceSyncConflictText: "동기화를 활성화 한 항목의 설정 값은 서버에 저장되지만, 해당 항목은 이미 서버에 설정 값이 저장되어져 있습니다. 어느 쪽의 설정 값을 덮어씌울까요?" -preferenceSyncConflictChoiceMerge: "병합" -preferenceSyncConflictChoiceServer: "서버 설정값" -preferenceSyncConflictChoiceDevice: "장치 설정값" -preferenceSyncConflictChoiceCancel: "동기화 취소" -paste: "붙여넣기" -emojiPalette: "이모지 팔레트" -postForm: "글 입력란" -textCount: "문자 수" -information: "정보" -chat: "채팅" -migrateOldSettings: "기존 설정 정보를 이전" -migrateOldSettings_description: "보통은 자동으로 이루어지지만, 어떤 이유로 인해 성공적으로 이전이 이루어지지 않는 경우 수동으로 이전을 실행할 수 있습니다. 현재 설정 정보는 덮어쓰게 됩니다." -compress: "압축" -right: "오른쪽" -bottom: "아래" -top: "위" -embed: "임베드" -settingsMigrating: "설정을 이전하는 중입니다. 잠시 기다려주십시오... (나중에 '환경설정 → 기타 → 기존 설정 정보를 이전'에서 수동으로 이전할 수도 있습니다)" -readonly: "읽기 전용" -goToDeck: "덱으로 돌아가기" -federationJobs: "연합 작업" -driveAboutTip: "드라이브는 이전에 업로드한 파일 목록을 표시해요.
\n노트에 첨부할 때 다시 사용하거나 나중에 게시할 파일을 미리 업로드할 수 있어요.
\n파일을 삭제하면, 지금까지 그 파일을 사용한 모든 장소(노트, 페이지, 아바타, 배너 등)에서도 보이지 않게 되므로 주의해 주세요. 폴더를 만들고 정리할 수도 있어요.
" -scrollToClose: "스크롤하여 닫기" -advice: "참고" -realtimeMode: "실시간 모드" -turnItOn: "켜기" -turnItOff: "끄기" -emojiMute: "이모티콘 뮤트" -emojiUnmute: "이모티콘 뮤트 해제" -muteX: "{x}를 뮤트" -unmuteX: "{x}의 뮤트를 해제" -abort: "중지" -tip: "팁과 유용한 정보" -redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시" -hideAllTips: "모든 '팁과 유용한 정보'를 비표시" -_chat: - noMessagesYet: "아직 메시지가 없습니다" - newMessage: "새로운 메시지" - individualChat: "개인 대화" - individualChat_description: "특정 유저와 일대일 채팅을 할 수 있습니다." - roomChat: "룸 채팅" - roomChat_description: "여러 명이 함께 채팅할 수 있습니다.\n또한, 개인 채팅을 허용하지 않은 유저와도 상대방이 수락하면 채팅을 할 수 있습니다." - createRoom: "룸을 생성" - inviteUserToChat: "유저를 초대하여 채팅을 시작하세요" - yourRooms: "생성한 룸" - joiningRooms: "참가 중인 룸" - invitations: "초대" - noInvitations: "초대장이 없습니다" - history: "이력" - noHistory: "기록이 없습니다" - noRooms: "룸이 없습니다" - inviteUser: "유저를 초대" - sentInvitations: "초대를 보내기" - join: "참여" - ignore: "무시" - leave: "룸을 떠나기" - members: "멤버" - searchMessages: "메시지 검색" - home: "홈" - send: "전송" - newline: "줄바꿈" - muteThisRoom: "이 룸을 뮤트" - deleteRoom: "룸을 삭제" - chatNotAvailableForThisAccountOrServer: "이 서버 또는 이 계정에서 채팅이 활성화되어 있지 않습니다." - chatIsReadOnlyForThisAccountOrServer: "이 서버 또는 이 계정에서 채팅은 읽기 전용입니다. 새로 쓰거나 채팅 룸을 만들거나 참가할 수 없습니다." - chatNotAvailableInOtherAccount: "상대방 계정에서 채팅 기능을 사용할 수 없는 상태입니다." - cannotChatWithTheUser: "이 유저와 채팅을 시작할 수 없습니다" - cannotChatWithTheUser_description: "채팅을 사용할 수 없는 상태이거나 상대방이 채팅을 열지 않은 상태입니다." - youAreNotAMemberOfThisRoomButInvited: "당신은 이 룸의 참가자가 아닙니다만 초대 신청을 받으셨습니다. 참가하려면 초대를 수락해주십시오." - doYouAcceptInvitation: "초대를 수락하시겠습니까?" - chatWithThisUser: "채팅하기" - thisUserAllowsChatOnlyFromFollowers: "이 유저는 팔로워만 채팅을 할 수 있습니다." - thisUserAllowsChatOnlyFromFollowing: "이 유저는 이 유저가 팔로우하는 유저만 채팅을 허용합니다." - thisUserAllowsChatOnlyFromMutualFollowing: "이 유저는 상호 팔로우하는 유저만 채팅을 허용합니다." - thisUserNotAllowedChatAnyone: "이 유저는 다른 사람의 채팅을 받지 않습니다." - chatAllowedUsers: "채팅을 허용한 상대" - chatAllowedUsers_note: "내가 채팅 메시지를 보낸 상대와는 이 설정과 상관없이 채팅이 가능합니다." - _chatAllowedUsers: - everyone: "누구나" - followers: "자신의 팔로워만" - following: "자신이 팔로우한 유저만" - mutual: "상호 팔로우한 유저만" - none: "아무도 허락하지 않기" -_emojiPalette: - palettes: "팔레트" - enableSyncBetweenDevicesForPalettes: "팔레트의 디바이스 간 동기화를 활성화" - paletteForMain: "메인으로 사용할 팔레트" - paletteForReaction: "리액션으로 사용할 팔레트" -_settings: - driveBanner: "드라이브 관리, 사용량 확인, 파일 업로드에 관한 설정을 합니다." - pluginBanner: "플러그인을 사용하면 클라이언트 기능을 확장할 수 있습니다. 플러그인 설치와 개별적인 설정을 합니다." - notificationsBanner: "서버에서 받는 알림의 종류 및 범위, 푸시 알림 설정을 합니다." - api: "API" - webhook: "Webhook" - serviceConnection: "서비스 연동" - serviceConnectionBanner: "외부 앱, 서비스와 연결하기 위한 액세스 토큰과 웹 훅 관리 설정을 합니다." - accountData: "계정 데이터" - accountDataBanner: "계정 데이터의 아카이브를 추출하기/가져오기 하여 관리할 수 있습니다." - muteAndBlockBanner: "숨길 컨텐츠의 설정과, 특정 유저의 리액션을 제한하는 설정을 관리합니다." - accessibilityBanner: "좀 더 쾌적하게 사용할 수 있도록 클라이언트의 시각 및 움직임에 관한 개인화 설정을 합니다." - privacyBanner: "컨텐츠, 계정의 발견 범위, 팔로우 승인제 등의 계정의 프라이버시에 관한 설정을 합니다." - securityBanner: "비밀번호, 로그인 방법, OTP, 패스 키 등의 계정의 보안에 관련된 설정을 합니다." - preferencesBanner: "취향에 알맞는 클라이언트의 전체적인 동작을 설정합니다." - appearanceBanner: "취향에 알맞는 클라이언트의 디스플레이, 표시 방법에 관한 설정을 합니다." - soundsBanner: "클라이언트에서 재생할 소리에 대한 설정을 합니다." - timelineAndNote: "타임라인과 노트" - makeEveryTextElementsSelectable: "모든 텍스트 요소를 선택할 수 있도록 함" - makeEveryTextElementsSelectable_description: "활성화 시, 일부 동작에서 유저의 접근성이 나빠질 수도 있습니다." - useStickyIcons: "아이콘이 스크롤을 따라가도록 하기" - enableHighQualityImagePlaceholders: "고화질 이미지의 플레이스홀더를 표시" - uiAnimations: "UI 애니메이션" - showNavbarSubButtons: "내비게이션 바에 보조 버튼 표시" - ifOn: "켜져 있을 때" - ifOff: "꺼져 있을 때" - enableSyncThemesBetweenDevices: "기기 간 설치한 테마 동기화" - enablePullToRefresh: "계속해서 갱신" - enablePullToRefresh_description: "마우스에서 휠을 누르면서 드래그해요." - realtimeMode_description: "서버에 접속하고 실시간으로 콘텐츠를 업데이트합니다. 데이터 사용량과 배터리의 소비가 증가할 수 있습니다." - contentsUpdateFrequency: "콘텐츠의 업데이트 빈도" - contentsUpdateFrequency_description: "높을수록 실시간으로 콘텐츠가 업데이트됩니다만, 성능이 저하되고 데이터 사용량과 배터리의 소비가 증가합니다." - contentsUpdateFrequency_description2: "실시간 모드가 켜져 있을 때는 이 설정과 상관없이 실시간으로 콘텐츠가 업데이트됩니다." - showUrlPreview: "URL 미리보기 표시" - _chat: - showSenderName: "발신자 이름 표시" - sendOnEnter: "엔터로 보내기" -_preferencesProfile: - profileName: "프로필 이름" - profileNameDescription: "이 디바이스를 식별할 이름을 설정해 주세요." - profileNameDescription2: "예: '메인PC', '스마트폰' 등" - manageProfiles: "프로파일 관리" -_preferencesBackup: - autoBackup: "자동 백업" - restoreFromBackup: "백업으로 복구" - noBackupsFoundTitle: "백업을 찾을 수 없습니다" - noBackupsFoundDescription: "자동으로 생성된 백업은 찾을 수 없었지만, 수동으로 백업 파일을 저장한 경우 해당 파일을 가져와 복원할 수 있습니다." - selectBackupToRestore: "복원할 백업을 선택하세요" - youNeedToNameYourProfileToEnableAutoBackup: "자동 백업을 활성화하려면 프로필 이름을 설정해야 합니다." - autoPreferencesBackupIsNotEnabledForThisDevice: "이 장치에서 설정 자동 백업이 활성화되어 있지 않습니다." - backupFound: "설정 백업이 발견되었습니다" -_accountSettings: - requireSigninToViewContents: "콘텐츠 열람을 위해 로그인을 필수로 설정하기" - requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다." - requireSigninToViewContentsDescription2: "URL 미리보기(OGP), 웹페이지에 삽입, 노트 인용을 지원하지 않는 서버에서 볼 수 없게 됩니다." - requireSigninToViewContentsDescription3: "리모트 서버에 연합된 콘텐츠에는 이러한 제한이 적용되지 않을 수 있습니다." - makeNotesFollowersOnlyBefore: "과거 노트는 팔로워만 볼 수 있도록 설정하기" - makeNotesFollowersOnlyBeforeDescription: "이 기능이 활성화되어 있는 동안, 설정된 날짜 및 시간보다 과거 또는 설정된 시간이 지난 노트는 팔로워만 볼 수 있게 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다." - makeNotesHiddenBefore: "과거 노트 비공개로 전환하기" - makeNotesHiddenBeforeDescription: "이 기능이 활성화되어 있는 동안 설정한 날짜 및 시간보다 과거 또는 설정한 시간이 지난 노트는 본인만 볼 수 있게(비공개로 전환) 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다." - mayNotEffectForFederatedNotes: "리모트 서버에 연합된 노트에는 효과가 없을 수도 있습니다." - mayNotEffectSomeSituations: "여기서 설정하는 제한은 모더레이션이나 리모트 서버에서 볼 때 등 일부 환경에서는 적용되지 않을 수도 있습니다." - notesHavePassedSpecifiedPeriod: "지정한 시간이 경과된 노트" - notesOlderThanSpecifiedDateAndTime: "지정된 날짜 및 시간 이전의 노트" -_abuseUserReport: - forward: "전달" - forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다." - resolve: "해결됨" - accept: "인용" - reject: "기각" - resolveTutorial: "적절한 신고 내용에 대응한 경우, \"인용\"을 선택하여 \"해결됨\"으로 기록합니다.\n적절하지 않은 신고를 받은 경우, \"기각\"을 선택하여 \"기각\"으로 기록합니다." -_delivery: - status: "전송 상태" - stop: "정지됨" - resume: "전송 다시 시작" - _type: - none: "배포 중" - manuallySuspended: "수동 정지 중" - goneSuspended: "서버 삭제를 이유로 정지 중" - autoSuspendedForNotResponding: "서버 응답 없음을 이유로 정지 중" - softwareSuspended: "전달 정지 중인 소프트웨어이므로 정지 중" -_bubbleGame: - howToPlay: "설명" - hold: "홀드" - _score: - score: "점수" - scoreYen: "번 돈" - highScore: "최고 점수" - maxChain: "최대 콤보 수" - yen: "{yen}엔" - estimatedQty: "{qty}개" - scoreSweets: "오니기리 {onigiriQtyWithUnit}" - _howToPlay: - section1: "위치를 조정하여 상자에 물건을 떨어뜨립니다." - section2: "같은 종류의 물건이 붙으면 다른 물건으로 바뀌면서 점수를 얻게 됩니다." - section3: "상자에서 물건이 넘치면 게임 오버입니다. 상자에서 물건이 넘치지 않도록 하면서 물건을 융합하여 높은 점수를 획득하세요!" -_announcement: - forExistingUsers: "기존 유저에게만 알림" - forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다." - needConfirmationToRead: "읽음으로 표시하기 전에 확인하기" - needConfirmationToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 확인 알림창을 띄웁니다. '모두 읽음'의 대상에서도 제외됩니다." - end: "공지에서 내리기" - tooManyActiveAnnouncementDescription: "공지사항이 너무 많을 경우, 유저 경험에 영향을 끼칠 가능성이 있습니다. 오래된 공지사항은 아카이브하시는 것을 권장드립니다." - readConfirmTitle: "읽음으로 표시합니까?" - readConfirmText: "〈{title}〉의 내용을 읽음으로 표시합니다." - shouldNotBeUsedToPresentPermanentInfo: "신규 유저의 이용 경험에 악영향을 끼칠 수 있으므로, 일시적인 알림 수단으로만 사용하고 고정된 정보에는 사용을 지양하는 것을 추천합니다." - dialogAnnouncementUxWarn: "다이얼로그 형태의 알림이 동시에 2개 이상 존재하는 경우, 유저 경험에 악영향을 끼칠 수 있으므로 신중히 결정하십시오." - silence: "조용히 알림" - silenceDescription: "활성화하면 공지사항에 대한 알림이 가지 않게 되며, 확인 버튼을 누를 필요가 없게 됩니다." _initialAccountSetting: accountCreated: "계정 생성이 완료되었습니다!" letsStartAccountSetup: "계정의 초기 설정을 진행합니다." @@ -1539,114 +1078,11 @@ _initialAccountSetting: pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다." initialAccountSettingCompleted: "초기 설정을 모두 마쳤습니다!" haveFun: "{name}와 함께 즐거운 시간 보내세요!" - youCanContinueTutorial: "이대로 {name}(Misskey)의 사용법에 대해 튜토리얼을 진행할 수도 있지만, 여기서 중단하고 바로 시작할 수도 있습니다." - startTutorial: "튜토리얼 시작" + ifYouNeedLearnMore: "{name}(Misskey)의 사용 방법에 대해 자세히 알아보려면 {link}를 참고해 주세요." skipAreYouSure: "초기 설정을 중단하시겠습니까?" laterAreYouSure: "초기 설정을 나중에 진행하시겠습니까?" -_initialTutorial: - launchTutorial: "튜토리얼 보기" - title: "튜토리얼" - wellDone: "잘 하셨습니다" - skipAreYouSure: "튜토리얼을 종료하시겠습니까?" - _landing: - title: "튜토리얼에 오신 걸 환영합니다" - description: "여기서는 미스키의 기본적인 사용법이나 기능을 확인할 수 있습니다." - _note: - title: "'노트'가 무엇인가요?" - description: "미스키에서는 게시물을 '노트'라고 합니다. 노트는 타임라인에 시간순으로 정렬되어 있고, 실시간으로 갱신됩니다." - reply: "답글을 달 수 있습니다. 답글에 답글을 달 수도 있고 글타래처럼 대화를 이어갈 수도 있습니다." - renote: "그 노트를 자기 타임라인에 가져와서 공유하는 것이 가능합니다. 글을 추가해서 인용하는 것도 가능합니다." - reaction: "리액션을 다는 것이 가능합니다. 다음 페이지에서 자세한 설명을 볼 수 있습니다." - menu: "노트의 상세 정보를 표시하거나, 링크를 복사하는 등의 다양한 조작을 할 수 있습니다." - _reaction: - title: "'리액션'이 무엇인가요?" - description: "노트에 '리액션'을 보낼 수 있습니다. '좋아요'만으로는 충분히 전해지지 않는 감정을, 이모지에 실어서 가볍게 보낼 수 있습니다." - letsTryReacting: "리액션은 노트의 '+' 버튼을 클릭하여 붙일 수 있습니다. 지금 표시되는 샘플 노트에 리액션을 달아 보세요!" - reactToContinue: "다음으로 진행하려면 리액션을 보내세요." - reactNotification: "누군가가 나의 노트에 리액션을 보내면 실시간으로 알림을 받게 됩니다." - reactDone: "'-' 버튼을 눌러서 리액션을 취소할 수 있습니다." - _timeline: - title: "타임라인에 대하여" - description1: "Misskey에는 종류에 따라 여러 가지의 타임라인으로 구성되어 있습니다.(서버에 따라서는 일부 타임라인을 사용할 수 없는 경우가 있습니다)" - home: "내가 팔로우 중인 계정의 노트를 볼 수 있습니다." - local: "이 서버에 있는 모든 유저의 게시물을 볼 수 있습니다." - social: "홈 타임라인과 로컬 타임라인의 게시물을 모두 볼 수 있습니다." - global: "연결되어 있는 모든 서버의 게시물을 볼 수 있습니다." - description2: "각각의 타임라인은 화면 상단에서 언제든지 변경할 수 있습니다." - description3: "이 외에도, '리스트 타임라인'이나 '채널 타임라인' 등이 있습니다. 자세한 사항은 {link}에서 확인하실 수 있습니다." - _postNote: - title: "노트 게시 설정" - description1: "Misskey에 노트를 게시할 때에는 다양한 옵션 설정이 가능합니다. 노트를 게시할 때 쓰이는 '글 입력란'은 이렇게 생겼습니다." - _visibility: - description: "노트를 볼 수 있는 사람을 제한할 수 있습니다." - public: "모든 유저에게 공개합니다." - home: "홈 타임라인에만 공개합니다. 팔로워, 프로필 화면, 리노트를 통해서 다른 유저가 볼 수 있습니다." - followers: "팔로워에게만 공개. 자기 자신을 제외하고는 리노트가 불가능하며, 팔로워 외에는 열람할 수 없습니다." - direct: "지정한 유저에게만 공개되며, 상대방에게 알림이 갑니다. 다이렉트 메시지(DM) 대용으로써 사용하실 수 있습니다." - doNotSendConfidencialOnDirect1: "민감한 정보를 보낼 때에는 주의하십시오." - doNotSendConfidencialOnDirect2: "서버 관리자는 기술적으로 게시물 내용을 열람할 수 있습니다. 신뢰할 수 없는 서버의 유저에게 다이렉트 메시지를 보내는 경우, 민감한 정보가 포함되어 있는 지 확인하십시오." - localOnly: "다른 서버에 게시물을 보내지 않습니다. 앞서 설정한 공개 범위와 상관 없이, 다른 서버의 유저는 이 게시물을 직접 열람할 수 없게 됩니다." - _cw: - title: "내용 가리기 (CW)" - description: "본문 대신에 '내용에 대한 주석'에 입력한 텍스트가 먼저 표시됩니다. '더 보기' 버튼을 누르면 본문이 표시됩니다." - _exampleNote: - cw: "배고픈 사람 주의" - note: "방금 초코도넛을 먹었어요 🍩😋" - useCases: "서버의 가이드라인에 따라 특정 주제를 다룰 때에 사용하거나, 스포일러 및 민감한 화제를 다룰 때에 자율적으로 사용하기도 합니다." - _howToMakeAttachmentsSensitive: - title: "첨부 파일을 열람주의로 설정하려면?" - description: "서버의 가이드라인에 따라 필요한 이미지, 또는 그대로 노출되기에 부적절한 미디어는 '열람 주의'를 설정해 주세요." - tryThisFile: "이 입력란에 첨부된 이미지를 열람 주의로 설정해 보세요!" - _exampleNote: - note: "낫또 뚜껑 뜯다가 실수했다…" - method: "첨부 파일을 열람 주의로 설정하려면, 해당 파일을 클릭하여 메뉴를 열고, '열람주의로 설정'을 클릭합니다." - sensitiveSucceeded: "파일을 첨부할 때에는 서버의 가이드라인에 따라 적절히 열람주의를 설정해 주시기 바랍니다." - doItToContinue: "이미지를 열람 주의로 설정하면 다음으로 넘어갈 수 있게 됩니다." - _done: - title: "튜토리얼이 끝났습니다! 🎉" - description: "여기에서 소개한 기능은 극히 일부에 지나지 않습니다. Misskey의 사용 방법을 더 자세히 알아보려면 {link}를 확인해 주세요!" -_timelineDescription: - home: "홈 타임라인에서는, 내가 팔로우한 계정의 게시물을 볼 수 있습니다." - local: "로컬 타임라인에서는, 이 서버의 모든 유저의 게시물을 볼 수 있습니다." - social: "소셜 타임라인에서는, 홈 타임라인과 로컬 타임라인의 게시물을 모두 볼 수 있습니다." - global: "글로벌 타임라인에서는, 여기와 연결된 다른 모든 서버의 게시물을 볼 수 있습니다." _serverRules: description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다." -_serverSettings: - iconUrl: "아이콘 URL" - appIconDescription: "{host}이 앱으로 표시될 때의 아이콘을 지정합니다." - appIconUsageExample: "예를 들어, PWA나 스마트폰 홈 화면에 북마크로 추가되었을 때 등" - appIconStyleRecommendation: "아이콘이 원형 또는 둥근 사각형으로 잘리는 경우가 있으므로, 가장자리 여백이 충분한 사진을 사용하는 것을 추천합니다." - appIconResolutionMustBe: "해상도는 반드시 {resolution} 이어야 합니다." - manifestJsonOverride: "manifest.json 오버라이드" - shortName: "약칭" - shortNameDescription: "서버의 정식 명칭이 긴 경우에, 대신에 표시할 수 있는 약칭이나 통칭." - fanoutTimelineDescription: "활성화하면 각종 타임라인을 가져올 때의 성능을 대폭 향상하며, 데이터베이스의 부하를 줄일 수 있습니다. 단, Redis의 메모리 사용량이 증가합니다. 서버의 메모리 용량이 작거나, 서비스가 불안정해지는 경우 비활성화할 수 있습니다." - fanoutTimelineDbFallback: "데이터베이스를 예비로 사용하기" - fanoutTimelineDbFallbackDescription: "활성화하면 타임라인의 캐시되어 있지 않은 부분에 대해 DB에 질의하여 정보를 가져옵니다. 비활성화하면 이를 실행하지 않음으로써 서버의 부하를 줄일 수 있지만, 타임라인에서 가져올 수 있는 게시물 범위가 한정됩니다." - reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다." - inquiryUrl: "문의처 URL" - inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다." - openRegistration: "회원 가입을 활성화 하기" - openRegistrationWarning: "회원 가입을 개방하는 것은 리스크가 따릅니다. 서버를 항상 감시할 수 있고, 문제가 발생했을 때 바로 대응할 수 있는 상태에서만 활성화 하는 것을 권장합니다." - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다." - deliverSuspendedSoftware: "전달 정지 중인 소프트웨어" - deliverSuspendedSoftwareDescription: "취약성 등의 이유로 서버의 소프트웨어 이름 및 버전 범위를 지정하여 전달을 정지할 수 있어요. 이 버전 정보는 서버가 제공한 것이며 신뢰성은 보장되지 않아요. 버전 지정에는 semver의 범위 지정을 사용할 수 있지만, >= 2024.3.1로 지정하면 2024.3.1-custom.0과 같은 custom.0과 같은 custom 버전이 포함되지 않기 때문에 >= 2024.3.1-0과 같이 prerelease를 지정하는 것이 좋아요." - singleUserMode: "1인 모드" - singleUserMode_description: "이 서버의 이용자가 자신 뿐인 경우, 이 모드를 활성화하면 동작이 최적화됩니다." - signToActivityPubGet: "GET 요청에 사인" - signToActivityPubGet_description: "보통의 경우 활성화해 주십시오. 연합의 통신에 관한 문제가 있는 경우, 비활성화하면 개선되는 경우도 있습니다만, 서버에 따라서는 통신이 불가능해지는 경우도 있습니다." - proxyRemoteFiles: "리모트 파일 프록시" - proxyRemoteFiles_description: "활성화하면 리모트 파일을 프록시로 제공합니다. 이미지의 섬네일 생성이나 유저의 개인정보 보호에 도움을 줍니다." - allowExternalApRedirect: "ActivityPub 경유 조회에 리디렉션 허가" - allowExternalApRedirect_description: "활성화하면 다른 서버가 이 서버를 통해 제3자의 콘텐츠를 조회할 수 있습니다만, 콘텐츠의 사칭 문제가 생길 수 있습니다." - userGeneratedContentsVisibilityForVisitor: "비이용자에 대한 유저 작성 콘텐츠의 공개 범위" - userGeneratedContentsVisibilityForVisitor_description: "조정을 하기 힘든 부적절한 리모트 콘텐츠 등이 자신의 서버 경유로 의도치 않게 인터넷에 공개되는 문제의 방지 등에 도움을 줍니다." - userGeneratedContentsVisibilityForVisitor_description2: "서버에서 받은 리모트 콘텐츠를 포함해 서버 내의 모든 콘텐츠를 무조건 인터넷에 공개하는 것에는 위험이 따릅니다. 특히, 분산형 특성에 대해 모르는 열람자에게는 리모트 콘텐츠여도 서버 내에서 작성된 콘텐츠라고 잘못 인식할 수 있기에 주의가 필요합니다." - _userGeneratedContentsVisibilityForVisitor: - all: "모두 공개" - localOnly: "로컬 콘텐츠만 공개하고 리모트 콘텐츠는 비공개" - none: "모두 비공개" _accountMigration: moveFrom: "다른 계정에서 이 계정으로 이사" moveFromSub: "다른 계정에 대한 별칭을 생성" @@ -1666,271 +1102,256 @@ _achievements: earnedAt: "달성 일시" _types: _notes1: - title: "미스키 계정 만들었어요" - description: "첫 노트를 게시했다" - flavor: "Misskey에 어서 오세요!" + title: "미스키 시작했는데요" + description: "첫 노트를 작성했습니다" + flavor: "Misskey에 오신 것을 환영합니다!" _notes10: - title: "몇 가지 노트" - description: "10개의 노트를 게시했다" + title: "노트 조금" + description: "10개의 노트를 작성했습니다" _notes100: - title: "많은 노트" - description: "100개의 노트를 게시했다" + title: "노트 많이" + description: "100개의 노트를 작성했습니다" _notes500: - title: "노트 범벅" - description: "500개의 노트를 게시했다" + title: "노트로 뒤덮여버렸어" + description: "500개의 노트를 작성했습니다" _notes1000: - title: "노트가 산더미" - description: "1,000개의 노트를 게시했다" + title: "노트만 산더미" + description: "1,000개의 노트를 작성했습니다" _notes5000: - title: "솟아나는 노트" - description: "5,000개의 노트를 게시했다" + title: "노트가 어디서 솟아?" + description: "5,000개의 노트를 작성했습니다" _notes10000: title: "슈퍼 노트" - description: "10,000개의 노트를 게시했다" + description: "10,000개의 노트를 작성했습니다" _notes20000: - title: "노트가 더 필요해요" - description: "20,000개의 노트를 게시했다" + title: "노트 더 없어?" + description: "20,000개의 노트를 작성했습니다" _notes30000: title: "노트노트노트" - description: "30,000개의 노트를 게시했다" + description: "30,000개의 노트를 작성했습니다" _notes40000: title: "노트 공장" - description: "40,000개의 노트를 게시했다" + description: "40,000개의 노트를 작성했습니다" _notes50000: title: "노트 행성" - description: "50,000개의 노트를 게시했다" + description: "50,000개의 노트를 작성했습니다" _notes60000: title: "노트 퀘이사" - description: "60,000개의 노트를 게시했다" + description: "60,000개의 노트를 작성했습니다" _notes70000: title: "노트 블랙홀" - description: "70,000개의 노트를 게시했다" + description: "70,000개의 노트를 작성했습니다" _notes80000: title: "노트 은하" - description: "80,000개의 노트를 게시했다" + description: "80,000개의 노트를 작성했습니다" _notes90000: title: "노트 우주" - description: "90,000개의 노트를 게시했다" + description: "90,000개의 노트를 작성했습니다" _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" - description: "100,000개의 노트를 게시했다" - flavor: "이렇게나 쓸 게 있어요?" + description: "100,000개의 노트를 작성했습니다" + flavor: "이만큼 쓸 일도 없겠지만... 다른 할 일이 있진 않으신가요?" _login3: - title: "초보자 I" - description: "총 로그인한 날이 3일" - flavor: "오늘부터 여러분도 미스키스트랍니다" + title: "비기너 I" + description: "총 3일간 로그인했습니다" + flavor: "오늘부터 여러분도 미스키스트에요!" _login7: - title: "초보자 II" - description: "총 로그인한 날이 7일" + title: "비기너 II" + description: "총 7일간 로그인했습니다" flavor: "슬슬 익숙해지셨나요?" _login15: - title: "초보자 III" - description: "총 로그인한 날이 15일" + title: "비기너 III" + description: "총 15일간 로그인했습니다" _login30: title: "미스키스트 I" - description: "총 로그인한 날이 30일" + description: "총 30일간 로그인했습니다" _login60: title: "미스키스트 II" - description: "총 로그인한 날이 60일" + description: "총 60일간 로그인했습니다" _login100: title: "미스키스트 III" - description: "총 로그인한 날이 100일" + description: "총 100일간 로그인했습니다" flavor: "그 유저, 미스키스트이다" _login200: title: "단골 I" - description: "총 로그인한 날이 200일" + description: "총 200일간 로그인했습니다" _login300: title: "단골 II" - description: "총 로그인한 날이 300일" + description: "총 300일간 로그인했습니다" _login400: title: "단골 III" - description: "총 로그인한 날이 400일" + description: "총 400일간 로그인했습니다" _login500: title: "베테랑 I" - description: "총 로그인한 날이 500일" + description: "총 500일간 로그인했습니다" flavor: "제군, 나는 노트가 좋다" _login600: title: "베테랑 II" - description: "총 로그인한 날이 600일" + description: "총 600일간 로그인했습니다" _login700: title: "베테랑 III" - description: "총 로그인한 날이 700일" + description: "총 700일간 로그인했습니다" _login800: title: "노트 마스터 I" - description: "총 로그인한 날이 800일" + description: "총 800일간 로그인했습니다" _login900: title: "노트 마스터 II" - description: "총 로그인한 날이 900일" + description: "총 900일간 로그인했습니다" _login1000: title: "노트 마스터 III" - description: "총 로그인한 날이 1,000일" + description: "총 1,000일간 로그인했습니다" flavor: "Misskey를 사용해 주셔서 감사합니다!" _noteClipped1: title: "클립할 수밖에 없었어" - description: "처음으로 노트를 클립했다" + description: "처음으로 노트를 클립했습니다" _noteFavorited1: title: "별을 바라보는 자" - description: "처음으로 노트를 즐겨찾기했다" + description: "처음으로 노트를 즐겨찾기했습니다" _myNoteFavorited1: title: "별을 원하는 자" - description: "다른 사람이 당신의 노트를 즐겨찾기했다" + description: "다른 사람이 당신의 노트를 즐겨찾기했습니다" _profileFilled: title: "준비 완료" - description: "프로필 설정을 완료했다" + description: "프로필 설정을 완료했습니다" _markedAsCat: title: "나는 고양이다냥!" - description: "계정을 고양이로 설정했다냥" + description: "계정을 고양이로 설정했습니다냥" flavor: "냐냐냐냐냐냐아아아아앙!" _following1: title: "첫 팔로우" - description: "유저를 처음으로 팔로우했다" + description: "사용자를 처음으로 팔로우했습니다" _following10: title: "팔로우, 팔로우" - description: "10명의 유저를 팔로우했다" + description: "10명의 사용자를 팔로우했습니다" _following50: title: "친구 잔뜩" - description: "50명의 유저를 팔로우했다" + description: "50명의 사용자를 팔로우했습니다" _following100: title: "주소록 한 권으론 부족해" - description: "100명의 유저를 팔로우했다" + description: "100명의 사용자를 팔로우했습니다" _following300: title: "친구가 넘쳐나" - description: "300명의 유저를 팔로우했다" + description: "300명의 사용자를 팔로우했습니다" _followers1: title: "첫 팔로워" - description: "유저가 처음으로 팔로잉했다" + description: "사용자가 처음으로 팔로잉했습니다" _followers10: title: "팔로우 미!" - description: "10명의 유저가 팔로우했다" + description: "10명의 사용자가 팔로우했습니다" _followers50: title: "이곳저곳" - description: "50명의 유저가 팔로우했다" + description: "50명의 사용자가 팔로우했습니다" _followers100: title: "인기왕" - description: "100명의 유저가 팔로우했다" + description: "100명의 사용자가 팔로우했습니다" _followers300: title: "줄 좀 서봐요" - description: "100명의 유저가 팔로우했다" + description: "100명의 사용자가 팔로우했습니다" _followers500: title: "기지국" - description: "500명의 유저가 팔로우했다" + description: "500명의 사용자가 팔로우했습니다" _followers1000: title: "유명인사" - description: "1,000명의 유저가 팔로우했다" + description: "1,000명의 사용자가 팔로우했습니다" _collectAchievements30: title: "도전 과제 콜렉터" - description: "30개의 도전과제를 획득했다" + description: "30개의 도전과제를 획득했습니다" _viewAchievements3min: title: "저 도전과제 좋아해요" - description: "도전 과제 목록을 3분 이상 쳐다봤다" + description: "도전 과제 목록을 3분 이상 쳐다봤습니다" _iLoveMisskey: title: "I Love Misskey" - description: "\"I ❤ #Misskey\"를 게시했다" - flavor: "Misskey를 이용해 주셔서 감사합니다! ― 개발 팀" + description: "\"I ❤ #Misskey\"를 포스트했습니다" + flavor: "Misskey를 이용해주셔서 감사합니다! - 개발팀 일동" _foundTreasure: title: "보물찾기" - description: "숨겨진 보물을 발견했다" + description: "숨겨진 보물을 발견했습니다" _client30min: - title: "잠시 쉬어요" - description: "클라이언트를 시작하고 30분이 경과했다" + title: "잠깐 쉬어" + description: "클라이언트를 시작하고 30분이 경과하였습니다" _client60min: title: "No \"Miss\" in Misskey" - description: "클라이언트를 시작하고 60분이 경과했다" + description: "클라이언트를 시작하고 60분이 경과하였습니다" _noteDeletedWithin1min: title: "있었는데요 없었습니다" - description: "노트를 게시한 후 1분 이내에 삭제했다" + description: "노트를 포스트한 후 1분 이내에 삭제했습니다" _postedAtLateNight: title: "올빼미" - description: "한밤중에 노트를 게시했다" + description: "한밤중에 노트를 포스트했습니다" flavor: "잠 좀 자세요. 걱정돼요." _postedAt0min0sec: title: "정각" - description: "0분 0초 정각에 노트를 게시했다" + description: "0분 0초 정각에 노트를 작성했습니다" flavor: "째깍 째깍 째깍 땡!" _selfQuote: title: "혼잣말" - description: "자기 노트를 인용했다" + description: "자기 노트를 인용했습니다" _htl20npm: title: "타임라인 폭주 중" - description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었다" + description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었습니다" _viewInstanceChart: title: "애널리스트" - description: "서버의 차트를 열었다" + description: "서버의 차트를 열었습니다" _outputHelloWorldOnScratchpad: title: "Hello, world!" - description: "스크래치패드에서 hello world를 출력했다" + description: "스크래치패드에서 hello world를 출력했습니다" _open3windows: title: "멀티 윈도우" - description: "3개 이상의 창을 열었다" + description: "3개 이상의 창을 열었습니다" _driveFolderCircularReference: title: "순환 참조" - description: "드라이브 폴더에 스스로를 넣게 했다" + description: "드라이브 폴더를 자신을 가리키도록 만드려 시도했습니다" _reactWithoutRead: title: "읽고 답하긴 하시는 건가요?" - description: "100자가 넘는 노트를 게시한 지 3초 안에 리액션했다" + description: "100자가 넘는 노트가 작성되고 3초 안에 반응했습니다" _clickedClickHere: title: "여길 눌러보세요" - description: "여기를 눌렀다" + description: "여길을 눌러봤습니다" _justPlainLucky: title: "그냥 운이 좋았어" - description: "매 10초마다 0.01%의 확률로 달성된다" + description: "매 10초마다 0.01%의 확률로 달성됩니다" _setNameToSyuilo: title: "신 콤플렉스" - description: "이름을 syuilo로 설정했다" + description: "이름을 syuilo로 설정했습니다" _passedSinceAccountCreated1: title: "1주년" - description: "계정을 생성하고 1년이 지났다" + description: "계정을 생성하고 1년이 지났습니다" _passedSinceAccountCreated2: title: "2주년" - description: "계정을 생성하고 2년이 지났다" + description: "계정을 생성하고 2년이 지났습니다" _passedSinceAccountCreated3: title: "3주년" - description: "계정을 생성하고 3년이 지났다" + description: "계정을 생성하고 3년이 지났습니다" _loggedInOnBirthday: title: "생일 축하합니다!" - description: "생일에 로그인했다" + description: "생일에 로그인했습니다" _loggedInOnNewYearsDay: title: "새해 복 많이 받으세요" - description: "새해 첫 날에 로그인했다" + description: "새해 첫 날에 로그인했습니다" flavor: "올해에도 저희 서버에 관심을 가져 주셔서 감사합니다" _cookieClicked: title: "쿠키를 클릭하는 게임" - description: "쿠키를 클릭했다" + description: "쿠키를 클릭했습니다" flavor: "소프트웨어 착각하지 않으셨나요?" _brainDiver: title: "Brain Diver" - description: "Brain Diver로의 링크를 첨부했다" + description: "Brain Diver로의 링크를 첨부했습니다" flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "테스트 과잉" - description: "매우 짧은 시간 안에 알림 테스트를 여러 번 수행했다" - _tutorialCompleted: - title: "Misskey 입문자 과정 수료증" - description: "튜토리얼을 완료했다" - _bubbleGameExplodingHead: - title: "🤯" - description: "버블 게임에서 가장 큰 물건을 내놓았다" - _bubbleGameDoubleExplodingHead: - title: "더블 🤯" - description: "버블게임에서 가장 큰 물건 2개를 동시에 내놓았다" - flavor: "이 정도만 도시락통에 🤯 🤯 조금만 더" _role: new: "새 역할 생성" edit: "역할 수정" name: "역할 이름" description: "역할 설명" permission: "역할 권한" - descriptionOfPermission: "조정자는 기본적인 조정 작업을 진행할 수 있습니다.\n관리자는 서버의 모든 설정을 변경할 수 있습니다." + descriptionOfPermission: "모더레이터는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n관리자는 서버의 모든 설정을 변경할 수 있습니다." assignTarget: "할당 대상" - descriptionOfAssignTarget: "수동을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n조건부를 선택하면 조건을 설정해 일치하는 유저를 자동으로 포함되게 할 수 있습니다." + descriptionOfAssignTarget: "수동을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n조건부를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다." manual: "수동" - manualRoles: "수동 역할" conditional: "조건부" - conditionalRoles: "조건부 역할" condition: "조건" isConditionalRole: "조건부 역할입니다." isPublic: "역할 공개" - descriptionOfIsPublic: "역할에 할당된 유저를 누구나 볼 수 있습니다. 또한 유저 프로필에 이 역할이 표시됩니다." + descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다." options: "옵션" policies: "정책" baseRole: "기본 역할" @@ -1943,10 +1364,8 @@ _role: descriptionOfIsExplorable: "활성화하면 역할 타임라인을 공개합니다. 비활성화 시 타임라인이 공개되지 않습니다." displayOrder: "표시 순서" descriptionOfDisplayOrder: "값이 클 수록 UI에서 먼저 표시됩니다." - preserveAssignmentOnMoveAccount: "이전 대상 계정에도 할당 상태 전달" - preserveAssignmentOnMoveAccount_description: "켜면 이 역할이 부여된 계정이 이전될 때 마이그레이션 대상 계정에도 이 역할이 승계됩니다." canEditMembersByModerator: "모더레이터의 역할 수정 허용" - descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 유저를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다." + descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다." priority: "우선순위" _priority: low: "낮음" @@ -1956,62 +1375,38 @@ _role: gtlAvailable: "글로벌 타임라인 보이기" ltlAvailable: "로컬 타임라인 보이기" canPublicNote: "공개 노트 허용" - mentionMax: "노트에 넣을 수 있는 멘션 수" canInvite: "서버 초대 코드 발행" - inviteLimit: "초대 한도" - inviteLimitCycle: "초대 발급 간격" - inviteExpirationTime: "초대 만료 기간" canManageCustomEmojis: "커스텀 이모지 관리" - canManageAvatarDecorations: "아바타 꾸미기 관리" driveCapacity: "드라이브 용량" - maxFileSize: "업로드 가능한 최대 파일 크기" alwaysMarkNsfw: "파일을 항상 NSFW로 지정" - canUpdateBioMedia: "아바타 및 배너 이미지 변경 허용" pinMax: "고정할 수 있는 노트 수" - antennaMax: "만들 수 있는 안테나 수" + antennaMax: "최대 안테나 생성 허용 수" wordMuteMax: "단어 뮤트할 수 있는 문자 수" - webhookMax: "만들 수 있는 Webhook 수" - clipMax: "만들 수 있는 클립 수" - noteEachClipsMax: "클립에 넣을 수 있는 노트 수" - userListMax: "만들 수 있는 유저 리스트 수" - userEachUserListsMax: "유저 리스트에 넣을 수 있는 유저 수" + webhookMax: "생성할 수 있는 웹훅 수" + clipMax: "생성할 수 있는 클립 수" + noteEachClipsMax: "각 클립에 추가할 수 있는 노트 수" + userListMax: "생성할 수 있는 유저 리스트 수" + userEachUserListsMax: "유저 리스트당 최대 사용자 수" rateLimitFactor: "요청 빈도 제한" descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다." canHideAds: "광고 숨기기" canSearchNotes: "노트 검색 이용 가능 여부" - canUseTranslator: "번역 기능의 사용" - avatarDecorationLimit: "아바타 장식의 최대 붙임 개수" - canImportAntennas: "안테나 가져오기 허용" - canImportBlocking: "차단 목록 가져오기 허용" - canImportFollowing: "팔로우 가져오기 허용" - canImportMuting: "뮤트 목록 가져오기 허용" - canImportUserLists: "리스트 목록 가져오기 허용" - chatAvailability: "채팅을 허락" - uploadableFileTypes: "업로드 가능한 파일 유형" - uploadableFileTypes_caption: "MIME 유형을 " - uploadableFileTypes_caption2: "파일에 따라서는 유형을 검사하지 못하는 경우가 있습니다. 그러한 파일을 허가하는 경우에는 {x}를 지정으로 추가해주십시오." _condition: - roleAssignedTo: "수동 역할에 이미 할당됨" - isLocal: "로컬 유저" - isRemote: "리모트 유저" - isCat: "고양이 유저" - isBot: "봇 유저" - isSuspended: "정지된 유저" - isLocked: "잠금 계정 유저" - isExplorable: "‘계정을 쉽게 발견하도록 하기’를 활성화한 유저" + isLocal: "로컬 사용자" + isRemote: "리모트 사용자" createdLessThan: "가입한 지 다음 일수 이내인 유저" createdMoreThan: "가입한 지 다음 일수 이상인 유저" followersLessThanOrEq: "팔로워 수가 다음 이하인 유저" - followersMoreThanOrEq: "팔로워 수가 다음보다 많은 유저" + followersMoreThanOrEq: "팔로워 수가 다음 이상인 유저" followingLessThanOrEq: "팔로잉 수가 다음 이하인 유저" - followingMoreThanOrEq: "팔로잉 수가 다음보다 많은 유저" + followingMoreThanOrEq: "팔로잉 수가 다음 이상인 유저" notesLessThanOrEq: "노트 수가 다음 이하인 유저" - notesMoreThanOrEq: "노트 수가 다음보다 많은 유저" + notesMoreThanOrEq: "노트 수가 다음 이상인 유저" and: "다음을 모두 만족" or: "다음을 하나라도 만족" not: "다음을 만족하지 않음" _sensitiveMediaDetection: - description: "기계 학습으로 민감한 미디어를 알아서 찾아내어 조정에 참고하도록 합니다. 서버가 부하를 다소 받습니다." + description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다." sensitivity: "탐지 민감도" sensitivityDescription: "민감도가 낮을수록 안전한 미디어가 잘못 탐지될 확률이 줄어들며, 높을수록 민감한 미디어가 탐지되지 않을 확률이 줄어듭니다." setSensitiveFlagAutomatically: "자동으로 NSFW로 설정하기" @@ -2024,7 +1419,6 @@ _emailUnavailable: disposable: "임시 이메일 주소는 사용할 수 없습니다" mx: "메일 서버가 올바르지 않습니다" smtp: "메일 서버가 응답하지 않습니다" - banned: "이 메일 주소는 사용할 수 없습니다" _ffVisibility: public: "공개" followers: "팔로워에게만 공개" @@ -2044,11 +1438,6 @@ _ad: back: "뒤로" reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기" hide: "보이지 않음" - timezoneinfo: "요일은 서버의 표준 시간대에 따라 결정됩니다." - adsSettings: "광고 표시 설정" - notesPerOneAd: "실시간으로 갱신되는 타임라인에서 광고를 노출시키는 간격 (노트 당)" - setZeroToDisable: "0으로 지정하면 실시간 타임라인에서의 광고를 비활성화합니다" - adsTooClose: "광고의 표시 간격이 매우 작아, 유저 경험에 부정적인 영향을 미칠 수 있습니다." _forgotPassword: enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다." ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오." @@ -2067,8 +1456,6 @@ _plugin: install: "플러그인 설치" installWarn: "신뢰할 수 없는 플러그인은 설치하지 않는 것이 좋습니다." manage: "플러그인 관리" - viewSource: "소스 보기" - viewLog: "로그 보기" _preferencesBackups: list: "생성한 백업" saveNew: "새 백업 만들기" @@ -2079,12 +1466,12 @@ _preferencesBackups: cannotSave: "저장하지 못했습니다" nameAlreadyExists: "\"{name}\" 백업이 이미 존재합니다. 다른 이름을 설정하여 주십시오." applyConfirm: "\"{name}\" 백업을 현재 기기에 적용하시겠습니까? 현재 설정은 덮어 씌워집니다." - saveConfirm: "{name} 백업을 덮어쓰시겠습니까?" - deleteConfirm: "{name} 백업을 삭제하시겠습니까?" - renameConfirm: "‘{old}’ 백업을 ‘{new}’ 백업으로 바꾸시겠습니까?" + saveConfirm: "{name} 을 덮어쓰시겠습니까?" + deleteConfirm: "{name} 을(를) 삭제하시겠습니까?" + renameConfirm: "\"{old}\" 백업을 \"{new}\"(으)로 바꾸시겠습니까?" noBackups: "저장된 백업이 없습니다. \"새 백업 만들기\"를 눌러 현재 클라이언트 설정을 서버에 백업할 수 있습니다." - createdAt: "만든 날짜: {date} {time}" - updatedAt: "고친 날짜: {date} {time}" + createdAt: "생성 날짜: {date} {time}" + updatedAt: "갱신 날짜: {date} {time}" cannotLoad: "가져오기에 실패했습니다" invalidFile: "파일 형식이 올바르지 않습니다." _registry: @@ -2094,21 +1481,14 @@ _registry: domain: "도메인" createKey: "키 생성" _aboutMisskey: - about: "Misskey는 syuilo가 2014년부터 개발한 오픈소스 소프트웨어입니다." + about: "Misskey는 syuilo에 의해서 2014년부터 개발되어 온 오픈소스 소프트웨어 입니다." contributors: "주요 기여자" allContributors: "모든 기여자" source: "소스 코드" - original: "원본" - thisIsModifiedVersion: "{name}에서는 원본 미스키를 수정한 버전을 사용하고 있습니다." translation: "Misskey를 번역하기" donate: "Misskey에 기부하기" morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰" patrons: "후원자" - projectMembers: "프로젝트 구성원" -_displayOfSensitiveMedia: - respect: "민감한 콘텐츠로 표시된 미디어 숨기기" - ignore: "민감한 콘텐츠로 표시된 미디어 보이기" - force: "미디어 항상 숨기기" _instanceTicker: none: "보이지 않음" remote: "리모트 유저에게만 보이기" @@ -2129,7 +1509,6 @@ _channel: notesCount: "{n}노트" nameAndDescription: "이름과 설명" nameOnly: "이름만" - allowRenoteToExternal: "채널 외부로의 리노트와 인용 리노트를 허가" _menuDisplay: sideFull: "가로" sideIcon: "가로(아이콘)" @@ -2139,13 +1518,18 @@ _wordMute: muteWords: "뮤트할 단어" muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다." muteWordsDescription2: "정규 표현식을 사용하려면 키워드를 빗금표(/)로 감싸 주세요." + softDescription: "지정한 조건의 노트를 타임라인에서 숨깁니다." + hardDescription: "지정한 조건의 노트를 타임라인에 추가하지 않습니다. 타임라인에 추가되지 않은 노트는 조건을 변경해도 표시되지 않습니다." + soft: "보통" + hard: "보다 높은 수준" + mutedNotes: "뮤트된 노트" _instanceMute: instanceMuteDescription: "뮤트한 서버에서 오는 답글을 포함한 모든 노트와 Renote를 뮤트합니다." instanceMuteDescription2: "한 줄에 하나씩 입력해 주세요" title: "지정한 서버의 노트를 숨깁니다." heading: "뮤트할 서버" _theme: - explore: "테마 둘러보기" + explore: "테마 찾아보기" install: "테마 설치" manage: "테마 관리" code: "테마 코드" @@ -2153,7 +1537,6 @@ _theme: installed: "{name} 테마가 설치되었습니다" installedThemes: "설치된 테마" builtinThemes: "표준 테마" - instanceTheme: "서버 테마" alreadyInstalled: "이미 설치된 테마입니다" invalid: "테마 형식이 올바르지 않습니다" make: "테마 만들기" @@ -2186,6 +1569,7 @@ _theme: header: "헤더" navBg: "사이드바 배경" navFg: "사이드바 텍스트" + navHoverFg: "사이드바 텍스트 (호버)" navActive: "사이드바 텍스트 (활성)" navIndicator: "사이드바 인디케이터" link: "링크" @@ -2202,28 +1586,30 @@ _theme: infoFg: "정보창 텍스트" infoWarnBg: "경고창 배경" infoWarnFg: "경고창 텍스트" + cwBg: "CW 버튼 배경" + cwFg: "CW 버튼 텍스트" + cwHoverBg: "CW 버튼 배경 (호버)" toastBg: "알림창 배경" toastFg: "알림창 텍스트" buttonBg: "버튼 배경" buttonHoverBg: "버튼 배경 (호버)" inputBorder: "입력 필드 테두리" + listItemHoverBg: "리스트 항목 배경 (호버)" + driveFolderBg: "드라이브 폴더 배경" + wallpaperOverlay: "배경화면 오버레이" badge: "배지" - messageBg: "대화 배경" + messageBg: "채팅 배경" + accentDarken: "강조 색상 (어두움)" + accentLighten: "강조 색상 (밝음)" fgHighlighted: "강조된 텍스트" _sfx: note: "새 노트" noteMy: "내 노트" notification: "알림" - reaction: "리액션 선택" - chatMessage: "채팅 메시지" -_soundSettings: - driveFile: "드라이브에 있는 오디오를 사용" - driveFileWarn: "드라이브에 있는 파일을 선택하세요." - driveFileTypeWarn: "이 파이" - driveFileTypeWarnDescription: "오디오 파일을 선택하세요." - driveFileDurationWarn: "오디오가 너무 깁니다" - driveFileDurationWarnDescription: "긴 오디오로 설정할 경우 미스키 사용에 지장이 갈 수도 있습니다. 그래도 괜찮습니까?" - driveFileError: "오디오를 불러올 수 없습니다. 설정을 바꿔주세요." + chat: "대화" + chatBg: "대화 (백그라운드)" + antenna: "안테나 수신" + channel: "채널 알림" _ago: future: "미래" justNow: "방금 전" @@ -2235,56 +1621,54 @@ _ago: monthsAgo: "{n}개월 전" yearsAgo: "{n}년 전" invalid: "없음" -_timeIn: - seconds: "{n}초 후" - minutes: "{n}분 후" - hours: "{n}시간 후" - days: "{n}일 후" - weeks: "{n}주 후" - months: "{n}개월 후" - years: "{n}년 후" _time: second: "초" minute: "분" hour: "시간" day: "일" +_timelineTutorial: + title: "Misskey의 사용 방법" + step1_1: "이것은 '타임라인'입니다. {name}에 게시된 '노트'가 시간 순서대로 표시됩니다." + step1_2: "타임라인은 몇 가지 종류로 나뉩니다. 그 중에 '홈 타임라인'은 내가 팔로우한 사람의 노트가 표시되며, '로컬 타임라인'에는 {name} 의 모든 노트가 표시됩니다." + step2_1: "그럼 시험삼아 노트를 작성해 봅시다. 화면에 있는 연필 버튼을 눌러 보세요." + step2_2: "첫 노트이니까 자기소개, 혹은 가볍게 \"안녕 {name}\"라고 올려 보는 건 어떨까요?" + step3_1: "노트 작성을 끝내셨나요?" + step3_2: "당신의 노트가 타임라인에 표시되어 있다면 성공입니다." + step4_1: "노트에는 '리액션'을 붙일 수 있습니다." + step4_2: "리액션을 붙이려면, 노트의 \"+\" 버튼을 클릭하고 원하는 이모지를 선택합니다." _2fa: alreadyRegistered: "이미 설정이 완료되었습니다." registerTOTP: "인증 앱 설정 시작" + passwordToTOTP: "비밀번호를 입력하세요." step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다." step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다." - step2Uri: "데스크톱 앱을 사용하려면 다음 URI를 입력하십시오" + step2Click: "QR 코드를 클릭하면 기기에 설치된 인증 앱에 등록할 수 있습니다." + step2Url: "데스크톱 앱에서는 다음 URL을 입력하세요:" step3Title: "인증 코드 입력" step3: "앱에 표시된 토큰을 입력하시면 완료됩니다." - setupCompleted: "설정 완료했습니다" step4: "다음 로그인부터는 토큰을 입력해야 합니다." securityKeyNotSupported: "이 브라우저는 보안 키를 지원하지 않습니다." registerTOTPBeforeKey: "보안 키 또는 패스키를 등록하려면 인증 앱을 등록하십시오." securityKeyInfo: "FIDO2를 지원하는 하드웨어 보안 키 혹은 디바이스의 지문인식이나 화면잠금 PIN을 이용해서 로그인하도록 설정할 수 있습니다." + chromePasskeyNotSupported: "현재 Chrome의 패스키는 지원되지 않습니다." registerSecurityKey: "보안 키 또는 패스키 등록" securityKeyName: "키 이름 입력" tapSecurityKey: "브라우저의 지시에 따라 보안 키 또는 패스키를 등록하여 주십시오" removeKey: "보안 키를 삭제" - removeKeyConfirm: "{name} 앱을 삭제하시겠습니까?" + removeKeyConfirm: "{name} 을(를) 삭제하시겠습니까?" whyTOTPOnlyRenew: "보안 키가 등록되어 있는 경우 인증 앱을 해제할 수 없습니다." renewTOTP: "인증 앱 재설정" renewTOTPConfirm: "기존에 등록되어 있던 인증 키는 사용하지 못하게 됩니다." renewTOTPOk: "재설정" renewTOTPCancel: "취소" - checkBackupCodesBeforeCloseThisWizard: "이 위자드를 닫기 전에 아래 백업 코드를 확인하십시오" - backupCodes: "백업 코드" - backupCodesDescription: "인증 앱을 사용할 수 없게 된 경우 아래 백업 코드를 사용하여 계정에 액세스 할 수 있습니다.이 코드들은 반드시 안전한 장소에 보관하십시오.각 코드는 한 번만 사용할 수 있습니다." - backupCodeUsedWarning: "백업 코드가 사용되었습니다.인증 앱을 사용할 수 없게 된 경우, 조속히 인증 앱을 다시 설정해 주십시오." - backupCodesExhaustedWarning: "백업 코드가 모두 사용되었습니다.인증 앱을 사용할 수 없는 경우 더 이상 계정에 액세스하는 것이 불가능합니다.인증 앱을 다시 등록해 주세요." - moreDetailedGuideHere: "여기에 자세한 설명이 있습니다" _permissions: "read:account": "계정의 정보를 봅니다" "write:account": "계정의 정보를 변경합니다" "read:blocks": "차단 여부를 확인합니다" "write:blocks": "차단을 하거나 해제합니다" - "read:drive": "드라이브 보기" + "read:drive": "드라이브를 조회합니다" "write:drive": "드라이브에 파일을 올리거나, 이름을 변경하거나, 삭제합니다" - "read:favorites": "즐겨찾기 보기" + "read:favorites": "즐겨찾기를 조회합니다" "write:favorites": "즐겨찾기에 추가하거나 삭제합니다" "read:following": "팔로우 상태를 봅니다" "write:following": "팔로우하거나 팔로우를 해제합니다" @@ -2302,7 +1686,7 @@ _permissions: "write:pages": "페이지를 수정합니다" "read:page-likes": "페이지의 좋아요를 확인합니다" "write:page-likes": "페이지에 좋아요를 추가하거나 취소합니다" - "read:user-groups": "유저 그룹 보기" + "read:user-groups": "유저 그룹을 조회합니다" "write:user-groups": "유저 그룹을 만들거나, 초대하거나, 이름을 변경하거나, 양도하거나, 삭제합니다" "read:channels": "채널을 보기" "write:channels": "채널을 추가하거나 삭제합니다" @@ -2310,79 +1694,21 @@ _permissions: "write:gallery": "갤러리를 추가하거나 삭제합니다" "read:gallery-likes": "갤러리의 좋아요를 확인합니다" "write:gallery-likes": "갤러리에 좋아요를 추가하거나 취소합니다" - "read:flash": "Play를 봅니다" - "write:flash": "Play를 조작합니다" - "read:flash-likes": "Play의 좋아요를 봅니다" - "write:flash-likes": "Play의 좋아요를 조작합니다" - "read:admin:abuse-user-reports": "유저 신고 보기" - "write:admin:delete-account": "유저 계정 삭제하기" - "write:admin:delete-all-files-of-a-user": "모든 유저 파일 삭제하기" - "read:admin:index-stats": "데이터베이스 색인 정보 보기" - "read:admin:table-stats": "데이터베이스 테이블 정보 보기" - "read:admin:user-ips": "유저 IP 주소 보기" - "read:admin:meta": "인스턴스 메타데이터 보기" - "write:admin:reset-password": "유저 비밀번호 재설정하기" - "write:admin:resolve-abuse-user-report": "유저 신고 처리하기" - "write:admin:send-email": "이메일 보내기" - "read:admin:server-info": "서버 정보 보기" - "read:admin:show-moderation-log": "조정 기록 보기" - "read:admin:show-user": "유저 개인정보 보기" - "write:admin:suspend-user": "유저 정지하기" - "write:admin:unset-user-avatar": "유저 아바타 삭제하기" - "write:admin:unset-user-banner": "유저 배너 삭제하기" - "write:admin:unsuspend-user": "유저 정지 해제하기" - "write:admin:meta": "인스턴스 메타데이터 수정하기" - "write:admin:user-note": "조정 기록 수정하기" - "write:admin:roles": "역할 수정하기" - "read:admin:roles": "역할 보기" - "write:admin:relays": "릴레이 수정하기" - "read:admin:relays": "릴레이 보기" - "write:admin:invite-codes": "초대 코드 수정하기" - "read:admin:invite-codes": "초대 코드 보기" - "write:admin:announcements": "공지사항 수정하기" - "read:admin:announcements": "공지사항 보기" - "write:admin:avatar-decorations": "아바타 꾸미기 수정하기" - "read:admin:avatar-decorations": "아바타 꾸미기 보기" - "write:admin:federation": "연합 정보 수정하기" - "write:admin:account": "유저 계정 수정하기" - "read:admin:account": "유저 정보 보기" - "write:admin:emoji": "이모지 수정하기" - "read:admin:emoji": "이모지 보기" - "write:admin:queue": "작업 대기열 수정하기" - "read:admin:queue": "작업 대기열 정보 보기" - "write:admin:promo": "홍보 기록 수정하기" - "write:admin:drive": "유저 드라이브 수정하기" - "read:admin:drive": "유저 드라이브 정보 보기" - "read:admin:stream": "관리자용 Websocket API 사용하기" - "write:admin:ad": "광고 수정하기" - "read:admin:ad": "광고 보기" - "write:invite-codes": "초대 코드 만들기" - "read:invite-codes": "초대 코드 불러오기" - "write:clip-favorite": "클립의 좋아요 수정하기" - "read:clip-favorite": "클립의 좋아요 보기" - "read:federation": "연합 정보 불러오기" - "write:report-abuse": "위반 내용 신고하기" - "write:chat": "대화를 시작하거나 메시지를 보냅니다" - "read:chat": "채팅 열람하기" _auth: shareAccessTitle: "어플리케이션의 접근 허가" - shareAccess: "‘{name}’에서 계정에 접근하는 것을 허용하시겠습니까?" + shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?" shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용하시겠습니까?" permission: "{name}에서 다음 권한을 요청하였습니다" permissionAsk: "이 앱은 다음의 권한을 요청합니다" pleaseGoBack: "앱으로 돌아가서 시도해 주세요" callback: "앱으로 돌아갑니다" - accepted: "접근 권한이 부여되었습니다." denied: "접근이 거부되었습니다" - scopeUser: "다음 유저로 활동하고 있습니다." pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오." - byClickingYouWillBeRedirectedToThisUrl: "접근을 허용하면 자동으로 다음 URL로 이동합니다." _antennaSources: all: "모든 노트" homeTimeline: "팔로우중인 유저의 노트" - users: "지정한 유저의 노트" + users: "지정한 한 명 혹은 여러 명의 유저의 노트" userList: "지정한 리스트에 속한 유저의 노트" - userBlacklist: "지정한 유저를 제외한 모든 노트" _weekday: sunday: "일요일" monday: "월요일" @@ -2421,8 +1747,6 @@ _widgets: _userList: chooseList: "리스트 선택" clicker: "클리커" - birthdayFollowings: "오늘이 생일인 유저" - chat: "채팅" _cw: hide: "숨기기" show: "더 보기" @@ -2470,7 +1794,7 @@ _postForm: b: "무슨 일이 일어나고 있나요?" c: "무엇을 생각하고 있나요?" d: "말하고 싶은 게 있나요?" - e: "여기에 적어 주세요" + e: "여기에 적어주세요" f: "작성해주시길 기다리고 있어요..." _profile: name: "이름" @@ -2484,28 +1808,21 @@ _profile: metadataContent: "내용" changeAvatar: "아바타 이미지 변경" changeBanner: "배너 이미지 변경" - verifiedLinkDescription: "내용에 자신의 프로필로 향하는 링크가 포함된 페이지의 URL을 삽입하면 소유자 인증 마크가 표시됩니다." - avatarDecorationMax: "최대 {max}개까지 장식을 할 수 있습니다." - followedMessage: "팔로우 받았을 때 메시지" - followedMessageDescription: "팔로우 받았을 때 상대방에게 보여줄 단문 메시지를 설정할 수 있습니다." - followedMessageDescriptionForLockedAccount: "팔로우를 승인제로 한 경우, 팔로우 요청을 수락했을 때 보여줍니다." _exportOrImport: allNotes: "모든 노트" favoritedNotes: "즐겨찾기한 노트" - clips: "클립" followingList: "팔로잉" muteList: "뮤트" blockingList: "차단" userLists: "리스트" excludeMutingUsers: "뮤트한 유저 제외하기" excludeInactiveUsers: "휴면 중인 계정 제외하기" - withReplies: "가져오기한 유저에 의한 답글을 타임라인에 포함" _charts: federation: "연합" apRequest: "요청" usersIncDec: "유저 수 증감" usersTotal: "유저 수 합계" - activeUsers: "활동 유저 수" + activeUsers: "활성 유저 수" notesIncDec: "노트 수 증감" localNotesIncDec: "로컬 노트 수 증감" remoteNotesIncDec: "리모트 노트 수 증감" @@ -2516,7 +1833,7 @@ _charts: storageUsageTotal: "스토리지 사용량 합계" _instanceCharts: requests: "요청" - users: "유저 수 차이" + users: "유저 수 증감" usersTotal: "누적 유저 수" notes: "노트 수 증감" notesTotal: "누적 노트 수" @@ -2546,15 +1863,17 @@ _play: title: "제목" script: "스크립트" summary: "설명" - visibilityDescription: "비공개로 설정하면 프로필에 표시하지 않지만 URL을 아는 사람은 계속해서 접속할 수 있습니다." _pages: newPage: "페이지 만들기" editPage: "페이지 수정" readPage: "소스 표시 중" + created: "페이지를 만들었습니다" + updated: "페이지를 수정했습니다" + deleted: "페이지가 삭제되었습니다" pageSetting: "페이지 설정" nameAlreadyExists: "지정한 페이지 URL이 이미 존재합니다" invalidNameTitle: "유효하지 않은 페이지 URL입니다" - invalidNameText: "비어있는지 확인해 주세요" + invalidNameText: "비어있지 않은지 확인해주세요" editThisPage: "이 페이지를 편집" viewSource: "소스 보기" viewPage: "페이지 보기" @@ -2571,14 +1890,13 @@ _pages: url: "페이지 URL" summary: "페이지 요약" alignCenter: "가운데 정렬" - hideTitleWhenPinned: "프로필에 고정한 경우 타이틀을 표시하지 않음" + hideTitleWhenPinned: "프로필에 고정해놓은 경우 타이틀을 표시하지 않음" font: "폰트" fontSerif: "명조체" fontSansSerif: "고딕체" eyeCatchingImageSet: "아이캐치 이미지를 설정" eyeCatchingImageRemove: "아이캐치 이미지를 삭제" chooseBlock: "블록 추가" - enterSectionTitle: "섹션 타이틀을 입력하기" selectType: "종류 선택" contentBlocks: "콘텐츠" inputBlocks: "입력" @@ -2589,8 +1907,6 @@ _pages: section: "섹션" image: "이미지" button: "버튼" - dynamic: "동적 블록" - dynamicDescription: "이 블록은 폐지되었습니다. 이제부터 {play}에서 이용해 주세요." note: "노트필기" _note: id: "노트 ID" @@ -2605,33 +1921,16 @@ _notification: youGotMention: "{name}님이 멘션함" youGotReply: "{name}님이 답글함" youGotQuote: "{name}님이 인용함" - youRenoted: "{name}님이 리노트했습니다" + youRenoted: "{name}님이 Renote" youWereFollowed: "새로운 팔로워가 있습니다" youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다" yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다" pollEnded: "투표 결과가 발표되었습니다" - newNote: "새 게시물" unreadAntennaNote: "안테나 {name}" - roleAssigned: "역할이 부여 되었습니다." - chatRoomInvitationReceived: "채팅 룸에 초대받았습니다" emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다" achievementEarned: "도전 과제를 달성했습니다" - testNotification: "알림 테스트" - checkNotificationBehavior: "알림 표시를 체크하기" - sendTestNotification: "테스트 알림 보내기" - notificationWillBeDisplayedLikeThis: "알림이 이렇게 표시됩니다" - reactedBySomeUsers: "{n}명이 리액션했습니다" - likedBySomeUsers: "{n}명이 좋아요를 했습니다" - renotedBySomeUsers: "{n}명이 리노트했습니다" - followedBySomeUsers: "{n}명에게 팔로우됨" - flushNotification: "알림 이력을 초기화" - exportOfXCompleted: "{x} 추출에 성공했습니다." - login: "로그인 알림이 있습니다" - createToken: "액세스 토큰이 생성되었습니다" - createTokenDescription: "만약 기억이 나지 않는다면 '{text}'를 통해 액세스 토큰을 삭제해 주세요." _types: all: "전부" - note: "유저의 새 글" follow: "팔로잉" mention: "멘션" reply: "답글" @@ -2641,13 +1940,7 @@ _notification: pollEnded: "투표가 종료됨" receiveFollowRequest: "팔로우 요청을 받았을 때" followRequestAccepted: "팔로우 요청이 승인되었을 때" - roleAssigned: "역할이 부여됨" - chatRoomInvitationReceived: "채팅 룸에 초대받음" achievementEarned: "도전 과제 획득" - exportCompleted: "추출을 성공함" - login: "로그인" - createToken: "액세스 토큰 만들기" - test: "알림 테스트" app: "연동된 앱을 통한 알림" _actions: followBack: "팔로우" @@ -2656,11 +1949,7 @@ _notification: _deck: alwaysShowMainColumn: "메인 칼럼 항상 표시" columnAlign: "칼럼 정렬" - columnGap: "칼럼 간 여백" - deckMenuPosition: "덱 메뉴 위치" - navbarPosition: "내비게이션 바 위치" addColumn: "칼럼 추가" - newNoteNotificationSettings: "새 노트 알림 설정" configureColumn: "칼럼 설정" swapLeft: "왼쪽으로 이동" swapRight: "오른쪽으로 이동" @@ -2674,10 +1963,6 @@ _deck: introduction: "칼럼을 조합해서 나만의 인터페이스를 구성해 보아요!" introduction2: "나중에라도 화면 우측의 + 버튼을 눌러 새 칼럼을 추가할 수 있습니다." widgetsIntroduction: "칼럼 메뉴의 \"위젯 편집\"에서 위젯을 추가해 주세요" - useSimpleUiForNonRootPages: "루트 이외의 페이지로 접속한 경우 UI 간략화하기" - usedAsMinWidthWhenFlexible: "'폭 자동 조정'이 활성화된 경우 최소 폭으로 사용됩니다" - flexible: "폭 자동 조정" - enableSyncBetweenDevicesForProfiles: "프로파일 정보의 디바이스 간 동기화를 활성화" _columns: main: "메인" widgets: "위젯" @@ -2689,9 +1974,8 @@ _deck: mentions: "받은 멘션" direct: "다이렉트" roleTimeline: "역할 타임라인" - chat: "채팅" _dialog: - charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {max}" + charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}" charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}" _disabledTimeline: title: "비활성화된 타임라인" @@ -2701,419 +1985,15 @@ _drivecleaner: orderByCreatedAtAsc: "등록일이 오래된 순" _webhookSettings: createWebhook: "Webhook 생성" - modifyWebhook: "Webhook 수정" name: "이름" secret: "시크릿" - trigger: "트리거" + events: "Webhook을 실행할 타이밍" active: "활성화" _events: follow: "누군가를 팔로우했을 때" followed: "누군가 나를 팔로우했을 때" note: "노트를 게시할 때" reply: "답글을 받았을 때" - renote: "누군가 내 글을 리노트했을 때" + renote: "누군가 내 글을 Renote했을 때" reaction: "누군가 내 노트에 리액션했을 때" mention: "누군가 나를 멘션했을 때" - _systemEvents: - abuseReport: "유저로부터 신고를 받았을 때" - abuseReportResolved: "받은 신고를 처리했을 때" - userCreated: "유저가 생성되었을 때" - inactiveModeratorsWarning: "모더레이터가 일정 기간동안 활동하지 않은 경우" - inactiveModeratorsInvitationOnlyChanged: "모더레이터가 일정 기간 활동하지 않아 시스템에 의해 초대제로 바뀐 경우" - deleteConfirm: "Webhook을 삭제할까요?" - testRemarks: "스위치 오른쪽에 있는 버튼을 클릭하여 더미 데이터를 사용한 테스트용 웹 훅을 보낼 수 있습니다." -_abuseReport: - _notificationRecipient: - createRecipient: "신고 수신자 추가" - modifyRecipient: "신고 수신자 편집" - recipientType: "알림 종류" - _recipientType: - mail: "이메일" - webhook: "Webhook" - _captions: - mail: "모더레이터 권한을 가진 유저의 이메일 주소에 알림을 보냅니다 (신고를 받은 때에만)" - webhook: "지정한 SystemWebhook에 알림을 보냅니다 (신고를 받은 때와 해결했을 때에 송신)" - keywords: "키워드" - notifiedUser: "알릴 유저" - notifiedWebhook: "사용할 Webhook" - deleteConfirm: "수신자를 삭제하시겠습니까?" -_moderationLogTypes: - createRole: "역할 생성" - deleteRole: "역할 삭제" - updateRole: "역할 수정" - assignRole: "역할 할당" - unassignRole: "역할 해제" - suspend: "정지" - unsuspend: "정지 해제" - addCustomEmoji: "커스텀 이모지 추가" - updateCustomEmoji: "커스텀 이모지 수정" - deleteCustomEmoji: "커스텀 이모지 삭제" - updateServerSettings: "서버 설정 갱신" - updateUserNote: "조정 기록 갱신" - deleteDriveFile: "파일 삭제" - deleteNote: "노트 삭제" - createGlobalAnnouncement: "전역 공지사항 생성" - createUserAnnouncement: "유저에게 공지사항 만들기" - updateGlobalAnnouncement: "모든 공지사항 수정" - updateUserAnnouncement: "유저의 공지사항 수정" - deleteGlobalAnnouncement: "모든 공지사항 삭제" - deleteUserAnnouncement: "유저의 공지사항 삭제" - resetPassword: "비밀번호 재설정" - suspendRemoteInstance: "리모트 서버를 정지" - unsuspendRemoteInstance: "리모트 서버의 정지를 해제" - updateRemoteInstanceNote: "리모트 서버의 조정 기록 갱신" - markSensitiveDriveFile: "파일에 열람주의를 설정" - unmarkSensitiveDriveFile: "파일에 열람주의를 해제" - resolveAbuseReport: "신고 처리" - forwardAbuseReport: "신고 전달" - updateAbuseReportNote: "신고 조정 노트 갱신" - createInvitation: "초대 코드 생성" - createAd: "광고 생성" - deleteAd: "광고 삭제" - updateAd: "광고 수정" - createAvatarDecoration: "아바타 장식 만들기" - updateAvatarDecoration: "아바타 장식 수정" - deleteAvatarDecoration: "아바타 장식 삭제" - unsetUserAvatar: "유저 아바타 제거" - unsetUserBanner: "유저 배너 제거" - createSystemWebhook: "SystemWebhook을 생성" - updateSystemWebhook: "SystemWebhook을 수정" - deleteSystemWebhook: "SystemWebhook을 삭제" - createAbuseReportNotificationRecipient: "신고 알림 수신자 생성" - updateAbuseReportNotificationRecipient: "신고 알림 수신자 편집" - deleteAbuseReportNotificationRecipient: "신고 알림 수신자 삭제" - deleteAccount: "계정을 삭제" - deletePage: "페이지를 삭제" - deleteFlash: "Play를 삭제" - deleteGalleryPost: "갤러리 게시물을 삭제" - deleteChatRoom: "채팅 룸 삭제" - updateProxyAccountDescription: "프록시 계정의 설명 업데이트" -_fileViewer: - title: "파일 상세" - type: "파일 유형" - size: "파일 크기" - url: "URL" - uploadedAt: "업로드 날짜" - attachedNotes: "첨부된 노트" - thisPageCanBeSeenFromTheAuthor: "이 페이지는 파일 소유자만 열람할 수 있습니다" -_externalResourceInstaller: - title: "외부 사이트로부터 설치" - checkVendorBeforeInstall: "제공자를 신뢰할 수 있는 경우에만 설치하십시오." - _plugin: - title: "이 플러그인을 설치하시겠습니까?" - _theme: - title: "이 테마를 설치하시겠습니까?" - _meta: - base: "기본 컬러 스키마" - _vendorInfo: - title: "제공자 정보" - endpoint: "참조한 엔드포인트" - hashVerify: "파일 무결성 확인" - _errors: - _invalidParams: - title: "파라미터가 부족합니다" - description: "외부 사이트로부터 데이터를 불러오기 위해 필요한 정보가 부족합니다. URL을 다시 한 번 확인하십시오." - _resourceTypeNotSupported: - title: "해당하는 외부 리소스는 지원되지 않습니다." - description: "외부 사이트의 해당 리소스는 지원되지 않습니다. 사이트 관리자에게 문의하십시오." - _failedToFetch: - title: "데이터를 불러올 수 없습니다" - fetchErrorDescription: "외부 사이트와의 통신에 실패하였습니다. 여러 번 시도해도 동일한 오류가 표시되는 경우 사이트 관리자에게 문의하십시오." - parseErrorDescription: "외부 사이트에서 불러온 데이터를 읽어들일 수 없습니다. 사이트 관리자에게 문의하십시오." - _hashUnmatched: - title: "데이터가 올바르지 않습니다." - description: "데이터의 무결성 확인에 실패하여, 보안을 위해 설치가 중단되었습니다. 사이트 관리자에게 문의하십시오." - _pluginParseFailed: - title: "AiScript 오류" - description: "데이터를 성공적으로 불러왔으나, AiScript 분석 과정에서 오류가 발생하여 읽어들일 수 없습니다. 플러그인 작성자에게 문의하십시오. 자세한 사항은 브라우저에 내장된 개발자 도구의 Javascript 콘솔에서 확인하실 수 있습니다." - _pluginInstallFailed: - title: "플러그인 설치에 실패했습니다" - description: "플러그인을 설치하는 도중 문제가 발생하였습니다. 다시 한 번 시도하십시오. 자세한 사항은 브라우저에 내장된 개발자 도구의 Javascript 콘솔에서 확인하실 수 있습니다." - _themeParseFailed: - title: "테마 코드 분석 오류" - description: "데이터를 성공적으로 불러왔으나, 테마 코드 분석 과정에서 오류가 발생하여 읽어들일 수 없습니다. 테마 작성자에게 문의하십시오. 자세한 사항은 브라우저에 내장된 개발자 도구의 Javascript 콘솔에서 확인하실 수 있습니다." - _themeInstallFailed: - title: "테마를 설치하지 못했습니다" - description: "테마를 설치하는 도중 문제가 발생하였습니다. 다시 한 번 시도하십시오. 자세한 사항은 브라우저에 내장된 개발자 도구의 Javascript 콘솔에서 확인하실 수 있습니다." -_dataSaver: - _media: - title: "미디어 불러오기" - description: "사진이나 동영상을 자동으로 불러오지 않습니다. 숨겨 놓은 사진이나 동영상은 누르면 불러옵니다." - _avatar: - title: "아이콘 이미지" - description: "아이콘 이미지의 애니메이션을 멈춥니다. 애니메이션 이미지는 일반 이미지보다 파일 크기가 클 수 있으므로 데이터 사용량을 더 줄일 수 있습니다." - _urlPreviewThumbnail: - title: "URL 미리보기의 섬네일을 비표시" - description: "URL 미리보기의 섬네일 이미지를 불러올 수 없게 됩니다." - _disableUrlPreview: - title: "URL 미리보기 비활성화" - description: "URL 미리보기 기능을 비활성화합니다. 섬네일 이미지와 달리 링크 정보 불러오기 자체를 줄일 수 있습니다." - _code: - title: "문자열 강조" - description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다." -_hemisphere: - N: "북반구" - S: "남반구" - caption: "일부 클라이언트 설정에서 계절을 판단하려고 사용합니다." -_reversi: - reversi: "리버시" - gameSettings: "대국 설정" - chooseBoard: "보드 선택" - blackOrWhite: "선공/후공" - blackIs: "{name}님이 흑(선공)" - rules: "규칙" - thisGameIsStartedSoon: "대국을 곧 시작합니다" - waitingForOther: "상대의 준비가 끝나기를 기다리고 있습니다." - waitingForMe: "나의 준비가 끝나기를 기다리고 있습니다." - waitingBoth: "준비하세요" - ready: "준비 완료" - cancelReady: "준비되지 않음" - opponentTurn: "상대의 차례입니다" - myTurn: "나의 차례입니다" - turnOf: "{name}님의 차례입니다" - pastTurnOf: "{name}님의 차례" - surrender: "기권" - surrendered: "상대의 기권" - timeout: "시간 초과" - drawn: "무승부" - won: "{name}님의 승리" - black: "흑" - white: "백" - total: "합계" - turnCount: "{count}번째 수" - myGames: "내 대국" - allGames: "모든 대국" - ended: "종료" - playing: "대국 중" - isLlotheo: "돌이 적은 쪽이 승리(로세오)" - loopedMap: "순환 지도" - canPutEverywhere: "어디든 둘 수 있는 모드" - timeLimitForEachTurn: "각 수의 시간 제한" - freeMatch: "자유 대국" - lookingForPlayer: "대국 상대를 찾고 있습니다" - gameCanceled: "대국이 취소되었습니다" - shareToTlTheGameWhenStart: "대국이 시작할 때 타임라인에 공유" - iStartedAGame: "대국을 시작하였습니다! #MisskeyReversi" - opponentHasSettingsChanged: "상대가 설정을 변경했습니다" - allowIrregularRules: "규칙 변경 허용(완전 자유)" - disallowIrregularRules: "규칙 변경 없음" - showBoardLabels: "판에 행·열 번호 표시" - useAvatarAsStone: "돌을 아이콘으로 표시" -_offlineScreen: - title: "오프라인 - 서버에 접속할 수 없습니다" - header: "서버에 접속할 수 없습니다" -_urlPreviewSetting: - title: "URL 미리보기 설정" - enable: "URL 미리보기 활성화" - allowRedirect: "미리보기 위치의 리디렉션 허가" - allowRedirectDescription: "입력된 URL이 리디렉션될 경우, 그 리디렉션 위치를 따라 미리보기를 표시할 것인지 설정합니다. 비활성화하면 서버 리소스를 절약할 수 있습니다만, 리디렉션 위치의 내용은 표시되지 않습니다." - timeout: "미리보기를 불러올 때의 타임아웃 (ms)" - timeoutDescription: "미리보기를 로딩하는데 걸리는 시간이 정한 시간보다 오래 걸리는 경우, 미리보기를 생성하지 않습니다." - maximumContentLength: "Content-Length의 최대치 (byte)" - maximumContentLengthDescription: "Content-Length가 이 값을 넘어서면 미리보기를 생성하지 않습니다." - requireContentLength: "Content-Length를 받아온 경우에만 " - requireContentLengthDescription: "상대 서버가 Content-Length를 되돌려주지 않는다면 미리보기를 만들지 않습니다." - userAgent: "User-Agent" - userAgentDescription: "미리보기를 얻을 때 사용한 User-Agent를 설정합니다. 비어 있다면 기본값의 User-Agent를 사용합니다." - summaryProxy: "미리보기를 만든 프록시의 엔드포인트" - summaryProxyDescription: "Misskey 본체를 사용하지 않고 서머리 프록시로 미리보기를 만듭니다." - summaryProxyDescription2: "프록시는 아래의 파라미터를 쿼리 문자열로 연동합니다. 프록시 측이 이를 지원하지 않으면 설정값을 무시합니다." -_mediaControls: - pip: "화면 속 화면" - playbackRate: "재생 속도" - loop: "반복 재생" -_contextMenu: - title: "컨텍스트 메뉴" - app: "애플리케이션" - appWithShift: "Shift 키로 애플리케이션" - native: "브라우저의 UI" -_gridComponent: - _error: - requiredValue: "이 값은 필수 항목입니다." - columnTypeNotSupport: "정규표현 규칙이 type:text인 칼럼만 지원합니다." - patternNotMatch: "이 값은 {pattern} 패턴과 일치하지 않습니다." - notUnique: "이 값은 다른 값과 중복되지 않아야 합니다." -_roleSelectDialog: - notSelected: "선택하지 않았습니다." -_customEmojisManager: - _gridCommon: - copySelectionRows: "선택한 행을 복사하기" - copySelectionRanges: "선택범위를 복사하기" - deleteSelectionRows: "선택한 행을 삭제" - deleteSelectionRanges: "선택한 행을 삭제" - searchSettings: "검색 설정" - searchSettingCaption: "고급 검색을 설정합니다." - searchLimit: "표시 건수" - sortOrder: "정렬 순서" - registrationLogs: "등록 로그" - registrationLogsCaption: "이모지를 갱신하거나 삭제할 때 로그가 표시됩니다. 갱신 또는 삭제하거나, 페이지 이동, 새로 고침하면 삭제됩니다." - alertEmojisRegisterFailedDescription: "이모지를 갱신 또는 삭제하지 못했습니다. 자세한 내용은 등록 로그를 확인해주세요." - _logs: - showSuccessLogSwitch: "성공 로그를 표시" - failureLogNothing: "실패 로그가 없습니다." - logNothing: "로그가 없습니다." - _remote: - selectionRowDetail: "선택 행 (상세)" - importSelectionRows: "선택 행을 가져오기" - importSelectionRangesRows: "선택한 범위 안의 행을 가져오기" - importEmojisButton: "선택한 이모지를 가져오기" - confirmImportEmojisTitle: "이모지 가져오기" - confirmImportEmojisDescription: "리모트 서버에서 받아온 이모지 {count}개를 이 서버로 가져옵니다. 이모지의 저작권, 라이선스를 확실히 확인하셨다면 실행해주세요." - _local: - tabTitleList: "등록한 이모지 리스트" - tabTitleRegister: "이모지 등록" - _list: - emojisNothing: "등록한 이모지가 없습니다." - markAsDeleteTargetRows: "선택한 행을 삭제할 대상으로 하기" - markAsDeleteTargetRanges: "선택한 범위의 행을 삭제 대상으로 하기" - alertUpdateEmojisNothingDescription: "변경할 이모지가 없습니다." - alertDeleteEmojisNothingDescription: "삭제 대상의 이모지는 없습니다." - confirmMovePage: "페이지를 이동할까요?" - confirmChangeView: "표시를 바꿀까요?" - confirmUpdateEmojisDescription: "{count}개의 이모지를 갱신합니다. 실행할까요?" - confirmDeleteEmojisDescription: "선택한 이모지 {count}개를 삭제합니다. 실행할까요?" - confirmResetDescription: "지금까지 했던 변경 내용이 모두 초기화됩니다." - confirmMovePageDesciption: "이 페이지의 이모지에 변경이 있습니다.\n저장하지 않은 상태로 페이지를 이동하면, 이 페이지에서 바꾼 변경 내용이 모두 지워집니다." - dialogSelectRoleTitle: "이모지에 설정된 역할을 검색" - _register: - uploadSettingTitle: "업로드 설정" - uploadSettingDescription: "여기서 이모지를 업로드 할 때의 동작을 설정할 수 있습니다." - directoryToCategoryLabel: "디렉토리 이름을 \"category\"로 입력하기" - directoryToCategoryCaption: "디렉토리를 드래그 앤 드롭한 경우, 디렉토리 이름을 \"category\"로 입력합니다." - confirmRegisterEmojisDescription: "리스트에 표시되어진 이모지를 새로운 커스텀 이모지로 등록합니다. 실행할까요? (부하를 피하기 위해, 한 번에 등록할 수 있는 이모지는 {count}건까지 입니다.)" - confirmClearEmojisDescription: "편집 내용을 지우고, 목록에 표시되어진 이모지를 지웁니다. 실행할까요?" - confirmUploadEmojisDescription: "드래그 앤 드롭한 {count}개의 파일을 드라이브에 업로드 합니다. 실행할까요?" -_embedCodeGen: - title: "임베디드 코드를 커스터마이즈" - header: "해더를 표시" - autoload: "자동으로 다음 코드를 실행 (비권장)" - maxHeight: "최대 높이" - maxHeightDescription: "최대 값을 무시하려면 0을 입력하세요. 위젯이 상하로 길어지는 것을 방지하려면, 임의의 값을 입력해 주세요." - maxHeightWarn: "높이 최대 값이 설정되어져 있지 않습니다(0). 의도적으로 설정 하지 않았다면 임의의 값을 설정해주세요." - previewIsNotActual: "미리보기로 표시할 수 있는 크기보다 큽니다. 실제로 넣은 코드의 표시가 다른 경우가 있습니다." - rounded: "외곽선을 둥글게 하기" - border: "외곽선에 테두리를 씌우기" - applyToPreview: "미리보기에 반영" - generateCode: "임베디드 코드를 만들기" - codeGenerated: "코드를 만들었습니다." - codeGeneratedDescription: "만들어진 코드를 웹 사이트에 붙여서 사용하세요." -_selfXssPrevention: - warning: "경고" - title: "“이 화면에 뭔가를 붙여넣어라\"는 것은 모두 사기입니다." - description1: "여기에 무언가를 붙여넣으면 악의적인 유저에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다." - description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오." - description3: "자세한 내용은 여기를 확인해 주세요. {link}" -_followRequest: - recieved: "받은 신청" - sent: "보낸 신청" -_remoteLookupErrors: - _federationNotAllowed: - title: "이 서버와 통신할 수 없음" - description: "이 서버와의 통신이 비활성화 되었거나, 이 서버를 차단 중이거나 서버에게 차단되었을 수 있습니다.\n서버 관리자에게 문의하세요." - _uriInvalid: - title: "URI가 잘못되었습니다." - description: "입력한 URI에 문제가 있습니다. URI에 쓸 수 없는 문자를 넣었는지 확인해보세요." - _requestFailed: - title: "요청을 실패했습니다." - description: "해당 서버와 통신을 실패했습니다. 상대방 서버에 접속 불가능한 상태일 수도 있습니다. 또는 잘못된 URI 또는 없는 URI를 입력했는지 확인해보세요." - _responseInvalid: - title: "유효하지 않은 반응입니다." - description: "이 서버와 통신할 수 있지만, 데이터가 올바르지 않습니다." - _noSuchObject: - title: "찾을 수 없습니다" - description: "요구된 리소스를 찾을 수 없습니다. URI를 다시 한 번 확인해보세요." -_captcha: - verify: "CAPTCHA를 먼저 해결하세요." - testSiteKeyMessage: "사이트 키와 비밀 키에 테스트용 값을 입력하여 미리보기를 확인할 수 있습니다.\n자세한 내용은 아래 페이지를 확인해보세요." - _error: - _requestFailed: - title: "CAPTCHA 요구에 실패했습니다." - text: "잠시 후에 다시 실행하거나, 설정을 다시 한 번 확인해보세요." - _verificationFailed: - title: "CAPTCHA 검증을 실패했습니다." - text: "설정이 올바른지 다시 한 번 확인해보세요." - _unknown: - title: "CAPTCHA 오류" - text: "알 수 없는 오류가 발생했습니다." -_bootErrors: - title: "로딩이 실패함" - serverError: "잠시 기다렸다가 다시 로드해도 여전히 문제가 해결되지 않으면 아래 Error ID와 함께 서버 관리자에게 연락해 주세요." - solution: "다음과 같은 방법으로 해결할 수 있습니다." - solution1: "브라우저 및 OS를 최신 버전으로 업데이트하기" - solution2: "광고 차단 비활성화하기" - solution3: "브라우저 캐시 지우기" - solution4: "(Tor Browser) dom.webaudio.enabled를 true로 설정하세요" - otherOption: "기타 옵션" - otherOption1: "클라이언트 설정 및 캐시 삭제" - otherOption2: "간편 클라이언트 실행" - otherOption3: "복구 툴 실행" -_search: - searchScopeAll: "전체" - searchScopeLocal: "로컬" - searchScopeServer: "서버 지정" - searchScopeUser: "유저 지정" - pleaseEnterServerHost: "서버의 호스트를 입력해 주세요." - pleaseSelectUser: "유저를 선택해주세요" - serverHostPlaceholder: "예: misskey.example.com" -_serverSetupWizard: - installCompleted: "Misskey의 설치가 완료됐습니다!" - firstCreateAccount: "먼저 관리자 계정을 만듭시다." - accountCreated: "관리자 계정이 만들어졌습니다!" - serverSetting: "서버 설정" - youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "이 위자드로 쉽게 최적화된 서버의 설정을 할 수 있습니다." - settingsYouMakeHereCanBeChangedLater: "이 설정은 나중에 변경 가능합니다." - howWillYouUseMisskey: "Misskey를 어떻게 사용하십니까?" - _use: - single: "1인 서버" - single_description: "자신 전용 서버로 혼자서 사용" - single_youCanCreateMultipleAccounts: "1인 서버로 운영하는 경우에도 계정은 필요에 따라 여러 개 만들 수 있습니다." - group: "그룹 서버" - group_description: "신뢰 가능한 다른 유저를 초대해 여러 명이 사용" - open: "오픈 서버" - open_description: "불특정 다수의 유저를 받아들이는 운영을 함" - openServerAdvice: "불특정 다수의 유저를 받아들이는 것에는 위험이 따릅니다. 문제에 대처할 수 있도록 확실한 조정 체제로 운영하는 것을 권장합니다." - openServerAntiSpamAdvice: "자신의 서버가 스팸으로 사용되지 않게끔 reCAPTCHA라는 안티 봇 기능을 활성화하는 등 보안에 대해서도 세심한 주의가 필요합니다." - howManyUsersDoYouExpect: "어느 정도의 인원으로 생각 중이십니까?" - _scale: - small: "100명 이하(소규모)" - medium: "100명 이상 1000명 이하(중간 규모)" - large: "1000명 이상(대규모)" - largeScaleServerAdvice: "대규모 서버에서는 부하분산이나 데이터베이스의 레플리케이션 등 높은 인프라스트럭처 지식이 필요할 수 있습니다." - doYouConnectToFediverse: "Fediverse에 접속하시겠습니까?" - doYouConnectToFediverse_description1: "분산형 서버로 구성된 네트워크(Fediverse)에 접속하면 다른 서버와 서로 콘텐츠의 주고받기를 할 수 있습니다." - doYouConnectToFediverse_description2: "Fediverse에 접속하는 것을 '연합'이라고도 부릅니다." - youCanConfigureMoreFederationSettingsLater: "나중에 연합 가능한 서버의 지정 등 고급 설정을 할 수 있습니다." - adminInfo: "관리자 정보" - adminInfo_description: "문의 접수를 위해 사용되는 관리자 정보를 설정합니다." - adminInfo_mustBeFilled: "오픈 서버 혹은 연합이 켜져 있는 경우 반드시 입력해야 합니다." - followingSettingsAreRecommended: "아래의 설정이 권장됩니다." - applyTheseSettings: "이 설정을 적용" - skipSettings: "설정 건너뛰기" - settingsCompleted: "설정이 완료됐습니다!" - settingsCompleted_description: "수고하셨습니다. 준비를 마쳤으므로 바로 서버의 이용을 시작하실 수 있습니다." - settingsCompleted_description2: "상세한 서버 설정은 '제어판'에서 하실 수 있습니다." - donationRequest: "기부 요청" - _donationRequest: - text1: "Misskey는 자원봉사자들에 의해 개발되는 무료 소프트웨어입니다." - text2: "앞으로도 계속해서 개발을 할 수 있도록 괜찮으시다면 부디 기부를 부탁드립니다." - text3: "지원자 대상 특전도 있습니다!" -_uploader: - compressedToX: "{x}로 압축" - savedXPercent: "{x}% 절약" - abortConfirm: "업로드되지 않은 파일이 있습니다만, 그만 두시겠습니까?" - doneConfirm: "업로드되지 않은 파일이 있습니다만, 완료하시겠습니까?" - maxFileSizeIsX: "업오드 가능한 최대 파일 크기는 {x}입니다." - allowedTypes: "업로드 가능한 파일 유형" - tip: "파일은 아직 업로드되지 않았습니다. 이 다이얼로그에서 업로드 전의 확인, 이름 바꾸기, 압축, 자르기 등을 하실 수 있습니다. 준비가 되셨다면 '업로드' 버튼을 클릭해 업로드를 시작하실 수 있습니다." -_clientPerformanceIssueTip: - title: "배터리 소비가 심하다고 생각되시면" - makeSureDisabledAdBlocker: "광고 차단을 비활성화해 주십시오." - makeSureDisabledAdBlocker_description: "광고 차단은 성능에 영향을 미칠 수 있습니다. OS의 기능이나 브라우저의 기능, 애드온 등으로 광고 차단이 활성화돼있지 않은지 확인해 주십시오." - makeSureDisabledCustomCss: "커스텀 CSS를 무효로 해주십시오." - makeSureDisabledCustomCss_description: "스타일을 덮어쓰기하면 성능에 영향을 미칠 수 있습니다. 커스텀 CSS나 스타일을 덮어쓰기하는 확장 기능이 유효로 돼있는지 확인해주십시오." - makeSureDisabledAddons: "확장 기능을 비활성화해 주십시오." - makeSureDisabledAddons_description: "일부 확장 기능은 클라이언트의 동작에 간섭해 성능에 영향을 미칠 수 있습니다. 브라우저의 확장 기능을 비활성화해 개선할지 확인해주십시오." -_clip: - tip: "클립은 노트를 정리할 수 있는 기능입니다." -_userLists: - tip: "임의의 유저가 포함된 리스트를 작성할 수 있습니다. 작성한 리스트는 타임라인으로 표시가 가능합니다." diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 455a71f302..cee139f502 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -1,9 +1,9 @@ --- _lang_: "ພາສາລາວ" -headlineMisskey: "ເຊື່ອມຕໍ່ເຄືອຂ່າຍໂດຍ note" -introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນຊອຟແວopensource, ສຳລັບບໍລິການ microblogging ແບບ decentralized\nສ້າງ “note” ເພື່ອແບ່ງປັນຄວາມຄິດຂອງທ່ານກັບທຸກໆ ຄົນທີ່ຢູ່ອ້ອມຮອບທ່ານ 📡\nຢ່າລືມ “reaction” ໂນຕຂອງລາວເພື່ອສະແດງຄວາມຮູ້ສຶກ 👍\nມາສຳຫຼວດໂລກໃໝ່ແນ! 🚀" +headlineMisskey: "ເຊື່ອມຕໍ່ເຄືອຂ່າຍໂດຍຫມາຍເຫດ" +introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນແຫຼ່ງເປີດ, ການບໍລິການ microblogging ກະຈາຍ\nສ້າງ \"ບັນທຶກ\" ເພື່ອແບ່ງປັນຄວາມຄິດຂອງທ່ານກັບທຸກໆຄົນທີ່ຢູ່ອ້ອມຮອບທ່ານ 📡\nດ້ວຍ \"ປະຕິກິລິຍາ\", ທ່ານຍັງສາມາດສະແດງຄວາມຮູ້ສຶກຂອງທ່ານຢ່າງໄວວາກ່ຽວກັບບັນທຶກຂອງທຸກໆຄົນ 👍\nມາສຳຫຼວດໂລກໃໝ່! 🚀" poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. Misskey (ເອີ້ນວ່າ \"Misskey instance\")" -monthAndDay: "ເດືອນ{month} / ວັນ{day}" +monthAndDay: "{ເດືອນ}/{ມື້}" search: "ຄົ້ນຫາ" notifications: "ການແຈ້ງເຕືອນ" username: "ຊື່ຜູ້ໃຊ້" @@ -15,79 +15,71 @@ gotIt: "ເຂົ້າໃຈແລ້ວ!" cancel: "ຍົກເລີກ" noThankYou: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້" enterUsername: "ປ້ອນຊື່ຜູ້ໃຊ້" -renotedBy: "Renoted ໂດຍ {user}" -noNotes: "ບໍ່ມີ note" +renotedBy: "Renoted ໂດຍ {ຜູ້ໃຊ້}" +noNotes: "ບໍ່ມີຫມາຍເຫດ" noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ" -instance: "ເຊີຟເວີຣ໌" -settings: "ຕັ້ງຄ່າ" -notificationSettings: "ຕັ້ງຄ່າການແຈ້ງເຕືອນ" +instance: "ອີນສະແຕນ" +settings: "ກຳນົດຄ່າ" basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ" otherSettings: "ການຕັ້ງຄ່າອື່ນໆ" -openInWindow: "ເປີດໃນ window" -profile: "ໂປຣໄຟລ໌" -timeline: "ໄທມ໌ໄລນ໌" -noAccountDescription: "ຜູ້ໃຊ້ຄົນນີ້ຍັງບໍ່ໄດ້ຂຽນຄຳແນະນຳໂຕ" +openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ" +profile: "ໂພຼຟາຍ" +timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" +noAccountDescription: "ຜູ້ໃຊ້ນີ້ຍັງບໍ່ໄດ້ຂຽນໃນຊີວະປະຫວັດຂອງເຂົາເຈົ້າເທື່ອ" login: "ເຂົ້າ​ສູ່​ລະ​ບົບ" loggingIn: "ກຳລັງເຂົ້າສູ່ລະບົບ..." logout: "ອອກ​ຈາກ​ລະ​ບົບ" signup: "ລົງ​ທະ​ບຽນ" -uploading: "ກຳລັງອັບໂຫຼດ..." +uploading: "ການອັບໂຫຼດ..." save: "ບັນທຶກ" -users: "ຜູ້ໃຊ້" +users: "ຜູ້ໃຊ້ຕ່າງໆ" addUser: "ເພີ່ມຜູ້ໃຊ້" favorite: "ເພີ່ມໃສ່ລາຍການທີ່ມັກ" favorites: "ລາຍການທີ່ມັກ" -unfavorite: "ເອົາອອກຈາກລາຍການທີ່ມັກ" +unfavorite: "ລຶບອອກຈາກລາຍການທີ່ມັກ" favorited: "ເພີ່ມໃສ່ລາຍການທີ່ມັກແລ້ວ" alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ." cantFavorite: "ບໍ່ສາມາດເພີ່ມໃສ່ລາຍການທີ່ມັກໄດ້." -pin: "ປັກໝຸດ" -unpin: "ຖອດປັກໝຸດອອກ" +pin: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌" +unpin: "ຖອດປັກໝຸດອອກຈາກໂປຣໄຟລ໌" copyContent: "ຄັດລອກເນື້ອຫາ" -copyLink: "ຄັດລອກລິ້ງ" -copyLinkRenote: "ຄັດລອກລິ້ງຂອງ renote" +copyLink: "ສຳເນົາລິ້ງ" delete: "ລຶບ" -deleteAndEdit: "ລຶບ​ແລະ​ແກ້​ໄຂ​" -deleteAndEditConfirm: "ຕ້ອງການລຶບ note ນີ້ແລະແກ້ໄຂໃໝ່ແມ່ນບໍ່? reaction, renote ແລະການຕອບກັບຕໍ່ note ນີ້ ທັງເບິດຈະຖືກລຶບອອກ" +deleteAndEdit: "ລົບ​ແລະ​ແກ້​ໄຂ​" +deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ" addToList: "ເພີ່ມໃສ່ລາຍຊື່" -addToAntenna: "ເພີ່ມໃສ່ເສົາອາກາດ" sendMessage: "ສົ່ງຂໍ້ຄວາມ" -copyRSS: "ຄັດລອກ RSS" -copyUsername: "ຄັດລອກຊື່ຜູ້ໃຊ້" -copyUserId: "ຄັດລອກ ID ຜູ້ໃຊ້" -copyNoteId: "ຄັດລອກ ID ຂອງ note" -copyFileId: "ຄັດລອກ ID ໄຟລ໌" -copyFolderId: "ຄັດລອກ ID ໂຟລ໌ເດີຣ໌" -copyProfileUrl: "ຄັດລອກ URL ໂປຣໄຟລ໌" +copyRSS: "ສຳເນົາ RSS" +copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້" searchUser: "ຄົ້ນຫາຜູ້ໃຊ້" -reply: "ຕອບ​ກັບ" +reply: "ຕອບ​ໄປ​ທີ" loadMore: "ໂຫຼດເພີ່ມເຕີມ" showMore: "ໂຫຼດເພີ່ມເຕີມ" showLess: "ປິດ" -youGotNewFollower: "ໄດ້ຕິດຕາມເຈົ້າ" -receiveFollowRequest: "ມີຄຳຂໍຕິດຕາມສົ່ງມາ" -followRequestAccepted: "ການຕິດຕາມໄດ້ຮັບອນຸຍາດແລ້ວ" -mention: "ເວົ້າເຖີງ" -mentions: "ເວົ້າເຖີງເຈົ້າ" -directNotes: "ໂພສ Direct note" +youGotNewFollower: "ໄດ້ຕິດຕາມທ່ານ" +receiveFollowRequest: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ໄດ້ຮັບ" +followRequestAccepted: "ຜູ້ຕິດຕາມໄດ້ຍອມຮັບຄໍາຮ້ອງຂໍຂອງທ່ານ" +mention: "ໄດ້ກ່າວມາ" +mentions: "ກ່າວເຖິງ" +directNotes: "ໂດຍກົງຫມາຍເຫດ" importAndExport: "ນໍາເຂົ້າ / ສົ່ງອອກ" import: "ນຳເຂົ້າ" -export: "ສົ່ງອອກ" +export: "ນຳອອກ" files: "ໄຟລ໌" download: "ດາວໂຫລດ" -driveFileDeleteConfirm: "ຕ້ອງການລຶບໄຟລ໌ “{name}” ແມ່ນບໍ່? Note ທີ່ແນບມາກັບໄຟລ໌ນີ້ຈະຖືກລຶບອອກ" -unfollowConfirm: "ຕ້ອງການເລີກຕິດຕາມ {name} ແມ່ນບໍ່?" -exportRequested: "ເຈົ້າໄດ້ຮ້ອງຂໍການສົ່ງອອກ ອາດໃຊ້ເວລາຈັກໜ່ອຍ ເມື່ອແລ້ວຈະຖືກເພີ່ມໃສ່ drive" -importRequested: "ເຈົ້າໄດ້ຮ້ອງຂໍການນຳເຂົ້າ ການດຳເນິນການນີ້ອາດໃຊ້ເວລາຈັກໜ່ອຍ" +driveFileDeleteConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບໄຟລ໌ \"{name}\"? ບັນທຶກທີ່ມີໄຟລ໌ແນບນີ້ຈະຖືກລຶບຖິ້ມ" +unfollowConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເຊົາຕິດຕາມ {name}?" +exportRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການສົ່ງອອກ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ ແລະມັນຈະຖືກເພີ່ມໃສ່ drive ຂອງທ່ານເມື່ອມັນສຳເລັດແລ້ວ" +importRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການນໍາເຂົ້າ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ" lists: "ລາຍການ" -noLists: "ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​" -note: "Note" -notes: "Note" +noLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​" +note: "ບັນທຶກ" +notes: "ບັນທຶກ" following: "ກຳລັງຕິດຕາມ" followers: "ຜູ້ຕິດຕາມ" followsYou: "ຕິດ​ຕາມ​ເຈົ້າ" createList: "ສ້າງລາຍຊື່" -manageLists: "ຈັດການລາຍຊື່" +manageLists: "ການບໍລິຫານບັນຊີລາຍການ" error: "ຂໍ້ຜິດພາດ" somethingHappened: "​ອຸຍ, ມີ​ບາງ​ຢ່າງ​ຜິ​ດ​ພາດ" retry: "ລອງໃຫມ່" @@ -97,42 +89,33 @@ serverIsDead: "ເຊີບເວີນີ້ບໍ່ຕອບສະໜອງ youShouldUpgradeClient: "ເພື່ອເບິ່ງໜ້ານີ້, ກະລຸນາໂຫຼດຂໍ້ມູນຄືນໃໝ່ເພື່ອອັບເດດລູກຄ້າຂອງທ່ານ" enterListName: "ໃສ່ຊື່ສຳລັບລາຍຊື່" privacy: "ຄວາມເປັນສ່ວນຕົວ" -makeFollowManuallyApprove: "ຕິດຕາມຄຳຂໍທີ່ຕ້ອງໄດ້ຮັບການອະນຸມັດ" -defaultNoteVisibility: "ການເບິ່ງເຫັນທີ່ເປັນຄ່າເລີ່ມຕົ້ນ" +makeFollowManuallyApprove: "ປະຕິບັດຕາມການຮ້ອງຂໍຮຽກຮ້ອງໃຫ້ມີການອະນຸມັດ" +defaultNoteVisibility: "ເປັນຄ່າເລີ່ມຕົ້ນ" follow: "ກຳລັງຕິດຕາມ" -followRequest: "ສົ່ງ​ຄຳຂໍ​ຕິ​ດ​ຕາມ​" -followRequests: "ສົ່ງ​ຄຳຂໍ​ຕິ​ດ​ຕາມ​" +followRequest: "ສົ່ງ​ການ​ຮ້ອງ​ຂໍ​ປະ​ຕິ​ບ​ຕາມ​" +followRequests: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍ" unfollow: "ເຊົາຕິດຕາມ" -followRequestPending: "ລໍຖ້າການອະນຸມັດໃຫ້ຕິດຕາມ" -enterEmoji: "ປ້ອນເອໂມຈິ" +followRequestPending: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ລໍຖ້າຢູ່" +enterEmoji: "ປ້ອນອີໂມຈິ" renote: "Renote" unrenote: "ເລີກ Renote" -renoted: "renote ແລ້ວ" -cantRenote: "ໂພສນີ້ບໍ່ສາມາດ renote ໃໝ່ໄດ້" -cantReRenote: "ບໍ່ສາມາດບັນທຶກຄືນໃໝ່ໄດ້" -quote: "ອ້າງອີງ" -inChannelRenote: "Renote ໃນ channel ເທົ່ານັ້ນ" -inChannelQuote: "ອ້າງອິງໃນ channel ເທົ່ານັ້ນ" -pinnedNote: "note ທີ່ປັກໝຸດໄວ້" -pinned: "ປັກໝຸດ" +renoted: "ເກັບບັນທຶກໄວ້" +quote: "ລວມຂໍ້ຄວາມອ້າງອີງ" +pinnedNote: "ບັນທຶກທີ່ປັກໝຸດໄວ້" +pinned: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌" you: "ເຈົ້າ" clickToShow: "ກົດເພື່ອສະແດງໃຫ້ເຫັນ" sensitive: "NSFW" add: "ເພີ່ມ" -reaction: "reaction" -reactions: "reaction" -attachCancel: "ເອົາໄຟລ໌ແນບ" +reaction: "ປະຕິກິລິຍາ" +reactions: "ປະຕິກິລິຍາ" mute: "ປີດສຽງ" unmute: "ເປີດສຽງ" -block: "ບລັອກ" -unblock: "ເລີກບລັອກ" +block: "ບ໋ອກ" +unblock: "ຍົກເລີກກາຮົບລັອກ" suspend: "ລະງັບ" unsuspend: "ເຊົາ​ລະ​ງັບ" -selectList: "ເລືອກລາຍຊື່" -editList: "ແກ້ໄຂລາຍຊື່" -selectChannel: "ເລືອກຊ່ອງ" -selectAntenna: "ເລືອກເສົາອາກາດ" -editAntenna: "ແກ້ໄຂເສົາອາກາດ" +selectList: "ເລືອກບັນຊີລາຍການ" selectWidget: "ເລືອກວິກເຈັດ" editWidgets: "ແກ້ໄຂ Widget" editWidgetsExit: "ສຳເລັດແລ້ວ" @@ -142,7 +125,6 @@ emojis: "ອີໂມຈິ" emojiName: "ຊື່ Emoji" emojiUrl: "URL ອີໂມຈິ" addEmoji: "ຕື່ມອີໂມຈິ" -settingGuide: "ການຕັ້ງຄ່າທີ່ແນະນໍາ" flagAsBot: "ໝາຍບັນຊີນີ້ເປັນບັອດ" flagAsCat: "ໝາຍບັນຊີນີ້ເປັນແມວ" flagAsCatDescription: "ເປີດໃຊ້ຕົວເລືອກນີ້ເພື່ອໝາຍບັນຊີນີ້ເປັນແມວ" @@ -151,34 +133,29 @@ flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບ autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່" addAccount: "ເພີ່ມບັນຊີ" loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ" -showOnRemote: "ເບິ່ງໃນເຊີຟເວີຣ໌ໄລຍະໄກ" general: "ທົ່ວໄປ" wallpaper: "ພາບພື້ນຫລັງ" setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ" -removeWallpaper: "ລຶບຮູບວໍເປເປີອອກ" searchWith: "ຊອກຫາ: {q}" -youHaveNoLists: "ເຈົ້າບໍ່ມີລາຍຊື່ໃດໆ" proxyAccount: "ບັນຊີພຣັອກຊີ" -host: "ໂຮສຕ໌" +host: "ໂຮດສ" selectUser: "ເລືອກຜູ້ໃຊ້" recipient: "ເຖິງ" annotation: "ຄຳເຫັນ" federation: "ສະຫະພັນ" -instances: "ເຊີຟເວີຣ໌" +instances: "ອີນສະແຕນ" registeredAt: "ລົງທະບຽນຢູ່" storageUsage: "ບ່ອນ​ຈັດ​ເກັບ​ຂໍ້​ມູນທີ່ໃຊ້" -charts: "ແຜນພູມ" +charts: "ອັນດັບເພງ" perHour: "ຕໍ່ຊົ່ວໂມງ" perDay: "ຕໍ່​ມື້" stopActivityDelivery: "ຢຸດເຊົາການສົ່ງກິດຈະກໍາ" blockThisInstance: "ຂັດຂວາງຕົວຢ່າງນີ້" operations: "ການດຳເນີນງານ" software: "ຊອບແວ" -version: "ເວີຣ໌ຊັນ" +version: "ສະບັບ" metadata: "Metadata" -withNFiles: "{n} ໄຟລ໌(s)" monitor: "ຈໍພາບ" -jobQueue: "ຄິວວຽກ" cpuAndMemory: "CPU ແລະ ຫນ່ວຍຄວາມຈໍາ" network: "ເຄືອຂ່າຍ" disk: "ດິສກ໌" @@ -199,15 +176,15 @@ federating: "ສະຫະພັນ" blocked: "ບລັອກແລ້ວ " suspended: "ໂຈະ" all: "ທັງໝົດ" -subscribing: "ກຳລັງສະມັກສະມາຊິກ" -publishing: "ກຳລັງ​ເຜີຍ​ແພ່" +subscribing: "ສະໝັກສະມາຊິກແລັວ" +publishing: "ການ​ພິມ​ເຜີຍ​ແຜ່" notResponding: "ບໍ່ຕອບສະໜອງ" -instanceFollowing: "ກຳລັງຕິດຕາມບົນເຊີຟເວີຣ໌" -instanceFollowers: "ຜູ້ຕິດຕາມຂອງເຊີຟເວີຣ໌" -instanceUsers: "ຜູ້​ໃຊ້​ຂອງ​ເຊີຟເວີຣ໌ນີ້" +instanceFollowing: "ກຳລັງຕິດຕາມສຸດຕົວຢ່າງ" +instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ" +instanceUsers: "ຜູ້​ຊົມ​ໃຊ້​ຂອງ​ຕົວ​ຢ່າງ​ນີ້​" changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ" security: "ຄວາມປອດໄພ" -retypedNotMatch: "ປ້ອນຂໍ້ມູນບໍ່ກົງກັນ" +retypedNotMatch: "ວັດສະດຸປ້ອນບໍ່ກົງກັນ" currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ" newPassword: "ລະຫັດຜ່ານໃໝ່" newPasswordRetype: "ໃສ່ລະຫັດຜ່ານໃໝ່ອີກເທື່ອໜຶ່ງ" @@ -223,18 +200,17 @@ remove: "ລຶບ" removed: "ລຶບແລ້ວ" resetAreYouSure: "ຣີ​ເຊັດບໍ?" saved: "ບັນທຶກແລ້ວ" +messaging: "ແຊ໋ດ" upload: "ອັບໂຫຼດ" keepOriginalUploading: "ຮັກສາຮູບພາບຕົ້ນສະບັບ" fromDrive: "ຈາກ Drive" fromUrl: "ຈາກ URL" uploadFromUrl: "ອັບໂຫຼດຈາກ URL" uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ" -uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດແລ້ວ" -explore: "ສຳຫຼວດ" +uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດ" messageRead: "ອ່ານແລ້ວ" +startMessaging: "ເລີ່ມການສົນທະນາໃໝ່" nUsersRead: "ອ່ານໂດຍ {n}" -agree: "ຍອມຮັບ" -termsOfService: "ເງື່ອນໄຂການບໍລິການ" start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ" home: "ໜ້າຫຼັກ" activity: "ກິດຈະກຳ" @@ -242,47 +218,46 @@ images: "ຮູບພາບ" image: "ຮູບພາບ" birthday: "ວັນເກີດ" yearsOld: "{age} ປີ" -registeredDate: "ວັນທີ່ລົງທະບຽນ" +registeredDate: "ວັນທີ່ເປັນສະມາຊິກ" location: "ທີ່ຕັ້ງ" -theme: "Theme" -themeForLightMode: "Theme ໃຊ້ໃນໂໝດສະຫວ່າງ" -themeForDarkMode: "Theme ໃຊ້ໃນໂໝດມືດ" +theme: "ແທ໋ມ" +themeForLightMode: "ຮູບແບບສີສັນເພື່ອໃຊ້ໃນໂໝດແສງ" +themeForDarkMode: "ຮູບແບບສີສັນທີ່ຈະໃຊ້ຢູ່ໃນໂໝດມືດ" light: "ສະຫວ່າງ" dark: "ມືດ" lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ" darkThemes: "ຮູບແບບສີສັນມືດ" syncDeviceDarkMode: "ຊິງຄ໌ໂໝດມືດກັບການຕັ້ງຄ່າທົ່ວອຸປະກອນ" -drive: "Drive" +drive: "ຂັບ" fileName: "ຊື່ໄຟລ໌" selectFile: "ເລືອກໄຟລ໌" selectFiles: "ເລືອກໄຟລ໌" selectFolder: "ເລືອກໂຟລເດີ" selectFolders: "ເລືອກໂຟລເດີ" renameFile: "ປ່ຽນຊື່ໄຟລ໌" -folderName: "ຊື່ໂຟລເດີຣ໌" +folderName: "ຊື່ໂຟນເດີ" createFolder: "​ສ້າງ​ໂຟ​ລ​ເດີ" renameFolder: "ປ່ຽນຊື່ໂຟນເດີນີ້" deleteFolder: "ລົບໂຟ​ລ​ເດີ​" addFile: "ເພີ່ມໄຟລ໌" emptyDrive: "Drive ຂອງທ່ານຫວ່າງເປົ່າ" -emptyFolder: "ໂຟລເດີຣ໌ນີ້ວ່າງເປົ່າ" +emptyFolder: "ໂຟນເດີນີ້ເປົ່າຫວ່າງ" unableToDelete: "ບໍ່​ສາ​ມາດລົບໄດ້" inputNewFileName: "ໃສ່ຊື່ໄຟລ໌ໃໝ່" inputNewDescription: "ໃສ່ຄຳບັນຍາຍໃໝ່" inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃໝ່" circularReferenceFolder: "ໂຟນເດີປາຍທາງແມ່ນໂຟນເດີຍ່ອຍຂອງໂຟນເດີທີ່ທ່ານຕ້ອງການຍ້າຍ" rename: "ປ່ຽນຊື່" -doNothing: "ຢ່າມັນ" -watch: "ເພັ່ງເລັງ" -unwatch: "ຢຸດເພັ່ງເລັງ" +watch: "ເບິ່ງ" +unwatch: "ຢຸດເບິ່ງ" accept: "ອະນຸຍາດ" reject: "ປະຕິເສດ" normal: "ປົກກະຕິ" instanceName: "ຊື່ເຊີເວີ້" -instanceDescription: "ຄຳອະທິບາຍແນະນຳເຊີຟເວີຣ໌" +instanceDescription: "ຄໍາອະທິບາຍຕົວຢ່າງ" maintainerName: "ຜູ້ດູແລ" -maintainerEmail: "ອີເມລຜູ້ດູແລ" -tosUrl: " URL ເງື່ອນໄຂການໃຫ້ບໍລິການ" +maintainerEmail: "ອີເມວ admin" +tosUrl: "ເງື່ອນໄຂການໃຫ້ບໍລິການ URL" thisYear: "ປີນີ້" thisMonth: "ເດືອນນີ້" today: "ມື້ນີ້" @@ -290,67 +265,43 @@ dayX: "ວັນ {day}" monthX: "ເດືອນ {month}" yearX: "ປີ {year}" pages: "ໜ້າ" -integration: "ເຊື່ອມໂຍງ" +integration: "ຄວາມສຳພັນຂອງ" connectService: "ເຊື່ອມຕໍ່" disconnectService: "ຕັດການເຊື່ອມຕໍ່" enableLocalTimeline: "ເປີດໃຊ້ທາມລາຍທ້ອງຖິ່ນ" enableGlobalTimeline: "ເປີດໃຊ້ທາມລາຍທົ່ວໂລກ" -disablingTimelinesInfo: "ຜູ້ດູແລລະບບແລະຜູ້ຄວບຄຸມຈະສາມາດເຂົ້າເຖີງໄທມ໌ໄລນ໌ທັ້ງເບີດ ເຖີງວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍ່ຕາມ" +disablingTimelinesInfo: "ຜູ້ເບິ່ງແຍງລະບົບ ແລະຜູ້ຄວບຄຸມຈະມີການເຂົ້າເຖິງທຸກກຳນົດເວລາ, ເຖິງແມ່ນວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍຕາມ" registration: "ລົງທະບຽນ" +enableRegistration: "ເປີດໃຊ້ການລົງທະບຽນຜູ້ໃຊ້ໃໝ່" invite: "ເຊີນ" -driveCapacityPerLocalAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ" -driveCapacityPerRemoteAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ໄລຍະໄກ" -basicInfo: "ຂໍ້ມຸນເບື້ອງຕົ້ນ" -pinnedNotes: "Note ທີ່ປັກໝຸດໄວ້" -hcaptchaSiteKey: "Site key" -hcaptchaSecretKey: "Secret key" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret Key" -recaptcha: "reCAPTCHA" -enableRecaptcha: "ເປີດໃຊ້ງານ reCAPTCHA" -recaptchaSiteKey: "Site key" -recaptchaSecretKey: "Secret key" -turnstileSiteKey: "Site key" -turnstileSecretKey: "Secret key" +driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ" +driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ" +pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້" +turnstileSiteKey: "ກະແຈໄຊທ໌" +turnstileSecretKey: "ກະແຈລັບ" name: "ຊື່" userList: "ລາຍການ" about: "ກ່ຽວກັບ" aboutMisskey: "ກ່ຽວກັບ Misskey" -administrator: "ຜູ້ດູແລ" -token: "ໂທເຄັນ" +administrator: "ຜູ້ບໍລິຫານ" share: "ແບ່ງປັນ" notFound: "ບໍ່ພົບ" -help: "ຊ່ວຍເຫຼືອ" -close: "ປິດ" +cacheClear: "ລຶບລ້າງແຄສ" invites: "ເຊີນ" -members: "ສະມາຊິກ" -transfer: "ໂອນຍ້າຍ" title: "ຫົວຂໍ້" text: "ຂໍ້ຄວາມ" enable: "ເປີດໃຊ້" next: "ຕໍ່ໄປ" -retype: "ລອງພິມລະຫັດອີກເທື່ອໜຶ່ງ" -quoteAttached: "ອ້າງອິງ" invitations: "ເຊີນ" -unavailable: "ບໍ່​ສາ​ມາດ​ໃຊ້​ໄດ້" language: "ພາສາ" -aboutX: "ກ່ຽວກັບ {x}" -emojiStyle: "ຮູບແບບອີໂມຈິ" native: "ພາ​ສາ​ແມ່" -noHistory: "​ບໍ່​ມີປະຫວັດ" -doing: "ກຳລັງປະມວນຜົນ..." category: "ຫມວດຫມູ່" -tags: "Aliases" +tags: "ແທ໋ກ" createAccount: "ສ້າງບັນຊີ" -existingAccount: "ບັນຊີທີ່ມີຢູ່ແລ້ວ" -dashboard: "Dashboard" +existingAccount: "ທີ່ມີຢູ່" +dashboard: "ໜ້າປັດ" local: "ທ້ອງຖິ່ນ" -numberOfDays: "ຈຳນວນມື້" -objectStorageBucket: "Bucket" -objectStoragePrefix: "Prefix" -objectStorageEndpoint: "Endpoint" -objectStorageRegion: "ພູມິພາກ" -deleteAll: "ລຶບທັງໝົດ" +objectStorageRegion: "ພາກ​ພື້ນ" sounds: "ສຽງ" sound: "ສຽງ" none: "ບໍ່ມີ" @@ -362,70 +313,36 @@ state: "ສະຖານະ" sort: "ຈັດຮຽງໂດຍ" ascendingOrder: "ນ້ອຍໄປຫາໃຫຍ່" descendingOrder: "ໃຫຍ່ຫານ້ອຍ" -output: "Output" -script: "Script" -menu: "ເມນູ" -rearrange: "ຈັດລຽງໃໝ່" -poll: "Poll" -description: "ລາຍລະອຽດ" -author: "ຜູ້ຂຽນ" -manage: "ການຈັດການ" -plugins: "ປລັ໋ກອີນ" -width: "ກວ້າງ" -height: "ຄວາມສູງ" -large: "ໃຫຍ່." -medium: "ປານກາງ" -small: "ເລັກ" -permission: "ການອະນຸຍາດ" -notificationType: "​ປະເພດການ​ແຈ້ງ​ເຕືອນ" -edit: "ແກ້ໄຂ" -email: "ອີເມວ" -smtpHost: "ໂຮສຕ໌" +output: "ຜົນຜະລິດ" +script: "ບົດ​ຄວາມ" +smtpHost: "ໂຮດສ" smtpUser: "ຊື່ຜູ້ໃຊ້" smtpPass: "ລະຫັດຜ່ານ" clearCache: "ລຶບລ້າງແຄສ" info: "ກ່ຽວກັບ" user: "ຜູ້ໃຊ້ຕ່າງໆ" -administration: "ການຈັດການ" -middle: "ປານກາງ" searchByGoogle: "ຄົ້ນຫາ" file: "ໄຟລ໌" -replies: "ຕອບ​ກັບ" -renotes: "Renote" -information: "ກ່ຽວກັບ" -_chat: - invitations: "ເຊີນ" - noHistory: "​ບໍ່​ມີປະຫວັດ" - members: "ສະມາຊິກ" - home: "ໜ້າຫຼັກ" -_delivery: - stop: "ໂຈະ" - _type: - none: "ກຳລັງ​ເຜີຍ​ແພ່" -_role: - _priority: - middle: "ປານກາງ" _email: _follow: title: "ໄດ້ຕິດຕາມທ່ານ" _theme: - description: "ລາຍລະອຽດ" keys: mention: "ໄດ້ກ່າວມາ" renote: "Renote" _sfx: note: "ບັນທຶກ" notification: "ການແຈ້ງເຕືອນ" + chat: "ແຊ໋ດ" _2fa: renewTOTPCancel: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້" _widgets: - profile: "ໂປຣໄຟລ໌" - instanceInfo: "ຂໍ້ມູລເຊີຟເວີຣ໌" + profile: "ໂພຼຟາຍ" + instanceInfo: "ອີນສະແຕນ" notifications: "ການແຈ້ງເຕືອນ" timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" activity: "ກິດຈະກຳ" federation: "ສະຫະພັນ" - jobQueue: "ຄິວວຽກ" _userList: chooseList: "ເລືອກບັນຊີລາຍການ" _cw: @@ -439,29 +356,27 @@ _profile: _exportOrImport: followingList: "ກຳລັງຕິດຕາມ" muteList: "ປີດສຽງ" - blockingList: "ບລັອກ" + blockingList: "ບ໋ອກ" userLists: "ລາຍການ" _charts: federation: "ສະຫະພັນ" _timelines: home: "ໜ້າຫຼັກ" _play: - script: "Script" - summary: "ລາຍລະອຽດ" + script: "ບົດ​ຄວາມ" _pages: blocks: image: "ຮູບພາບ" _notification: - youWereFollowed: "ໄດ້ຕິດຕາມເຈົ້າ" + youWereFollowed: "ໄດ້ຕິດຕາມທ່ານ" _types: follow: "ກຳລັງຕິດຕາມ" - mention: "ໄດ້ກ່າວເຖິງ" + mention: "ໄດ້ກ່າວມາ" renote: "Renote" - quote: "ອ້າງອີງ" - reaction: "Reaction" - login: "ເຂົ້າ​ສູ່​ລະ​ບົບ" + quote: "ລວມຂໍ້ຄວາມອ້າງອີງ" + reaction: "ປະຕິກິລິຍາ" _actions: - reply: "ຕອບ​ກັບ" + reply: "ຕອບ​ໄປ​ທີ" renote: "Renote" _deck: _columns: @@ -469,17 +384,6 @@ _deck: tl: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" list: "ລາຍການ" channel: "ຊ່ອງ" - mentions: "ກ່າວເຖິງເຈົ້າ" + mentions: "ກ່າວເຖິງ" _webhookSettings: name: "ຊື່" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "ອີເມວ" -_moderationLogTypes: - suspend: "ລະງັບ" -_remoteLookupErrors: - _noSuchObject: - title: "ບໍ່ພົບ" -_search: - searchScopeAll: "ທັງໝົດ" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 1fc4342e92..c22b978f3c 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -5,13 +5,9 @@ introMisskey: "Welkom! Misskey is een open source, gedecentraliseerde microblogd poweredByMisskeyDescription: "{name} is één van de services die door het open source platform Misskey wordt geleverd (het wordt ook wel een \"Misskey server genmoemd\")." monthAndDay: "{day} {month}" search: "Zoeken" -reset: "Herstellen" notifications: "Meldingen" username: "Gebruikersnaam" password: "Wachtwoord" -initialPasswordForSetup: "Initiële wachtwoord voor configuratie" -initialPasswordIsIncorrect: "Initiële wachtwoord voor configuratie is onjuist" -initialPasswordForSetupDescription: "Gebruik het initiële wachtwoord uit de configuratie, als je Misskey zelf hebt geïnstalleerd.\nAls je een Misskey hosting provider gebruikt, gebruik dan het gegeven wachtwoord.\nAls je geen wachtwoord hebt gezet, laat het dan leeg om verder te gaan." forgotPassword: "Wachtwoord vergeten" fetchingAsApObject: "Ophalen vanuit de Fediverse" ok: "Ok" @@ -24,7 +20,6 @@ noNotes: "Geen notities" noNotifications: "Geen meldingen" instance: "Server" settings: "Instellingen" -notificationSettings: "Notificatie instellingen" basicSettings: "Basisinstellingen" otherSettings: "Overige instellingen" openInWindow: "In een venster openen" @@ -49,23 +44,13 @@ pin: "Vastmaken aan profielpagina" unpin: "Losmaken van profielpagina" copyContent: "Kopiëren inhoud" copyLink: "Kopiëren link" -copyRemoteLink: "Remote-link kopiëren" -copyLinkRenote: "" delete: "Verwijderen" deleteAndEdit: "Verwijderen en bewerken" deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop." addToList: "Aan lijst toevoegen" -addToAntenna: "Voeg toe aan antenne" sendMessage: "Verstuur bericht" -copyRSS: "Kopieer RSS" copyUsername: "Kopiëren gebruikersnaam " -copyUserId: "Kopieer gebruiker ID" -copyNoteId: "Kopieer notitie ID" -copyFileId: "Kopieer veld ID" -copyFolderId: "Kopieer folder ID" -copyProfileUrl: "Kopieer profiel URL" searchUser: "Zoeken een gebruiker" -searchThisUsersNotes: "Notities van deze gebruiker doorzoeken" reply: "Antwoord" loadMore: "Laad meer" showMore: "Toon meer" @@ -114,14 +99,9 @@ enterEmoji: "Voer een emoji in" renote: "Herdelen" unrenote: "Stop herdelen" renoted: "Herdeeld" -renotedToX: "Renoted naar {name}" cantRenote: "Dit bericht kan niet worden herdeeld" cantReRenote: "Een herdeling kan niet worden herdeeld" quote: "Quote" -inChannelRenote: "Alleen-kanaal Renote" -inChannelQuote: "Alleen-kanaal Citaat" -renoteToChannel: "Renote naar kanaal" -renoteToOtherChannel: "Renote naar ander kanaal" pinnedNote: "Vastgemaakte notitie" pinned: "Vastmaken aan profielpagina" you: "Jij" @@ -130,23 +110,15 @@ sensitive: "NSFW" add: "Toevoegen" reaction: "Reacties" reactions: "Reacties" -emojiPicker: "Emoji kiezer" -pinnedEmojisForReactionSettingDescription: "Kies de emojis die als eerste getoond worden tijdens het reageren" -pinnedEmojisSettingDescription: "Kies de emojis die als eerste getoond worden tijdens het reageren" -emojiPickerDisplay: "Emoji kiezer weergave" -overwriteFromPinnedEmojisForReaction: "Overschrijven met reactieinstellingen" -overwriteFromPinnedEmojis: "Overschrijven met algemene instellingen" +reactionSetting: "Reacties die in de reactie-selector worden getoond" reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen" rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen" attachCancel: "Verwijder bijlage" -deleteFile: "Bestand verwijderen" markAsSensitive: "Markeren als NSFW" unmarkAsSensitive: "Geen NSFW" enterFileName: "Invoeren bestandsnaam" mute: "Dempen" unmute: "Stop dempen" -renoteMute: "Renotes dempen" -renoteUnmute: "Dempen Renotes opheffen" block: "Blokkeren" unblock: "Deblokkeren" suspend: "Opschorten" @@ -156,15 +128,11 @@ unblockConfirm: "Ben je zeker dat je deze account wil blokkeren?" suspendConfirm: "Ben je zeker dat je deze account wil suspenderen?" unsuspendConfirm: "Ben je zeker dat je deze account wil opnieuw aanstellen?" selectList: "Kies een lijst." -editList: "Lijst bewerken" -selectChannel: "Kanaal selecteren" selectAntenna: "Kies een antenne" -editAntenna: "Antenne bewerken" -createAntenna: "Antenne aanmaken" selectWidget: "Kies een widget" editWidgets: "Bewerk widgets" editWidgetsExit: "Klaar" -customEmojis: "Eigen emoji" +customEmojis: "Maatwerk emoji" emoji: "Emoji" emojis: "Emoji" emojiName: "Naam emoji" @@ -172,10 +140,6 @@ emojiUrl: "URL emoji" addEmoji: "Toevoegen emoji" settingGuide: "Aanbevolen instellingen" cacheRemoteFiles: "Externe bestanden cachen" -cacheRemoteFilesDescription: "Als deze instelling uitgeschakeld is worden bestanden altijd direct van remote servers geladen. Hiermee wordt opslagruimte bespaard, maar doordat er geen thumbnails worden gegenereerd, zal netwerkverkeer toenemen." -youCanCleanRemoteFilesCache: "Klik op de 🗑️ knop in de bestandsbeheerweergave om de cache te wissen." -cacheRemoteSensitiveFiles: "Gevoelige bestanden van externe instances in de cache bewaren" -cacheRemoteSensitiveFilesDescription: "Als deze instelling is uitgeschakeld, worden gevoelige bestanden op afstand direct vanuit de instantie op afstand geladen zonder caching." flagAsBot: "Markeer dit account als een robot." flagAsBotDescription: "Als dit account van een programma wordt beheerd, zet deze vlag aan. Het aanzetten helpt andere ontwikkelaars om bijvoorbeeld onbedoelde feedback loops te doorbreken of om Misskey meer geschikt te maken." flagAsCat: "Markeer dit account als een kat." @@ -184,13 +148,8 @@ flagShowTimelineReplies: "Toon antwoorden op de tijdlijn." flagShowTimelineRepliesDescription: "Als je dit vlag aanzet, toont de tijdlijn ook antwoorden op andere en niet alleen jouw eigen notities." autoAcceptFollowed: "Accepteer verzoeken om jezelf te volgen vanzelf als je de verzoeker al volgt." addAccount: "Account toevoegen" -reloadAccountsList: "Accountlijst opnieuw laden" loginFailed: "Aanmelding mislukt." showOnRemote: "Toon op de externe instantie." -continueOnRemote: "Verder op remote server" -chooseServerOnMisskeyHub: "Kies een server van de Misskey Hub" -specifyServerHost: "Serverhost uitkiezen" -inputHostName: "Domein invullen" general: "Algemeen" wallpaper: "Achtergrond" setWallpaper: "Achtergrond instellen" @@ -201,7 +160,6 @@ followConfirm: "Weet je zeker dat je {name} wilt volgen?" proxyAccount: "Proxy account" proxyAccountDescription: "Een proxy-account is een account dat onder bepaalde voorwaarden fungeert als externe volger voor gebruikers. Als een gebruiker bijvoorbeeld een externe gebruiker aan de lijst toevoegt, wordt de activiteit van de externe gebruiker niet aan de server geleverd als geen lokale gebruiker die gebruiker volgt, dus het proxy-account volgt in plaats daarvan." host: "Server" -selectSelf: "Mezelf kiezen" selectUser: "Kies een gebruiker" recipient: "Ontvanger" annotation: "Reacties" @@ -216,8 +174,6 @@ perHour: "Per uur" perDay: "Per dag" stopActivityDelivery: "Stop met versturen activiteiten" blockThisInstance: "Blokkeer deze server" -silenceThisInstance: "Instantie dempen" -mediaSilenceThisInstance: "Media van deze server dempen" operations: "Verwerkingen" software: "Software" version: "Versie" @@ -237,12 +193,6 @@ clearCachedFiles: "Cache opschonen" clearCachedFilesConfirm: "Weet je zeker dat je alle externe bestanden in de cache wilt verwijderen?" blockedInstances: "Geblokkeerde servers" blockedInstancesDescription: "Maak een lijst van de servers die moeten worden geblokkeerd, gescheiden door regeleinden. Geblokkeerde servers kunnen niet meer communiceren met deze server." -silencedInstances: "Gedempte instanties" -silencedInstancesDescription: "Geef de hostnamen van de servers die je wil dempen op, elk op hun eigen regel. Alle accounts die bij de opgegeven servers horen worden als gedempt behandeld, kunnen alleen maar volgverzoeken maken, en kunnen lokale accounts niet vermelden als ze niet gevolgd worden. Geblokkeerde servers worden hier niet door beïnvloed." -mediaSilencedInstances: "Media-gedempte servers" -mediaSilencedInstancesDescription: "Geef de hostnamen van de servers die je wil media-dempen op, elk op hun eigen regel. Alle accounts die bij de opgegeven servers horen worden als gedempt behandeld, en kunnen geen eigen emojis gebruiken. Geblokkeerde servers worden hier niet door beïnvloed." -federationAllowedHosts: "Servers die mogen federeren " -federationAllowedHostsDescription: "Geef de hostnamen van de servers die mogen federeren op, elk op hun eigen regel." muteAndBlock: "Gedempt en geblokkeerd" mutedUsers: "Gedempte gebruikers" blockedUsers: "Geblokkeerde gebruikers" @@ -250,6 +200,7 @@ noUsers: "Er zijn geen gebruikers." editProfile: "Bewerk Profiel" noteDeleteConfirm: "Ben je zeker dat je dit bericht wil verwijderen?" pinLimitExceeded: "Je kunt geen berichten meer vastprikken" +intro: "Installatie van Misskey geëindigd! Maak nu een beheerder aan." done: "Klaar" processing: "Bezig met verwerken" preview: "Voorbeeld" @@ -286,8 +237,8 @@ removed: "Succesvol verwijderd" removeAreYouSure: "Weet je zeker dat je \"{x}\" wil verwijderen?" deleteAreYouSure: "Weet je zeker dat je \"{x}\" wil verwijderen?" resetAreYouSure: "Resetten?" -areYouSure: "Weet je het zeker?" saved: "Opgeslagen" +messaging: "Chat" upload: "Uploaden" keepOriginalUploading: "Origineel beeld behouden." keepOriginalUploadingDescription: "Bewaar de originele versie bij het uploaden van afbeeldingen. Indien uitgeschakeld, wordt bij het uploaden een alternatieve versie voor webpublicatie genereert." @@ -300,13 +251,9 @@ uploadFromUrlMayTakeTime: "Het kan even duren voordat het uploaden voltooid is." explore: "Verkennen" messageRead: "Lezen" noMoreHistory: "Er is geen verdere geschiedenis" -startChat: "Chat starten" +startMessaging: "Start een gesprek" nUsersRead: "gelezen door {n}" agreeTo: "Ik stem in met {0}" -agree: "Akkoord" -agreeBelow: "Ik ga akkoord met de volgende" -basicNotesBeforeCreateAccount: "Belangrijke informatie" -termsOfService: "Gebruiksvoorwaarden" start: "Aan de slag" home: "Startpagina" remoteUserCaution: "Aangezien deze gebruiker van een externe server afkomstig is, kan de weergegeven informatie onvolledig zijn." @@ -331,15 +278,12 @@ selectFile: "Kies een bestand" selectFiles: "Selecteer bestanden" selectFolder: "Kies een map" selectFolders: "Kies mappen" -fileNotSelected: "Geen bestand geselecteerd" renameFile: "Wijzig bestandsnaam" folderName: "Mapnaam" createFolder: "Map aanmaken" renameFolder: "Map hernoemen" deleteFolder: "Map verwijderen" -folder: "Map" addFile: "Bestand toevoegen" -showFile: "Bestanden weergeven" emptyDrive: "Jouw Drive is leeg." emptyFolder: "Deze map is leeg" unableToDelete: "Kan niet worden verwijderd" @@ -352,7 +296,6 @@ copyUrl: "URL kopiëren" rename: "Hernoemen" avatar: "Avatar" banner: "Banner" -displayOfSensitiveMedia: "Weergave van gevoelige media" whenServerDisconnected: "Wanneer de verbinding met de server wordt onderbroken" disconnectedFromServer: "Verbinding met de server onderbroken." reload: "Verversen" @@ -382,28 +325,22 @@ enableLocalTimeline: "Inschakelen lokale tijdlijn" enableGlobalTimeline: "Inschakelen globale tijdlijn " disablingTimelinesInfo: "Beheerders en moderators hebben altijd toegang tot alle tijdlijnen, ook als ze niet actief zijn." registration: "Registreren" +enableRegistration: "Inschakelen registratie nieuwe gebruikers " invite: "Uitnodigen" driveCapacityPerLocalAccount: "Opslagruimte per lokale gebruiker" driveCapacityPerRemoteAccount: "Opslagruimte per externe gebruiker" inMb: "in megabytes" +iconUrl: "Pictogram URL" bannerUrl: "Banner URL" backgroundImageUrl: "URL afbeelding" basicInfo: "Basisinformatie" pinnedUsers: "Vastgeprikte gebruikers" -pinnedUsersDescription: "Een lijst met gebruikersnamen, gescheiden door regeleinden, die moet worden vastgemaakt in het tabblad “Verkennen”" pinnedPages: "Vastgeprikte pagina's" -pinnedPagesDescription: "Voer de paden in van de Pagina's die je aan de bovenste pagina van deze instantie wilt vastmaken, gescheiden door regeleinden." -pinnedClipId: "ID van de clip die moet worden vastgepind" pinnedNotes: "Vastgemaakte notitie" hcaptcha: "hCaptcha" enableHcaptcha: "Inschakelen hCaptcha" hcaptchaSiteKey: "Site sleutel" hcaptchaSecretKey: "Geheime sleutel" -mcaptcha: "mCaptcha" -enableMcaptcha: "mCaptcha activeren" -mcaptchaSiteKey: "Site sleutel" -mcaptchaSecretKey: "Geheime sleutel" -mcaptchaInstanceUrl: "mCaptcha server-URL" recaptcha: "reCAPTCHA" enableRecaptcha: "Inschakelen reCAPTCHA" recaptchaSiteKey: "Site sleutel" @@ -412,21 +349,12 @@ turnstile: "Tourniquet" enableTurnstile: "Inschakelen tourniquet" turnstileSiteKey: "Site sleutel" turnstileSecretKey: "Geheime sleutel" -avoidMultiCaptchaConfirm: "Het gebruik van meerdere Captcha-systemen kan interferentie tussen deze systemen veroorzaken. Wil je de andere Captcha-systemen die momenteel actief zijn uitschakelen? Als je wilt dat ze ingeschakeld blijven, druk dan op annuleren." antennas: "Antennes" manageAntennas: "Antennes beheren" name: "Naam" antennaSource: "Bron antenne" antennaKeywords: "Sleutelwoorden" antennaExcludeKeywords: "Blokkeerwoorden" -antennaExcludeBots: "Bot-accounts uitsluiten" -antennaKeywordsDescription: "Scheid met spaties voor een EN-voorwaarde of met regeleinden voor een OF-voorwaarde." -notifyAntenna: "Houd een notificatie bij nieuwe notities" -withFileAntenna: "Alleen notities met bestanden" -excludeNotesInSensitiveChannel: "Sluit notities uit van gevoelige kanalen" -enableServiceworker: "Activeer pushmeldingen in de browser" -antennaUsersDescription: "Lijst één gebruikersnaam per regel" -caseSensitive: "Hoofdlettergevoelig" withReplies: "Antwoorden toevoegen" connectedTo: "De volgende accounts zijn verbonden" notesAndReplies: "Berichten en reacties" @@ -447,31 +375,20 @@ about: "Over" aboutMisskey: "Over Misskey" administrator: "Beheerder" token: "Token" -2fa: "Twee factor authenticatie" -setupOf2fa: "Tweefactorauthenticatie instellen" -totp: "Verificatie-App" -totpDescription: "Log in via de verificatie-app met het eenmalige wachtwoord" moderator: "Moderator" moderation: "Moderatie" -moderationNote: "Moderatienotitie" -moderationNoteDescription: "Voer hier notities in. Deze zijn alleen zichtbaar voor de moderators." -addModerationNote: "Moderatienotitie toevoegen" -moderationLogs: "Moderatieprotocollen" nUsersMentioned: "Vermeld door {n} gebruikers" -securityKeyAndPasskey: "Beveiligings- en pasjessleutels" securityKey: "Beveiligingssleutel" lastUsed: "Laatst gebruikt" -lastUsedAt: "Laatst gebruikt: {t}" unregister: "Uitschrijven" passwordLessLogin: "Inloggen zonder wachtwoord" -passwordLessLoginDescription: "Maakt aanmelden zonder wachtwoord mogelijk met een beveiligingstoken of -wachtsleutel" resetPassword: "Wachtwoord terugzetten" newPasswordIs: "Het nieuwe wachtwoord is „{password}”." reduceUiAnimation: "Verminder beweging in de UI" share: "Delen" notFound: "Niet gevonden" -notFoundDescription: "Er is geen pagina gevonden onder deze URL." uploadFolder: "Standaardmap voor uploaden" +cacheClear: "Cache verwijderen" markAsReadAllNotifications: "Markeer alle meldingen als gelezen" markAsReadAllUnreadNotes: "Markeer alle berichten als gelezen" markAsReadAllTalkMessages: "Markeer alle berichten als gelezen" @@ -479,466 +396,21 @@ help: "Help" inputMessageHere: "Voer hier je bericht in" close: "Sluiten" invites: "Uitnodigen" -members: "Leden" -transfer: "Overdracht" -title: "Titel" -text: "Tekst" -enable: "Inschakelen" -next: "Volgende" -retype: "Opnieuw invoeren" -noteOf: "Notitie van {user}" -quoteAttached: "Citaat" -quoteQuestion: "Toevoegen als citaat?" -attachAsFileQuestion: "De tekst op het klembord is te lang. Wilt u het als een tekstbestand bijvoegen?" -onlyOneFileCanBeAttached: "Per bericht kan slechts één bestand worden bijgevoegd" -signinRequired: "Gelieve te registreren of in te loggen om verder te gaan" -signinOrContinueOnRemote: "Ga naar je eigen instantie of registreer je/log in op deze server om door te gaan." invitations: "Uitnodigen" -invitationCode: "Uitnodigingscode" -checking: "Wordt gecheckt ..." -available: "Beschikbaar" -unavailable: "Onbeschikbaar" -usernameInvalidFormat: "Je kunt kleine letters, hoofdletters, cijfers en onderstrepingstekens gebruiken." -tooShort: "Te kort" -tooLong: "Te lang" -weakPassword: "Zwak wachtwoord" -normalPassword: "Redelijke wachtwoord" -strongPassword: "Sterk wachtwoord" -passwordMatched: "Lucifers" -passwordNotMatched: "Komt niet overeen" -signinWith: "Aanmelden met {x}" -signinFailed: "Inloggen mislukt. Controleer gebruikersnaam en wachtwoord." -or: "Of" -language: "Taal" -uiLanguage: "Taal van gebruikersinterface" -aboutX: "Over {x}" -emojiStyle: "Emoji-stijl" -native: "Inheems" -menuStyle: "Menustijl" -style: "Stijl" -drawer: "Lade" -popup: "Pop-up" -showNoteActionsOnlyHover: "Toon notitiemenu alleen bij muisaanwijzer" -showReactionsCount: "Zie het aantal reacties op notities" -noHistory: "Geen geschiedenis gevonden" -signinHistory: "Inloggeschiedenis" -enableAdvancedMfm: "Uitgebreide MFM activeren" -enableAnimatedMfm: "Geanimeerde MFM activeren" -doing: "In uitvoering..." -category: "Categorie" -tags: "Aliassen" -docSource: "Broncode van dit document" -createAccount: "Gebruikersaccount maken" -existingAccount: "Bestaand gebruikersaccount" -regenerate: "Regenereer" -fontSize: "Lettergrootte" -mediaListWithOneImageAppearance: "Hoogte van medialijsten met slechts één afbeelding" -limitTo: "Beperken tot {x}" -noFollowRequests: "Je hebt geen lopende volgverzoeken" -openImageInNewTab: "Afbeeldingen in nieuw tabblad openen" -dashboard: "Overzicht" -local: "Lokaal" -remote: "Remote" -total: "Totaal" -weekOverWeekChanges: "Wijzigingen sinds vorige week" -dayOverDayChanges: "Dagelijkse wijzigingen" -appearance: "Weergave" -clientSettings: "Clientinstellingen" -accountSettings: "Accountinstellingen" -promotion: "Promotie" -promote: "Promoot" -numberOfDays: "Aantal dagen" -hideThisNote: "Verberg deze notitie" -showFeaturedNotesInTimeline: "Laat featured notities in tijdlijn zien" -objectStorage: "Object Storage" -useObjectStorage: "Object Storage gebruiken" -objectStorageBaseUrl: "Basis-URL" -objectStorageBaseUrlDesc: "De URL die wordt gebruikt als referentie. Als je een CDN of proxy gebruikt, voer dan de URL daarvan in. Gebruik voor S3 ‘https://.s3.amazonaws.com’. Gebruik voor GCS of vergelijkbaar ‘https://storage.googleapis.com/’." -objectStorageBucket: "Bucket" -objectStorageBucketDesc: "Geef de bucketnaam op die bij je provider wordt gebruikt." -objectStoragePrefix: "Prefix" -objectStoragePrefixDesc: "Bestanden worden opgeslagen in de mappen onder deze prefix." -objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "Laat dit leeg als je AWS S3 gebruikt, anders geef je het eindpunt op als ‘’ of ‘:’, afhankelijk van de service die je gebruikt." -objectStorageRegion: "Region" -objectStorageRegionDesc: "Voer een regio in zoals “xx-east-1”. Als je provider geen onderscheid maakt tussen regio's, voer dan “us-east-1” in. Laat leeg als je AWS-configuratiebestanden of omgevingsvariabelen gebruikt." -objectStorageUseSSL: "SSL gebruiken" -objectStorageUseSSLDesc: "Deactiveer dit als u geen HTTPS gebruikt voor API-verbindingen" -objectStorageUseProxy: "Verbinden via proxy" -objectStorageUseProxyDesc: "Deactiveer dit als u geen proxy wilt gebruiken voor verbindingen met de API" -objectStorageSetPublicRead: "Instellen op “public-read” op upload" -s3ForcePathStyleDesc: "Als s3ForcePathStyle is geactiveerd, moet de bucketnaam niet worden opgegeven in de hostnaam van de URL, maar in het pad van de URL. Deze optie moet mogelijk worden geactiveerd als services zoals een zelfbediende Minio-instantie worden gebruikt." -serverLogs: "Serverprotocollen" -deleteAll: "Alles verwijderen" -showFixedPostForm: "Het postingformulier bovenaan de tijdbalk weergeven" -showFixedPostFormInChannel: "Het postingformulier bovenaan de tijdbalk weergeven (Kanalen)" -withRepliesByDefaultForNewlyFollowed: "Toon replies van nieuw gevolgde gebruikers standaard in de tijdlijn" -newNoteRecived: "Er zijn nieuwe notities" -sounds: "Geluiden" sound: "Geluid" -listen: "Luisteren" -none: "Niets" -showInPage: "Weergeven in een pagina" -popout: "Pop-Up" -volume: "Volume" -masterVolume: "Hoofdvolume" -notUseSound: "Geluid uitschakelen" -useSoundOnlyWhenActive: "Geluid alleen inschakelen wanneer Misskey actief is" -details: "Details" -renoteDetails: "Renote Details" -chooseEmoji: "Emoji selecteren" -unableToProcess: "De operatie kan niet worden voltooid." -recentUsed: "Recent gebruikt" -install: "Installeren" -uninstall: "Deinstalleren" -installedApps: "Geautoriseerde toepassingen" -nothing: "Niets te zien hier" -installedDate: "Geautoriseerd at" -lastUsedDate: "Laatst gebruikt at" -state: "Status" -sort: "Sorteren" -ascendingOrder: "Oplopende volgorde" -descendingOrder: "Aflopende volgorde" -scratchpad: "Testomgeving" -scratchpadDescription: "De testomgeving biedt een gebied voor AiScript experimenten. Daar kunt u AiScript schrijven en uitvoeren en de effecten ervan op Misskey controleren." -uiInspector: "UI-inspecteur" -uiInspectorDescription: "De lijst met servers van UI-componenten kan worden bekeken in de cache. De UI-component wordt gegenereerd door de functie Ui:C:" -output: "Uitvoer" -script: "Script" -disablePagesScript: "AiScript uitschakelen op pagina's" -updateRemoteUser: "Gebruikersinformatie bijwerken" -unsetUserAvatar: "Avatar verwijderen" -unsetUserAvatarConfirm: "Weet je zeker dat je je avatar wil verwijderen?" -unsetUserBanner: "Banner verwijderen" -unsetUserBannerConfirm: "Weet je zeker dat je je banner wil verwijderen?" -deleteAllFiles: "Alle bestanden verwijderen" -deleteAllFilesConfirm: "Wil je echt alle bestanden verwijderen?" -removeAllFollowing: "Ontvolg alle gevolgde gebruikers" -removeAllFollowingDescription: "Door dit uit te voeren worden alle accounts van {host} ontvolgd. Voer dit uit als de instantie bijvoorbeeld niet meer bestaat." -userSuspended: "Deze gebruiker is geschorst." -userSilenced: "Deze gebruiker is instantiebreed gedempt." -yourAccountSuspendedTitle: "Deze account is geschorst" -yourAccountSuspendedDescription: "Dit gebruikersaccount is geschorst omdat het de gebruiksvoorwaarden van deze server heeft geschonden. Neem contact op met de operator voor meer informatie. Maak geen nieuwe gebruikersaccount aan." -tokenRevoked: "Ongeldig token" -tokenRevokedDescription: "Het token is verlopen. Log opnieuw in." -accountDeleted: "Het gebruikersaccount is verwijderd" -accountDeletedDescription: "Deze account is verwijderd." -menu: "Menu" -divider: "Scheider" -addItem: "Element toevoegen" -rearrange: "Sorteren" -relays: "Relays" -addRelay: "Relay toevoegen" -inboxUrl: "Inbox-URL" -addedRelays: "Toegevoegd Relays" -serviceworkerInfo: "Moet worden geactiveerd voor pushmeldingen." -deletedNote: "Verwijderde notitie" -invisibleNote: "Privé notitie" -enableInfiniteScroll: "Automatisch meer laden" -visibility: "Zichtbaarheid" -poll: "Peiling" -useCw: "Inhoudswaarschuwing gebruiken" -enablePlayer: "Videospeler openen" -disablePlayer: "Videospeler sluiten" -expandTweet: "Notitie uitklappen" -themeEditor: "Thema-editor" -description: "Beschrijving" -describeFile: "Beschrijving toevoegen" -enterFileDescription: "Beschrijving invoeren" -author: "Auteur" -leaveConfirm: "Er zijn niet-opgeslagen wijzigingen. Wil je ze verwijderen?" -manage: "Beheer" -plugins: "Plugins" -preferencesBackups: "Instellingen Back-ups" -deck: "Dek" -undeck: "Dek verlaten" -useBlurEffectForModal: "Vervagingseffect gebruiken voor modals" -useFullReactionPicker: "Volledige reaktieselectier gebruiken" -width: "Breedte" -height: "Hoogte" -large: "Groot" -medium: "Medium" -small: "Klein" -generateAccessToken: "Toegangstoken genereren" -permission: "Machtigingen" -adminPermission: "Administratorrechten" -enableAll: "Alle activeren" -disableAll: "Alle deactiveren" -tokenRequested: "Toegang verlenen tot het gebruikersaccount" -pluginTokenRequestedDescription: "Deze plugin kan de hier geconfigureerde autorisaties gebruiken." -notificationType: "Type melding" -edit: "Bewerken" -emailServer: "Email-Server" -enableEmail: "Email distributie inschakelen" -emailConfigInfo: "Wordt gebruikt om je email te bevestigen tijdens het aanmelden of als je je wachtwoord bent vergeten" -email: "Email" -emailAddress: "Email adres" -smtpConfig: "SMTP-server configuratie" smtpHost: "Server" -smtpPort: "Poort" smtpUser: "Gebruikersnaam" smtpPass: "Wachtwoord" -emptyToDisableSmtpAuth: "Laat gebruikersnaam en wachtwoord leeg om SMTP-authenticatie uit te schakelen." -smtpSecure: "Impliciet SSL/TLS gebruiken voor SMTP-verbindingen" -smtpSecureInfo: "Schakel dit uit bij gebruik van STARTTLS" -testEmail: "Emailversand testen" -wordMute: "Woord dempen" -wordMuteDescription: "Minimaliseert notities die het gespecificeerde woord of zin bevatten. Geminimaliseerde notities kunnen worden weergegeven door er op te klikken." -hardWordMute: "Harde woorddemping" -showMutedWord: "Gedempte woorden weergeven" -hardWordMuteDescription: "Verbert notities die het gespecificeerde woord of zin bevatten. In tegenstelling tot woorddemping wordt de notitie volledig verborgen." -regexpError: "Fout in reguliere expressie" -regexpErrorDescription: "Er is een fout opgetreden in de reguliere expressie op regel {line} van uw {tab} woord dempen:" -instanceMute: "Instantie dempers" -userSaysSomething: "{name} zei iets" -userSaysSomethingAbout: "{name} zei iets over '{word}'" -makeActive: "Activeren" -display: "Weergave" -copy: "Kopiëren" -copiedToClipboard: "Naar het klembord gekopieerd" -metrics: "Metrieken" -overview: "Overzicht" -logs: "Protocollen" -delayed: "Vertraagd" -database: "Database" -channel: "Kanalen" -create: "Creëer" -notificationSetting: "Instellingen meldingen" -notificationSettingDesc: "Selecteer het type meldingen dat moet worden weergegeven." -useGlobalSetting: "Globale instelling gebruiken" -useGlobalSettingDesc: "Als deze optie is ingeschakeld, worden de meldingsinstellingen van je account gebruikt. Als deze optie uitgeschakeld is, kunnen individuele configuraties worden gemaakt." -other: "Ander" -regenerateLoginToken: "Login token opnieuw genereren" -regenerateLoginTokenDescription: "Regenereren van het token dat intern wordt gebruikt om in te loggen. Dit is normaal gezien niet nodig. Alle apparaten worden afgemeld tijdens het regenereren." -theKeywordWhenSearchingForCustomEmoji: "Dit is het keyword dat gebruikt wordt bij het zoeken naar eigen emojis." -setMultipleBySeparatingWithSpace: "Scheid elementen met een spatie om meerdere instellingen te configureren." -fileIdOrUrl: "Bestands-ID of URL" -behavior: "Gedrag" -sample: "Voorbeeld" -abuseReports: "Meldt" -reportAbuse: "Meld" -reportAbuseRenote: "Meld renote" -reportAbuseOf: "Meld {name}" -fillAbuseReportDescription: "Vul s.v.p. de details in over deze melding. Geef, als het over een specifieke notitie gaat, ook de URL op." -abuseReported: "Uw rapport is verzonden. Hartelijk dank." -reporter: "Verslaggever" -reporteeOrigin: "Oorsprong van de gemelde persoon" -reporterOrigin: "Verslaggever Oorsprong" -send: "Stuur" -openInNewTab: "In nieuw tabblad openen" -openInSideView: "In zijaanzicht openen" -defaultNavigationBehaviour: "Standaard navigatie gedrag" -editTheseSettingsMayBreakAccount: "Het wijzigen van deze instellingen kan je account beschadigen." -instanceTicker: "Instantie-informatie van notities" -waitingFor: "Wachten op {x}" -random: "Willekeurig" -system: "Systeem" -switchUi: "UI omschakelen" -desktop: "Desktop" -clip: "Clip aanmaken" -createNew: "Nieuwe aanmaken" -optional: "Optioneel" -createNewClip: "Nieuwe clip aanmaken" -unclip: "Van clip verwijderen" -confirmToUnclipAlreadyClippedNote: "Deze notitie is al toegevoegd aan de clip “{name}”. Wil je deze uit deze clip verwijderen?" -public: "Openbare" -private: "Privé" -i18nInfo: "Misskey wordt in veel verschillende talen vertaald door vrijwilligers. Je kunt helpen op {link}" -manageAccessTokens: "Toegangstokens beheren" -accountInfo: "Informatie over gebruikersaccount" -notesCount: "Aantal notities" -repliesCount: "Aantal verzonden replies" -renotesCount: "Aantal verzonden renotes" -repliedCount: "Aantal ontvangen replies" -renotedCount: "Aantal ontvangen renotes" -followingCount: "Aantal gevolgde accounts" -followersCount: "Aantal volgers" -sentReactionsCount: "Aantal verzonden reacties" -receivedReactionsCount: "Aantal ontvangen reacties" -pollVotesCount: "Aantal verzonden peiling stemmen" -pollVotedCount: "Aantal ontvangen peiling stemmen" -yes: "Ja" -no: "Nee" -driveFilesCount: "Aantal bestanden in station" -driveUsage: "Schijfruimtegebruik" -noCrawle: "Crawler-indexering verwerpen" -noCrawleDescription: "Vraag zoekmachines om je eigen profielpagina, notities, pagina's, enz. niet te indexeren." -lockedAccountInfo: "Tenzij je de zichtbaarheid van je notities instelt op “Alleen volgers”, zijn je notities zichtbaar voor iedereen, zelfs als je vereist dat volgers handmatig worden goedgekeurd." -alwaysMarkSensitive: "Markeer media standaard als gevoelig" -loadRawImages: "Toon altijd originele afbeeldingen in plaats van miniaturen" -disableShowingAnimatedImages: "Speel geen geanimeerde afbeeldingen af" -highlightSensitiveMedia: "Markeer gevoelige media" -verificationEmailSent: "Er is een bevestigingsmail naar uw e-mailadres verzonden. Ga naar de link in de e-mail om het verificatieproces te voltooien." -notSet: "Niet geconfigureerd" -emailVerified: "Emailadres bevestigd" -noteFavoritesCount: "Aantal notities gemarkeerd als favoriet" -pageLikesCount: "Aantal gelikete pagina's" -pageLikedCount: "Aantal ontvangen pagina-likes" -contact: "Contact" -useSystemFont: "Het standaardlettertype van het systeem gebruiken" -clips: "Clips" -experimentalFeatures: "Experimentele functionaliteiten" -experimental: "Experimentele" -thisIsExperimentalFeature: "Dit is een experimentele functie. De functionaliteit kan worden gewijzigd en werkt mogelijk niet zoals bedoeld." -developer: "Ontwikkelaar" -makeExplorable: "Gebruikersaccount zichtbaar maken in “Verkennen”" -makeExplorableDescription: "Als deze optie is uitgeschakeld, is uw gebruikersaccount niet zichtbaar in het gedeelte “Verkennen”." -duplicate: "Dupliceren" -left: "Links" -center: "Center" -wide: "Breed" -narrow: "Smal" -reloadToApplySetting: "Deze instelling gaat pas in nadat de pagina herladen is. Nu herladen?" -needReloadToApply: "Deze instelling wordt van kracht nadat de pagina is vernieuwd." -showTitlebar: "Titelbalk weergeven" clearCache: "Cache opschonen" -onlineUsersCount: "{n} Gebruikers zijn online" -nUsers: "{n} Gebruikers" -nNotes: "{n} Notities" -sendErrorReports: "Foutrapporten sturen" -sendErrorReportsDescription: "Als u deze optie inschakelt, wordt gedetailleerde foutinformatie met Misskey gedeeld wanneer zich een probleem voordoet. Dit helpt de kwaliteit van Misskey te verbeteren.\nDit omvat informatie zoals de versie van uw OS, welke browser u gebruikt, uw activiteit in Misskey, enz." -myTheme: "Mijn thema" -backgroundColor: "Achtergrondkleur" -accentColor: "Accentkleur" -textColor: "Tekstkleur" -saveAs: "Opslaan als…" -advanced: "Geavanceerd" -advancedSettings: "Geavanceerde instellingen" -value: "Waarde" -createdAt: "Aangemaakt at" -updatedAt: "Laatst gewijzigd at" -saveConfirm: "Wijzigingen opslaan?" -deleteConfirm: "Echt verwijderen?" -invalidValue: "Ongeldige waarde." -registry: "Registry" -closeAccount: "Gebruikersaccount sluiten" -currentVersion: "Huidige versie" -latestVersion: "Nieuwste versie" -youAreRunningUpToDateClient: "Je gebruikt de nieuwste versie van je client." -newVersionOfClientAvailable: "Er is een nieuwere versie van je client beschikbaar." -usageAmount: "Gebruik" -capacity: "Capaciteit" -inUse: "Gebruikt" -editCode: "Code bewerken" -apply: "Toepassen" -receiveAnnouncementFromInstance: "Meldingen ontvangen van deze instantie" -emailNotification: "E-mailmeldingen" -publish: "Publiceren" -inChannelSearch: "In kanaal zoeken" -useReactionPickerForContextMenu: "Open reactieselectie door rechts te klikken" -typingUsers: "{users} is/zijn aan het schrijven..." -jumpToSpecifiedDate: "Naar een specifieke datum springen" -showingPastTimeline: "Momenteel wordt een oude tijdlijn weergeven" -clear: "Terugkeren" -markAllAsRead: "Alles als gelezen markeren" -goBack: "Terug" -unlikeConfirm: "Wil je echt je like verwijderen?" -fullView: "Volledig zicht" -quitFullView: "Volledig zicht verlaten" -addDescription: "Beschrijving toevoegen" -userPagePinTip: "Je kunt hier notities tonen door “Vastmaken aan profiel” te selecteren in het menu van de individuele notities." -notSpecifiedMentionWarning: "Deze notitie bevat verwijzingen naar gebruikers die niet zijn geselecteerd als ontvangers" info: "Over" -userInfo: "Gebruikersinformatie" -unknown: "Onbekend" -onlineStatus: "Online status" -hideOnlineStatus: "Online status verbergen" -hideOnlineStatusDescription: "Het verbergen van je online status vermindert het nut van functies zoals zoeken." -online: "Online" -active: "Actief" -offline: "Offline" -notRecommended: "Niet aanbevolen" -botProtection: "Beveiliging tegen bots" -instanceBlocking: "Geblokkeerde/gedempte Instanties" -selectAccount: "Gebruikersaccount selecteren" -switchAccount: "Account wisselen" -enabled: "Ingeschakeld" -disabled: "Uitgeschakeld" -quickAction: "Snelle acties" user: "Gebruikers" -administration: "Beheer" -accounts: "Gebruikersaccounts" -switch: "Wissel" -noMaintainerInformationWarning: "Operatorinformatie is niet geconfigureerd." -noInquiryUrlWarning: "Contact-URL niet opgegeven" -noBotProtectionWarning: "Bescherming tegen bots is niet geconfigureerd." -configure: "Configureer" -postToGallery: "Nieuw galerijbericht maken" -postToHashtag: "Post naar deze hashtag" -gallery: "Galerij" -recentPosts: "Recente berichten" -popularPosts: "Populair berichten" -shareWithNote: "Delen met notitie" -ads: "Advertenties" -expiration: "Deadline" -startingperiod: "Start" -memo: "Memo" -priority: "Prioriteit" -high: "Hoge" -middle: "Medium" -low: "Lage" -emailNotConfiguredWarning: "E-mailadres niet ingesteld." -ratio: "Verhouding" -previewNoteText: "Show voorproefje" -customCss: "Aangepaste CSS" -customCssWarn: "Gebruik deze instelling alleen als je weet wat het doet. Ongeldige invoer kan ertoe leiden dat de client niet meer normaal functioneert." -global: "Globaal" -squareAvatars: "Toon profielfoto's as vierkant" -sent: "Verzonden" -received: "Ontvangen" -searchResult: "Zoekresultaten" -hashtags: "Hashtags" -troubleshooting: "Probleemoplossing" -useBlurEffect: "Vervagingseffecten in de UI gebruike" -learnMore: "Meer leren" -misskeyUpdated: "Misskey is bijgewerkt!" -whatIsNew: "Wijzigingen tonen" -translate: "Vertalen" -translatedFrom: "Vertaald uit {x}" -accountDeletionInProgress: "De verwijdering van je gebruikersaccount wordt momenteel verwerkt." -usernameInfo: "Een naam die kan worden gebruikt om je gebruikersaccount op deze server te identificeren. Je kunt het alfabet (a~z, A~Z), cijfers (0~9) of underscores (_) gebruiken. Gebruikersnamen kunnen later niet worden gewijzigd." -aiChanMode: "Ai Mode" -devMode: "Ontwikkelaar modus" -keepCw: "Inhoudswaarschuwingen behouden" -pubSub: "Pub/Sub Gebruikersaccounts" -lastCommunication: "Laatste communicatie" -resolved: "Opgelost" -unresolved: "Onopgelost" -breakFollow: "Volger verwijderen" -breakFollowConfirm: "Deze volger echt weghalen?" -itsOn: "Ingeschakeld" -itsOff: "Uitgeschakeld" -on: "Op" -off: "Uit" -emailRequiredForSignup: "Vereist e-mailadres voor aanmelding" -unread: "Ongelezen" -filter: "Filter" -controlPanel: "Controlepaneel" -manageAccounts: "Gebruikersaccounts beheren" -makeReactionsPublic: "Reactiegeschiedenis publiceren" -makeReactionsPublicDescription: "Hierdoor wordt de lijst met al je eerdere reacties openbaar." -classic: "Classic" muteThread: "Discussies dempen " unmuteThread: "Dempen van discussie ongedaan maken" -followingVisibility: "Zichtbaarheid van gevolgden" -followersVisibility: "Zichtbaarheid van volgers" -continueThread: "Bekijk draad voortzetting" -deleteAccountConfirm: "Je gebruikersaccount wordt onherroepelijk verwijderd. Wil je nog steeds doorgaan?" -incorrectPassword: "Onjuist wachtwoord." -incorrectTotp: "Het eenmalige wachtwoord is incorrect of verlopen" -voteConfirm: "Bevestig je je stem op “{choice}”?" hide: "Verbergen" -useDrawerReactionPickerForMobile: "Toon reactiekiezer als lade op mobiel" -welcomeBackWithName: "Welkom terug, {name}" -clickToFinishEmailVerification: "Druk op [{ok}] om de e-mailbevestiging af te ronden." searchByGoogle: "Zoeken" -threeMonths: "3 maanden" -oneYear: "1 jaar" -threeDays: "3 dagen" cropImage: "Afbeelding bijsnijden" cropImageAsk: "Bijsnijdengevraagd" file: "Bestanden" -account: "Gebruikersaccounts" pushNotification: "Pushberichten" subscribePushNotification: "Push meldingen inschakelen" unsubscribePushNotification: "Pushberichten uitschakelen" @@ -946,62 +418,17 @@ pushNotificationAlreadySubscribed: "Pushberichtrn al ingeschakeld" windowMaximize: "Maximaliseren" windowRestore: "Herstellen" loggedInAsBot: "Momenteel als bot ingelogd" -show: "Weergave" -correspondingSourceIsAvailable: "De bijbehorende broncode is beschikbaar bij {anchor}" -invalidParamErrorDescription: "De aanvraagparameters zijn ongeldig. Dit komt meestal door een bug, maar kan ook omdat de invoer te lang is of iets dergelijks." -collapseRenotes: "Renotes die je al gezien hebt, inklappen" -collapseRenotesDescription: "Klapt notities in waar je al op gereageerd hebt of die je al gerenotet hebt." -prohibitedWords: "Verboden woorden" -prohibitedWordsDescription: "Activeert een foutmelding als er geprobeerd wordt een notitie met de ingestelde woorden te plaatsen. Meerdere woorden kunnen worden ingesteld, elk op hun eigen regel." -hiddenTags: "Verborgen hashtags" -hiddenTagsDescription: "Selecteer tags die niet worden weergegeven in de trends. Meerdere tags kunnen worden geregistreerd, elk op hun eigen regel." -enableStatsForFederatedInstances: "Statistieken van remote servers ontvangen" -limitWidthOfReaction: "Limiteert de maximale breedte van reacties en geef ze verkleind weer" -audio: "Audio" -audioFiles: "Audio" -archived: "Gearchiveerd" -unarchive: "Dearchiveren" -lookupConfirm: "Weet je zeker dat je dit wil opzoeken?" -openTagPageConfirm: "Wil je deze hashtagpagina openen?" -specifyHost: "Specificeer host" -icon: "Avatar" -replies: "Antwoorden" -renotes: "Herdelen" -followingOrFollower: "Gevolgd of volger" -confirmShowRepliesAll: "Dit is een onomkeerbare operatie. Weet je zeker dat reacties op anderen van iedereen die je volgt, wil weergeven in je tijdlijn?" -information: "Over" -_chat: - invitations: "Uitnodigen" - noHistory: "Geen geschiedenis gevonden" - members: "Leden" - home: "Startpagina" - send: "Stuur" -_delivery: - stop: "Opgeschort" - _type: - none: "Publiceren" -_role: - priority: "Prioriteit" - _priority: - low: "Lage" - middle: "Medium" - high: "Hoge" -_ffVisibility: - public: "Publiceren" -_ad: - back: "Terug" _email: _follow: title: "volgde jou" _theme: - description: "Beschrijving" keys: mention: "Vermelding" renote: "Herdelen" - divider: "Scheider" _sfx: note: "Notities" notification: "Meldingen" + chat: "Chat" _2fa: renewTOTPCancel: "Nee, bedankt" _widgets: @@ -1023,7 +450,6 @@ _profile: name: "Naam" username: "Gebruikersnaam" _exportOrImport: - clips: "Clip aanmaken" followingList: "Volgend" muteList: "Dempen" blockingList: "Blokkeren" @@ -1034,9 +460,6 @@ _charts: federation: "Federatie" _timelines: home: "Startpagina" -_play: - script: "Script" - summary: "Beschrijving" _pages: blocks: image: "Afbeeldingen" @@ -1049,7 +472,6 @@ _notification: renote: "Herdelen" quote: "Quote" reaction: "Reacties" - login: "Inloggen" _actions: reply: "Antwoord" renote: "Herdelen" @@ -1059,22 +481,6 @@ _deck: tl: "Tijdlijn" antenna: "Antennes" list: "Lijsten" - channel: "Kanalen" mentions: "Vermeldingen" _webhookSettings: name: "Naam" - active: "Ingeschakeld" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" -_moderationLogTypes: - suspend: "Opschorten" - resetPassword: "Wachtwoord terugzetten" -_reversi: - total: "Totaal" -_remoteLookupErrors: - _noSuchObject: - title: "Niet gevonden" -_search: - searchScopeAll: "Alle" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 578183efa5..ec2900527b 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -102,6 +102,7 @@ clickToShow: "Klikk for å vise" add: "Legg til" reaction: "Reaksjon" reactions: "Reaksjoner" +reactionSetting: "Reaksjoner som vises i reaksjonsvelgeren" reactionSettingDescription2: "Dra for å endre rekkefølgen, klikk for å slette, trykk \"+\" for å legge til." rememberNoteVisibility: "Husk innstillingene for synlighet av Notes" attachCancel: "Fjern vedlegg" @@ -171,6 +172,7 @@ noUsers: "Det er ingen brukere" editProfile: "Rediger profil" noteDeleteConfirm: "Er du sikker på at du vil slette denne Noten?" pinLimitExceeded: "Du kan ikke feste flere." +intro: "Installasjonen av Misskey er ferdig! Vennligst opprett en administratorkonto." done: "Ferdig" default: "Standard" defaultValueIs: "Standard: {value}" @@ -259,6 +261,7 @@ enableLocalTimeline: "Aktiver lokal tidslinje" enableGlobalTimeline: "Aktiver global tidslinje" disablingTimelinesInfo: "Administratorer og Moderatorer vil alltid ha tilgang til alle tidslinjer, selv om de ikke er aktivert." registration: "Registrer" +enableRegistration: "Aktiver registrering av nye brukere" invite: "Inviter" basicInfo: "Grunnleggende informasjon" pinnedUsers: "Festede brukrere" @@ -298,6 +301,8 @@ text: "Tekst" next: "Neste" retype: "Gjenta" quoteAttached: "Sitat" +noMessagesYet: "Ingen meldinger ennå" +newMessageExists: "Det er nye meldinger" onlyOneFileCanBeAttached: "Du kan bare legge ved én fil i en melding" invitations: "Inviter" available: "Tilgjengelig" @@ -456,18 +461,6 @@ videos: "Videoer" continue: "Fortsett" youFollowing: "Følger" options: "Alternativ" -icon: "Avatar" -replies: "Svar" -renotes: "Renote" -surrender: "Avbryt" -information: "Informasjon" -_chat: - invitations: "Inviter" - members: "Medlemmer" - home: "Hjem" - send: "Send" -_delivery: - stop: "Suspendert" _initialAccountSetting: theseSettingsCanEditLater: "Du kan endre disse innstillingene senere." _achievements: @@ -579,6 +572,9 @@ _channel: nameAndDescription: "Navn og beskrivelse" _menuDisplay: hide: "Skjul" +_wordMute: + soft: "Myk" + hard: "Hard" _theme: description: "Beskrivelse" color: "Farge" @@ -605,6 +601,9 @@ _time: minute: "Minutter" hour: "Timer" day: "Dager" +_timelineTutorial: + title: "Hvordan bruke Misskey" + step2_2: "Hva med å skrive en selvpresentasjon, eller bare \"Hei {name}!\" hvis du ikke har lyst?" _2fa: renewTOTPCancel: "Avbryt" _weekday: @@ -703,7 +702,6 @@ _notification: renote: "Renotes" quote: "Sitater" reaction: "Reaksjoner" - login: "Logg inn" _actions: reply: "Svar" renote: "Renote" @@ -724,14 +722,3 @@ _deck: direct: "Direkte" _webhookSettings: name: "Navn" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "E-post" -_moderationLogTypes: - suspend: "Suspender" -_remoteLookupErrors: - _noSuchObject: - title: "Ikke funnet" -_search: - searchScopeAll: "Alle" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 9e98e158bd..8b6b4be7d0 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -20,7 +20,6 @@ noNotes: "Brak wpisów" noNotifications: "Brak powiadomień" instance: "Instancja" settings: "Ustawienia" -notificationSettings: "Powiadomienia" basicSettings: "Podstawowe ustawienia" otherSettings: "Pozostałe ustawienia" openInWindow: "Otwórz w oknie" @@ -45,20 +44,13 @@ pin: "Przypnij do profilu" unpin: "Odepnij z profilu" copyContent: "Skopiuj zawartość" copyLink: "Skopiuj odnośnik" -copyLinkRenote: "Skopiuj link renote'a" delete: "Usuń" deleteAndEdit: "Usuń i edytuj" deleteAndEditConfirm: "Czy na pewno chcesz usunąć ten wpis i zedytować go? Utracisz wszystkie reakcje, udostępnienia i odpowiedzi do tego wpisu." addToList: "Dodaj do listy" -addToAntenna: "Dodaj do anteny" sendMessage: "Wyślij wiadomość" copyRSS: "Kopiuj RSS" copyUsername: "Kopiuj nazwę użytkownika" -copyUserId: "Kopiuj ID użytkownika" -copyNoteId: "Kopiuj ID notatki" -copyFileId: "Kopiuj ID pliku" -copyFolderId: "Kopiuj ID folderu" -copyProfileUrl: "Kopiuj URL profilu" searchUser: "Wyszukiwanie użytkowników" reply: "Odpowiedz" loadMore: "Załaduj więcej" @@ -111,8 +103,6 @@ renoted: "Udostępniono." cantRenote: "Ten wpis nie może zostać udostępniony." cantReRenote: "Udostępnienie nie może zostać udostępnione." quote: "Cytuj" -inChannelRenote: "Renote tylko na kanale" -inChannelQuote: "Cytat tylko na kanale" pinnedNote: "Przypięty wpis" pinned: "Przypnij do profilu" you: "Ty" @@ -121,23 +111,15 @@ sensitive: "NSFW" add: "Dodaj" reaction: "Reakcja" reactions: "Reakcja" -emojiPicker: "Selektor Emoji" -pinnedEmojisForReactionSettingDescription: "Ustaw emotikony które powinny być przypięte i od razu wyświetlone podczas reagowania." -pinnedEmojisSettingDescription: "Ustaw emotikony które powinny być przypięte i wyświetlone podczas przeglądania selektora Emoji" -emojiPickerDisplay: "Wyświetlanie selektora Emoji" -overwriteFromPinnedEmojisForReaction: "Zastąp z ustawień reakcji" -overwriteFromPinnedEmojis: "Zastąp z ogólnych ustawień" +reactionSetting: "Reakcje do pokazania w wyborniku reakcji" reactionSettingDescription2: "Przeciągnij aby zmienić kolejność, naciśnij aby usunąć, naciśnij „+” aby dodać" rememberNoteVisibility: "Zapamiętuj ustawienia widoczności wpisu" attachCancel: "Usuń załącznik" -deleteFile: "Usuń plik" markAsSensitive: "Oznacz jako NSFW" unmarkAsSensitive: "Cofnij NSFW" enterFileName: "Wprowadź nazwę pliku" mute: "Wycisz" unmute: "Cofnij wyciszenie" -renoteMute: "Wycisz renote'y" -renoteUnmute: "Wyłącz wyciszenie renote'ów" block: "Zablokuj" unblock: "Odblokuj" suspend: "Zawieś" @@ -147,10 +129,8 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?" suspendConfirm: "Czy na pewno chcesz zawiesić to konto?" unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?" selectList: "Wybierz listę" -editList: "Edytuj listę" selectChannel: "Wybierz kanał" selectAntenna: "Wybierz Antennę" -editAntenna: "Edytuj antenę" selectWidget: "Wybierz widżet" editWidgets: "Edytuj widżety" editWidgetsExit: "Gotowe" @@ -163,15 +143,11 @@ addEmoji: "Dodaj emoji" settingGuide: "Proponowana konfiguracja" cacheRemoteFiles: "Przechowuj zdalne pliki w pamięci podręcznej" cacheRemoteFilesDescription: "Gdy ta opcja jest wyłączona, zdalne pliki są ładowane bezpośrednio ze zdalnych instancji. Wyłączenie the opcji zmniejszy użycie powierzchni dyskowej, ale zwiększy transfer, ponieważ miniaturki nie będą generowane." -youCanCleanRemoteFilesCache: "Możesz wyczyścić cache poprzez kliknięcie przycisku 🗑️ w widoku menedżera plików." -cacheRemoteSensitiveFiles: "Przechowuj wrażliwe zdalne pliki w pamięci podręcznej" -cacheRemoteSensitiveFilesDescription: "Gdy ta opcja jest wyłączona, wrażliwe pliki zdalne są wczytywane bezpośrednio ze zdalnej instancji bez cacheowania." flagAsBot: "To konto jest botem" flagAsBotDescription: "Jeżeli ten kanał jest kontrolowany przez jakiś program, ustaw tę opcję. Jeżeli włączona, będzie działać jako flaga informująca innych programistów, aby zapobiegać nieskończonej interakcji z różnymi botami i dostosowywać wewnętrzne systemy Misskey, traktując konto jako bota." flagAsCat: "To konto jest kotem" flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot." flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu" -flagShowTimelineRepliesDescription: "Gdy włączone, pokazuje odpowiedzi użytkowników na notatki innych użytkowników w osi czasu." autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz" addAccount: "Dodaj konto" reloadAccountsList: "Odśwież listę kont" @@ -201,7 +177,6 @@ perHour: "co godzinę" perDay: "co dzień" stopActivityDelivery: "Przestań przesyłać aktywności" blockThisInstance: "Zablokuj tę instancję" -silenceThisInstance: "Wycisz tę instancję" operations: "Działania" software: "Oprogramowanie" version: "Wersja" @@ -221,8 +196,6 @@ clearCachedFiles: "Wyczyść pamięć podręczną" clearCachedFilesConfirm: "Czy na pewno chcesz usunąć wszystkie zdalne pliki z pamięci podręcznej?" blockedInstances: "Zablokowane instancje" blockedInstancesDescription: "Wypisz nazwy hostów instancji, które powinny zostać zablokowane. Wypisane instancje nie będą mogły dłużej komunikować się z tą instancją." -silencedInstances: "Wyciszone instancje" -silencedInstancesDescription: "Wypisz nazwy hostów instancji, które chcesz wyciszyć. Wszystkie konta wymienionych instancji będą traktowane jako wyciszone, będą mogły jedynie wysyłać prośby o obserwację i nie będą mogły wspominać kont lokalnych, jeśli nie będą obserwowane. Nie będzie to miało wpływu na zablokowane instancje." muteAndBlock: "Wycisz / Zablokuj" mutedUsers: "Wyciszeni użytkownicy" blockedUsers: "Zablokowani użytkownicy" @@ -230,6 +203,7 @@ noUsers: "Brak użytkowników" editProfile: "Edytuj profil" noteDeleteConfirm: "Czy na pewno chcesz usunąć ten wpis?" pinLimitExceeded: "Nie możesz przypiąć więcej wpisów." +intro: "Zakończono instalację Misskey! Utwórz konto administratora." done: "Gotowe" processing: "Przetwarzanie" preview: "Podgląd" @@ -266,11 +240,10 @@ removed: "Pomyślnie usunięto" removeAreYouSure: "Czy na pewno chcesz usunąć „{x}”?" deleteAreYouSure: "Czy na pewno chcesz usunąć „{x}”?" resetAreYouSure: "Czy na pewno chcesz zresetować?" -areYouSure: "Na pewno?" saved: "Zapisano" +messaging: "Wiadomości" upload: "Wyślij" keepOriginalUploading: "Zachowaj oryginalny obraz" -keepOriginalUploadingDescription: "Zapisuje oryginalnie przesłany obraz w niezmienionej postaci. Jeśli ta opcja jest wyłączona, po przesłaniu zostanie wygenerowana wersja do wyświetlenia w Internecie." fromDrive: "Z dysku" fromUrl: "Z adresu URL" uploadFromUrl: "Wyślij z adresu URL" @@ -280,12 +253,10 @@ uploadFromUrlMayTakeTime: "Wysyłanie może chwilę potrwać." explore: "Eksploruj" messageRead: "Przeczytano" noMoreHistory: "Nie ma dalszej historii" +startMessaging: "Rozpocznij czat" nUsersRead: "przeczytano przez {n}" agreeTo: "Wyrażam zgodę na {0}" -agree: "Zatwierdź" agreeBelow: "Zaakceptuj poniżej" -basicNotesBeforeCreateAccount: "Ważne notatki" -termsOfService: "Warunki usługi" start: "Rozpocznij" home: "Strona główna" remoteUserCaution: "Te informacje mogą nie być aktualne, ponieważ użytkownik pochodzi ze zdalnej instancji." @@ -315,7 +286,6 @@ folderName: "Nazwa katalogu" createFolder: "Utwórz katalog" renameFolder: "Zmień nazwę katalogu" deleteFolder: "Usuń ten katalog" -folder: "Folder" addFile: "Dodaj plik" emptyDrive: "Dysk jest pusty" emptyFolder: "Ten katalog jest pusty" @@ -329,7 +299,6 @@ copyUrl: "Skopiuj adres URL" rename: "Zmień nazwę" avatar: "Awatar" banner: "Baner" -displayOfSensitiveMedia: "Wyświetlanie wrażliwej zawartości" whenServerDisconnected: "Po utracie połączenia z serwerem" disconnectedFromServer: "Utracono połączenie z serwerem." reload: "Odśwież" @@ -359,10 +328,12 @@ enableLocalTimeline: "Włącz lokalną oś czasu" enableGlobalTimeline: "Włącz globalną oś czasu" disablingTimelinesInfo: "Administratorzy i moderatorzy będą zawsze mieć dostęp do wszystkich osi czasu, nawet gdy są one wyłączone." registration: "Zarejestruj się" +enableRegistration: "Włącz rejestrację nowych użytkowników" invite: "Zaproś" driveCapacityPerLocalAccount: "Powierzchnia dyskowa na lokalnego użytkownika" driveCapacityPerRemoteAccount: "Powierzchnia dyskowa na zdalnego użytkownika" inMb: "W megabajtach" +iconUrl: "Adres URL ikony" bannerUrl: "Adres URL banera" backgroundImageUrl: "Adres URL tła" basicInfo: "Podstawowe informacje" @@ -376,11 +347,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Włącz hCaptcha" hcaptchaSiteKey: "Klucz strony" hcaptchaSecretKey: "Tajny klucz" -mcaptcha: "mCaptcha" -enableMcaptcha: "Włącz mCaptcha" -mcaptchaSiteKey: "Klucz strony" -mcaptchaSecretKey: "Tajny klucz" -mcaptchaInstanceUrl: "URL instancji mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Włącz reCAPTCHA" recaptchaSiteKey: "Klucz strony" @@ -423,19 +389,15 @@ aboutMisskey: "O Misskey" administrator: "Admin" token: "Token" 2fa: "Klucz 2FA " -setupOf2fa: "Skonfiguruj dwuetapową autentykację" totp: "Klucz aplikacji uwierzytelniającej (totp)" totpDescription: "Opis klucza czasowego" moderator: "Moderator" moderation: "Moderacja" -moderationNote: "Notka moderacyjna" -addModerationNote: "Dodaj notkę moderacyjną" -moderationLogs: "Logi moderacyjne" nUsersMentioned: "{n} wspomnianych użytkowników" securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey" securityKey: "Klucz bezpieczeństwa" lastUsed: "Ostatnio używane" -lastUsedAt: "Ostatnio używane: {t}" +lastUsedAt: "Ostatnio używane w" unregister: "Cofnij rejestrację" passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła" passwordLessLoginDescription: "Opis logowania bez użycia hasła" @@ -446,6 +408,7 @@ share: "Udostępnij" notFound: "Nie znaleziono" notFoundDescription: "Nie ma strony odpowiadającej określonemu adresowi URL." uploadFolder: "Domyślne położenie wysłanych" +cacheClear: "Wyczyść pamięć podręczną" markAsReadAllNotifications: "Oznacz wszystkie powiadomienia jako przeczytane" markAsReadAllUnreadNotes: "Oznacz wszystkie wpisy jako przeczytane" markAsReadAllTalkMessages: "Oznacz wszystkie wiadomości jako przeczytane" @@ -463,6 +426,8 @@ retype: "Wprowadź ponownie" noteOf: "Wpisy {user}" quoteAttached: "Zacytowano" quoteQuestion: "Czy na pewno chcesz umieścić cytat?" +noMessagesYet: "Nie napisano jeszcze wiadomości" +newMessageExists: "Masz nową wiadomość" onlyOneFileCanBeAttached: "Możesz załączyć tylko jeden plik do wiadomości" signinRequired: "Proszę się zalogować" invitations: "Zaproś" @@ -486,16 +451,9 @@ uiLanguage: "Język wyświetlania UI" aboutX: "O {x}" emojiStyle: "Styl emoji" native: "Natywny" -menuStyle: "Styl Menu" -style: "Styl" -drawer: "Schowek" -popup: "Wyskakujące okienka" -showNoteActionsOnlyHover: "Pokazuj akcje notatek tylko po najechaniu myszką" -showReactionsCount: "Wyświetl liczbę reakcji na notatkę" +disableDrawer: "Nie używaj menu w stylu szuflady" noHistory: "Brak historii" signinHistory: "Historia logowania" -enableAdvancedMfm: "Włącz zaawansowane MFM" -enableAnimatedMfm: "Włącz animowane MFM" doing: "Przetwarzanie..." category: "Kategoria" tags: "Tagi" @@ -504,8 +462,6 @@ createAccount: "Utwórz konto" existingAccount: "Istniejące konto" regenerate: "Wygeneruj ponownie" fontSize: "Rozmiar czcionki" -mediaListWithOneImageAppearance: "Wysokość list multimediów z tylko jednym obrazem" -limitTo: "Limituj do {x}" noFollowRequests: "Nie masz żadnych oczekujących próśb o możliwość obserwacji" openImageInNewTab: "Otwórz obraz w nowej karcie" dashboard: "Kokpit" @@ -525,7 +481,6 @@ showFeaturedNotesInTimeline: "Pokazuj wyróżnione wpisy w osi czasu" objectStorage: "Pamięć obiektowa" useObjectStorage: "Używaj pamięci obiektowej" objectStorageBaseUrl: "Podstawowy URL" -objectStorageBaseUrlDesc: "Adres URL używany jako odniesienie. Podaj adres URL swojego CDN lub Proxy, gdy używasz któregokolwiek z nich.\nDla S3 użyj 'https://.s3.amazonaws.com' a dla GCS lub równej usługi użyj 'https://storage.googleapis.com/', itd." objectStorageBucket: "Bucket" objectStorageBucketDesc: "Podaj nazwę „wiadra” używaną przez konfigurowaną usługę." objectStoragePrefix: "Prefiks" @@ -538,13 +493,9 @@ objectStorageUseSSL: "Użyj SSL" objectStorageUseSSLDesc: "Wyłącz, jeżeli nie zamierzasz używać HTTPS dla połączenia z API" objectStorageUseProxy: "Połącz przez proxy" objectStorageUseProxyDesc: "Wyłącz, jeżeli nie zamierzasz używać proxy dla połączenia z pamięcią blokową" -objectStorageSetPublicRead: "Ustaw opcję \"public-read\" przy przesyłaniu" -s3ForcePathStyleDesc: "Jeśli opcja s3ForcePathStyle jest włączona, nazwa Bucket'u musi być zawarta w ścieżce adresu URL, a nie w nazwie hosta adresu URL. Włączenie tego ustawienia może być konieczne w przypadku użycia usług takich jak self-hosted instancja Minio." serverLogs: "Dziennik zdarzeń" deleteAll: "Usuń wszystkie" showFixedPostForm: "Wyświetlaj formularz tworzenia wpisu w górnej części osi czasu" -showFixedPostFormInChannel: "Wyświetl formularz postowania w górnej części osi czasu (Kanały)" -withRepliesByDefaultForNewlyFollowed: "Domyślnie uwzględnij odpowiedzi nowo obserwowanych użytkowników w osi czasu" newNoteRecived: "Masz nowy wpis" sounds: "Dźwięk" sound: "Dźwięki" @@ -554,8 +505,6 @@ showInPage: "Pokaż na stronie" popout: "Popout" volume: "Głośność" masterVolume: "Głośność główna" -notUseSound: "Wyłącz dźwięk" -useSoundOnlyWhenActive: "Puszczaj dźwięki tylko, gdy Misskey jest aktywne." details: "Szczegóły" chooseEmoji: "Wybierz emoji" unableToProcess: "Nie udało się dokończyć działania." @@ -572,15 +521,10 @@ ascendingOrder: "Rosnąco" descendingOrder: "Malejąco" scratchpad: "Brudnopis" scratchpadDescription: "Brudnopis zawiera eksperymentalne środowisko dla AiScript. Możesz pisać, wykonywać i sprawdzać wyniki w interakcji z Misskey." -uiInspector: "Inspektor UI" output: "Wyjście" script: "Skrypt" disablePagesScript: "Wyłącz AiScript na Stronach" updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku" -unsetUserAvatar: "Usuń awatar" -unsetUserAvatarConfirm: "Czy na pewno chcesz usunąć awatar tego użytkownika?" -unsetUserBanner: "Usuń baner" -unsetUserBannerConfirm: "Czy na pewno chcesz usunąć baner?" deleteAllFiles: "Usuń wszystkie pliki" deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?" removeAllFollowing: "Przestań obserwować" @@ -596,7 +540,6 @@ accountDeletedDescription: "Opis konta usuniętego" menu: "Menu" divider: "Rozdzielacz" addItem: "Dodaj element" -rearrange: "Posortuj" relays: "Przekaźniki" addRelay: "Dodaj przekaźnik" inboxUrl: "Adres URL skrzynki nadawczej" @@ -631,7 +574,6 @@ medium: "Średnie" small: "Małe" generateAccessToken: "Generuj token dostępu" permission: "Uprawnienia" -adminPermission: "Uprawnienia administracyjne" enableAll: "Włącz wszystko" disableAll: "Wyłącz wszystko" tokenRequested: "Przydziel dostęp do konta" @@ -649,13 +591,9 @@ smtpPort: "Port" smtpUser: "Nazwa użytkownika" smtpPass: "Hasło" emptyToDisableSmtpAuth: "Pozostaw adres e-mail i hasło puste, aby wyłączyć weryfikację SMTP" -smtpSecure: "Użyj niejawnego SSL/TLS dla połączeń SMTP" smtpSecureInfo: "Wyłącz, jeżeli używasz STARTTLS" testEmail: "Przetestuj dostarczanie wiadomości e-mail" wordMute: "Wyciszenie słowa" -hardWordMute: "Wyciszaj przekleństwa" -regexpError: "Błąd wyrażenia regularnego" -regexpErrorDescription: "Wystąpił błąd w wyrażeniu regularnym w linii {line} twoich {tab} wyciszeń:" instanceMute: "Wyciszone instancje" userSaysSomething: "{name} powiedział(-a) coś" makeActive: "Aktywuj" @@ -675,21 +613,20 @@ useGlobalSettingDesc: "Jeżeli włączone, zostaną wykorzystane ustawienia powi other: "Inne" regenerateLoginToken: "Generuj token logowania ponownie" regenerateLoginTokenDescription: "Regeneruje token używany wewnętrznie podczas logowania. Zazwyczaj nie jest to konieczne. Po regeneracji wszystkie urządzenia zostaną wylogowane." -theKeywordWhenSearchingForCustomEmoji: "To jest słowo kluczowe używane podczas wyszukiwania customowych Emoji." setMultipleBySeparatingWithSpace: "Możesz ustawić wiele, oddzielając je spacjami." fileIdOrUrl: "ID pliku albo URL" behavior: "Zachowanie" sample: "Przykład" abuseReports: "Zgłoszenia" reportAbuse: "Zgłoś" -reportAbuseRenote: "Zgłoś renote" reportAbuseOf: "Zgłoś {name}" fillAbuseReportDescription: "Wypełnij szczegóły zgłoszenia. Jeżeli dotyczy ono określonego wpisu, uwzględnij jego adres URL." abuseReported: "Twoje zgłoszenie zostało wysłane. Dziękujemy." -reporter: "Zgłaszający" reporteeOrigin: "Pochodzenie zgłoszonego" reporterOrigin: "Pochodzenie zgłaszającego" +forwardReport: "Przekaż zgłoszenie do innej instancji" send: "Wyślij" +abuseMarkAsResolved: "Oznacz zgłoszenie jako rozwiązane" openInNewTab: "Otwórz w nowej karcie" openInSideView: "Otwórz w bocznym widoku" defaultNavigationBehaviour: "Domyślne zachowanie nawigacji" @@ -707,7 +644,6 @@ createNewClip: "Utwórz nowy klip" unclip: "Odczep" confirmToUnclipAlreadyClippedNote: "Ten wpis jest już częścią klipu \"{name}\". Czy chcesz ją usunąć z tego klipu?" public: "Publiczny" -private: "Prywatne" i18nInfo: "Misskey jest tłumaczone na wiele języków przez wolontariuszy. Możesz pomóc na {link}." manageAccessTokens: "Zarządzaj tokenami dostępu" accountInfo: "Informacje o koncie" @@ -732,7 +668,6 @@ lockedAccountInfo: "Dopóki nie ustawisz widoczności wpisu na \"Obserwujący\", alwaysMarkSensitive: "Oznacz domyślnie jako NSFW" loadRawImages: "Wyświetlaj zdjęcia w załącznikach w całości zamiast miniatur" disableShowingAnimatedImages: "Nie odtwarzaj animowanych obrazów" -highlightSensitiveMedia: "Podkreśl wrażliwą zawartość" verificationEmailSent: "Wiadomość weryfikacyjna została wysłana. Odwiedź uwzględniony odnośnik, aby ukończyć weryfikację." notSet: "Nie ustawiono" emailVerified: "Adres e-mail został potwierdzony" @@ -743,11 +678,10 @@ contact: "Kontakt" useSystemFont: "Używaj domyślnej czcionki systemu" clips: "Klipy" experimentalFeatures: "Eksperymentalne funkcje" -experimental: "Eksperymentalne" -thisIsExperimentalFeature: "Ta funkcja jest eksperymentalna. Jej funkcjonalność może ulec zmianie, i może ona nie funkcjonować tak, jak zamierzono." developer: "Programista" makeExplorable: "Pokazuj konto na stronie „Eksploruj”" makeExplorableDescription: "Jeżeli wyłączysz tę opcję, Twoje konto nie będzie wyświetlać się w sekcji „Eksploruj”." +showGapBetweenNotesInTimeline: "Pokazuj odstęp między wpisami na osi czasu." duplicate: "Duplikuj" left: "Lewo" center: "Wyśsrodkuj" @@ -761,14 +695,12 @@ onlineUsersCount: "{n} osób jest online" nUsers: "{n} użytkowników" nNotes: "{n} wpisów" sendErrorReports: "Wyślij raporty o błędach" -sendErrorReportsDescription: "Gdy włączone, jeśli wystąpi problem, szczegółowe informacje o błędach będą udostępniane Misskey, pomagając ulepszyć jakość Misskey.\nBędzie to zawierało informacje takie jak wersja twojego systemu operacyjnego, jakiej przeglądarki używasz, twoja aktywność w Misskey, itd." myTheme: "Mój motyw" backgroundColor: "Tło" accentColor: "Akcent" textColor: "Tekst" saveAs: "Zapisz jako…" advanced: "Zaawansowane" -advancedSettings: "Zaawansowane ustawienia" value: "Wartość" createdAt: "Utworzono" updatedAt: "Zaktualizowano" @@ -825,18 +757,15 @@ administration: "Zarządzanie" accounts: "Konta" switch: "Przełącz" noMaintainerInformationWarning: "Informacje o administratorze nie są skonfigurowane." -noInquiryUrlWarning: "Adres URL zapytania nie został ustawiony" noBotProtectionWarning: "Zabezpieczenie przed botami nie jest skonfigurowane." configure: "Skonfiguruj" postToGallery: "Opublikuj w galerii" -postToHashtag: "Postuj do tego hashtagu" gallery: "Galeria" recentPosts: "Ostatnie wpisy" popularPosts: "Popularne wpisy" shareWithNote: "Udostępnij z wpisem" ads: "Reklamy" expiration: "Ankieta kończy się" -startingperiod: "Początek" memo: "Notatki" priority: "Priorytet" high: "Wysoki" @@ -863,19 +792,13 @@ translatedFrom: "Przetłumaczone z {x}" accountDeletionInProgress: "Trwa usuwanie konta" usernameInfo: "Nazwa, która identyfikuje Twoje konto spośród innych na tym serwerze. Możesz użyć alfabetu (a~z, A~Z), cyfr (0~9) lub podkreślników (_). Nazwy użytkownika nie mogą być później zmieniane." aiChanMode: "Tryb Ai" -devMode: "Tryb programisty" keepCw: "Zostaw ostrzeżenia o zawartości" pubSub: "Konta Pub/Sub" -lastCommunication: "Ostatnia komunikacja" resolved: "Rozwiązane" unresolved: "Nierozwiązane" breakFollow: "Usuń obserwującego" -breakFollowConfirm: "Czy na pewno usunąć tego obserwującego?" itsOn: "Włączone" itsOff: "Wyłączone" -on: "Włączone" -off: "Wyłączone" -emailRequiredForSignup: "Wymagaj adresu e-mail do rejestracji" unread: "Nieodczytane" filter: "Filtr" controlPanel: "Panel sterowania" @@ -885,12 +808,11 @@ makeReactionsPublicDescription: "To spowoduje, że lista wszystkich Twoich dotyc classic: "Klasyczny" muteThread: "Wycisz wątek" unmuteThread: "Wyłącz wyciszenie wątku" -followingVisibility: "Widoczność obserwacji" -followersVisibility: "Widoczność obserwujących" +ffVisibility: "Widoczność obserwowanych/obserwujących" +ffVisibilityDescription: "Pozwala skonfigurować, kto może zobaczyć, kogo obserwujesz i kto Cię obserwuje." continueThread: "Pokaż kontynuację wątku" deleteAccountConfirm: "Spowoduje to nieodwracalne usunięcie Twojego konta. Kontynuować?" incorrectPassword: "Nieprawidłowe hasło." -incorrectTotp: "Hasło pojedynczego użytku jest nie poprawne, lub straciło ważność" voteConfirm: "Potwierdzić swój głos na \"{choice}\"?" hide: "Ukryj" useDrawerReactionPickerForMobile: "Wyświetlaj wybornik reakcji jako szufladę na urządzeniach mobilnych" @@ -900,14 +822,9 @@ overridedDeviceKind: "Typ urządzenia" smartphone: "Smartfon" tablet: "Tablet" auto: "Automatycznie" -themeColor: "Motyw kolorystyczny" size: "Rozmiar" numberOfColumn: "Liczba kolumn" searchByGoogle: "Szukaj" -instanceDefaultLightTheme: "Domyślny motyw dla trybu jasnego" -instanceDefaultDarkTheme: "Domyślny motyw dla trybu ciemnego" -instanceDefaultThemeDescription: "Opis domyślnego motywu instancji" -mutePeriod: "Okres wyciszenia" period: "Ankieta kończy się" indefinitely: "Nigdy" tenMinutes: "10 minut" @@ -915,57 +832,30 @@ oneHour: "1 godzina" oneDay: "1 dzień" oneWeek: "1 tydzień" oneMonth: "jeden miesiąc" -threeMonths: "3 miesiące" -oneYear: "Rok" -threeDays: "3 dni" -reflectMayTakeTime: "Może minąć trochę czasu, zanim będzie to uwzględnione" failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie" -rateLimitExceeded: "Limit szybkości przekroczony" -cropImage: "Przytnij obraz" -cropImageAsk: "Czy chcesz przyciąć obrazek?" -cropYes: "Tak, przytnij" -cropNo: "Nie chce przycinać" file: "Pliki" -recentNHours: "W ciągu ostatnich {n} godzin" -recentNDays: "W ciągu ostatnich {n} dni" -noEmailServerWarning: "Serwer Email nie jest skonfigurowany" -thereIsUnresolvedAbuseReportWarning: "Istnieją niewyjaśnione raporty" recommended: "Zalecane" check: "Zweryfikuj" -driveCapOverrideLabel: "Zmień limit pojemności dysku użytkownika" -driveCapOverrideCaption: "Resetuje pojemność do wartości domyślnej, przez wpisanie wartości 0 lub niższej" -requireAdminForView: "Aby to zobaczyć, musisz być administratorem" -isSystemAccount: "To jest konto stworzone i zarządzane przez system" -typeToConfirm: "Wprowadź {x}, aby potwierdzić" deleteAccount: "Usuń konto" document: "Dokumentacja" numberOfPageCache: "Ilość stron w cache" -numberOfPageCacheDescription: "Zwiększenie tej liczby polepszy wygodę, ale spowoduje większe obciążenie jako użycie pamięci na urządzeniu użytkownika." logoutConfirm: "Czy na pewno chcesz się wylogować?" lastActiveDate: "Ostatnio użyte w" statusbar: "Pasek stanu" pleaseSelect: "Wybierz opcję" reverse: "Odwróć" colored: "Kolorowe" -refreshInterval: "Okres aktualizacji" label: "Etykieta" type: "Typ" speed: "Prędkość" -slow: "Wolny" -fast: "Szybki" -sensitiveMediaDetection: "Detekcja wrażliwej zawartości" localOnly: "Lokalne tylko" -remoteOnly: "Tylko zdalne instancje" failedToUpload: "Przesyłanie nie powiodło się" cannotUploadBecauseInappropriate: "Nie można przesłać tego pliku, ponieważ jego części zostały wykryte jako potencjalnie nieodpowiednie." cannotUploadBecauseNoFreeSpace: "Przesyłanie nie powiodło się z powodu braku miejsca na dysku." -cannotUploadBecauseExceedsFileSizeLimit: "Nie można przesłać pliku, ponieważ wykracza on poza limit wielkości pliku." beta: "Beta" enableAutoSensitive: "Automatyczne oznaczanie NSFW" enableAutoSensitiveDescription: "Umożliwia automatyczne wykrywanie i oznaczanie zawartości NSFW za pomocą uczenia maszynowego. Nawet jeśli ta opcja jest wyłączona, może być włączona w całej instancji." -activeEmailValidationDescription: "Włącza bardziej restrykcyjną walidację adresów e-mail, co obejmuje sprawdzanie adresów jednorazowych i czy komunikacja z tym adresem jest możliwa. Gdy wyłączone, tylko format adresu e-mail jest sprawdzany." navbar: "Pasek nawigacyjny" -shuffle: "Mieszaj" account: "Konta" move: "Przenieś" pushNotification: "Powiadomienia" @@ -975,94 +865,17 @@ pushNotificationAlreadySubscribed: "Powiadomienia push są włączone" pushNotificationNotSupported: "Przeglądarka lub instancja nie obsługuje powiadomień push" sendPushNotificationReadMessage: "Usuń powiadomienia push po przeczytaniu powiadomień i wiadomości." sendPushNotificationReadMessageCaption: "Chwilowo pojawi się powiadomienie \"{emptyPushNotificationMessage}\". Może wzrosnąć zużycie baterii urządzenia." -windowMaximize: "Maksymalizuj" -windowMinimize: "Minimalizuj" -windowRestore: "Przywróć" -caption: "Legenda" loggedInAsBot: "Jesteś obecnie zalogowany/a jako bot" -tools: "Narzędzia" -cannotLoad: "Nie można wczytać" -numberOfProfileView: "Wyświetlenia profilu" like: "Polub" -unlike: "Usuń polubienie" -numberOfLikes: "Liczba polubień" show: "Wyświetlanie" -neverShow: "Nie pokazuj ponownie" -remindMeLater: "Przypomnij później" -didYouLikeMisskey: "Czy Misskey się tobie spodobało?" -pleaseDonate: "{host} używa darmowego oprogramowania — Misskey. Bylibyśmy bardzo wdzięczni za datki, które pozwolą na kontynuację rozwoju Misskey!" -correspondingSourceIsAvailable: "Odpowiedni kod źródłowy jest dostępny pod {anchor}." -roles: "Role" -role: "Rola" -noRole: "Rola nie znaleziona" -normalUser: "Normalny użytkownik" -undefined: "Niezdefiniowane" -assign: "Przydziel" -unassign: "Cofnij przydzielenie" color: "Kolor" -manageCustomEmojis: "Zarządzaj niestandardowymi Emoji" -manageAvatarDecorations: "Zarządzaj dekoracjami awatara" -youCannotCreateAnymore: "Limit kreacji został przekroczony" -cannotPerformTemporary: "Opcja tymczasowo niedostępna" -cannotPerformTemporaryDescription: "Ta akcja nie może zostać wykonana, z powodu przekroczenia limitu wykonań. Prosimy poczekać chwilę i spróbować ponownie" -invalidParamError: "Błąd parametrów" -invalidParamErrorDescription: "Wartości, które zostały podane są niepoprawne. Zwykle jest to spowodowane bugiem, lecz również może być to spowodowane przekroczeniem limitu wartości, lub podobnym problemem" -permissionDeniedError: "Odrzucono operacje" -permissionDeniedErrorDescription: "Konto nie posiada uprawnień" -preset: "Konfiguracja" -selectFromPresets: "Wybierz konfiguracje" -achievements: "Osiągnięcia" -gotInvalidResponseError: "Niepoprawna odpowiedź serwera" -gotInvalidResponseErrorDescription: "Wystąpił problem z Twoim połączeniem z Internetem, lub z serwerem. {Spróbuj ponownie} wkrótce." -thisPostMayBeAnnoying: "Ten wpis może obrażać pozostałych użytkowników" -thisPostMayBeAnnoyingHome: "Opublikuj na domowej osi czasu" -thisPostMayBeAnnoyingCancel: "Odrzuć" -thisPostMayBeAnnoyingIgnore: "Zignoruj i wyślij" -collapseRenotes: "Zwiń wpisy, które już zobaczyłeś" -collapseRenotesDescription: "Zwiń wpisy, na które już zareagowałeś lub udostępniłeś" -internalServerError: "Wewnętrzny błąd serwera" -internalServerErrorDescription: "Niespodziewany błąd po stronie serwera" -copyErrorInfo: "Kopiuj informacje o błędzie" -joinThisServer: "Dołącz do chaty" -exploreOtherServers: "Szukaj innej instancji" -disableFederationOk: "Wyłącz federacje" -invitationRequiredToRegister: "Ten serwer wymaga zaproszenia. Tylko osoby z zaproszeniem mogą się zarejestrować" -emailNotSupported: "Wysyłanie wiadomości E-mail nie jest obsługiwane na tym serwerze" -postToTheChannel: "Publikuj na kanale" youFollowing: "Śledzeni" -icon: "Awatar" -replies: "Odpowiedz" -renotes: "Udostępnij" -sourceCode: "Kod źródłowy" -flip: "Odwróć" -lastNDays: "W ciągu ostatnich {n} dni" -surrender: "Odrzuć" -gameRetry: "Spróbuj ponownie" -postForm: "Formularz tworzenia wpisu" -information: "Informacje" -_chat: - invitations: "Zaproś" - noHistory: "Brak historii" - members: "Członkowie" - home: "Strona główna" - send: "Wyślij" -_delivery: - stop: "Zawieszono" - _type: - none: "Publikowanie" -_bubbleGame: - _score: - score: "Wynik" _role: - assignTarget: "Przydziel" priority: "Priorytet" _priority: low: "Niski" middle: "Średnie" high: "Wysoki" - _options: - canManageCustomEmojis: "Zarządzaj niestandardowymi Emoji" - canManageAvatarDecorations: "Zarządzaj dekoracjami awatara" _sensitiveMediaDetection: description: "Zmniejsza wysiłek związany z moderacją serwera dzięki automatycznemu rozpoznawaniu zawartości NSFW za pomocą uczenia maszynowego. To nieznacznie zwiększy obciążenie serwera." setSensitiveFlagAutomatically: "Oznacz jako NSFW" @@ -1109,7 +922,6 @@ _plugin: install: "Zainstaluj wtyczki" installWarn: "Nie instaluj niezaufanych wtyczek." manage: "Zarządzanie wtyczkami" - viewSource: "Zobacz źródło" _preferencesBackups: list: "Utworzone kopie zapasowe" saveNew: "Zapisz nową kopię zapasową" @@ -1166,6 +978,9 @@ _menuDisplay: _wordMute: muteWords: "Słowo do wyciszenia" muteWordsDescription2: "Otocz słowa kluczowe ukośnikami, aby używać wyrażeń regularnych." + soft: "Łagodny" + hard: "Twardy" + mutedNotes: "Wyciszone wpisy" _instanceMute: title: "Ukrywa wpisy z wymienionych instancji." heading: "Lista instancji do wyciszenia" @@ -1210,6 +1025,7 @@ _theme: header: "Nagłówek" navBg: "Tło paska bocznego" navFg: "Tekst paska bocznego" + navHoverFg: "Tekst paska bocznego (zbliżenie)" navActive: "Tekst paska bocznego (aktywny)" navIndicator: "Wskaźnik paska bocznego" link: "Odnośnik" @@ -1226,18 +1042,30 @@ _theme: infoFg: "Tekst informacji" infoWarnBg: "Tło ostrzeżenia" infoWarnFg: "Tekst ostrzeżenia" + cwBg: "Tło CW" + cwFg: "Tekst CW" + cwHoverBg: "Tło CW (po najechaniu)" toastBg: "Tło powiadomień" toastFg: "Tekst powiadomień" buttonBg: "Tło przycisku" buttonHoverBg: "Tło przycisku (po najechaniu)" inputBorder: "Obramowanie pola wejścia" + listItemHoverBg: "Tło elementu listy (po najechaniu)" + driveFolderBg: "Tło folderu na dysku" + wallpaperOverlay: "Nakładka tapety" badge: "Odznaka" messageBg: "Tło czatu" + accentDarken: "Akcent (ciemniejszy)" + accentLighten: "Akcent (jaśniejszy)" fgHighlighted: "Wyróżniony tekst" _sfx: note: "Wpisy" noteMy: "Mój wpis" notification: "Powiadomienia" + chat: "Wiadomości" + chatBg: "Rozmowy (tło)" + antenna: "Anteny" + channel: "Powiadomienia kanału" _ago: future: "W przyszłości" justNow: "Przed chwilą" @@ -1261,8 +1089,6 @@ _2fa: step3: "Wprowadź token podany w aplikacji, aby ukończyć konfigurację." step4: "Od teraz, przy każdej próbie logowania otrzymasz prośbę o token logowania." removeKeyConfirm: "Usunąć kopię zapasową {name}?" - renewTOTPConfirm: "Spowoduje to, że kody weryfikacyjne z poprzedniej aplikacji przestaną działać" - renewTOTPOk: "Rekonfiguruj" renewTOTPCancel: "Nie teraz" _permissions: "read:account": "Wyświetl informacje o swoim koncie" @@ -1276,10 +1102,8 @@ _permissions: "read:following": "Wyświetlanie informacji o obserwowanych" "write:following": "Obserwowanie lub cofanie obserwacji innych kont" "read:messaging": "Zobacz swoje czaty" - "write:messaging": "Tworzenie lub usuwanie wiadomości czatu" "read:mutes": "Wyświetlanie listy osób, które wyciszyłeś(-aś)" "write:mutes": "Edycja listy osób, które wyciszyłeś(-aś)" - "write:notes": "Tworzenie lub usuwanie wpisów" "read:notifications": "Wyświetlanie powiadomień" "write:notifications": "Działanie na powiadomieniach" "read:reactions": "Wyświetlanie reakcji" @@ -1295,24 +1119,9 @@ _permissions: "write:channels": "Edytuj swoje kanały" "read:gallery": "Zobacz swoją galerię" "write:gallery": "Edytuj swoją galerię" - "read:gallery-likes": "Wyświetlanie listy polubionych postów w galerii" - "write:gallery-likes": "Edytowanie listy polubionych postów w galerii" - "write:chat": "Tworzenie lub usuwanie wiadomości czatu" _auth: - shareAccessTitle: "Przyznawanie uprawnień aplikacji" shareAccess: "Czy chcesz autoryzować „{name}” do dostępu do tego konta?" - shareAccessAsk: "Czy na pewno chcesz zezwolić tej aplikacji na dostęp do Twojego konta?" - permission: "{name} żąda następujących uprawnień" permissionAsk: "Ta aplikacja wymaga następujących uprawnień:" - pleaseGoBack: "Proszę, wróć do aplikacji" - callback: "Powracanie do aplikacji" - denied: "Odmowa dostępu" - pleaseLogin: "Zaloguj się, aby autoryzować aplikacje." -_antennaSources: - all: "Wszystkie wpisy" - homeTimeline: "Wpisy obserwowanych użytkowników" - users: "Wpisy określonych użytkowników" - userList: "Wpisy z określonej listy użytkowników" _weekday: sunday: "Niedziela" monday: "Poniedziałek" @@ -1345,10 +1154,8 @@ _widgets: serverMetric: "Metryka serwera" aiscript: "Konsola AiScript" aichan: "Ai" - userList: "Lista użytkowników" _userList: chooseList: "Wybierz listę" - clicker: "Clicker" _cw: hide: "Ukryj" show: "Załaduj więcej" @@ -1380,16 +1187,10 @@ _visibility: public: "Publiczny" publicDescription: "Twój wpis pojawi się w publicznych osiach czasu" home: "Strona główna" - homeDescription: "Publikuj tylko na głównej osi czasu" followers: "Obserwujący" - followersDescription: "Widoczne tylko dla obserwujących" specified: "Bezpośredni" specifiedDescription: "Napisz tylko określonym użytkownikom" - disableFederationDescription: "Nie przesyłaj do innych instancji" _postForm: - replyPlaceholder: "Odpowiedz na ten wpis..." - quotePlaceholder: "Zacytuj ten wpis…" - channelPlaceholder: "Publikuj na kanale..." _placeholders: a: "Co się dzieje?" b: "Co się wydarzyło?" @@ -1411,30 +1212,17 @@ _profile: changeBanner: "Zmień baner" _exportOrImport: allNotes: "Wszystkie wpisy" - favoritedNotes: "Ulubione wpisy" - clips: "Klip" followingList: "Obserwowani" muteList: "Wycisz" blockingList: "Zablokuj" userLists: "Listy" - excludeMutingUsers: "Wyklucz wyciszonych użytkowników" - excludeInactiveUsers: "Wyklucz nieaktywnych użytkowników" _charts: federation: "Federacja" apRequest: "Żądania" - usersIncDec: "Różnica w liczbie użytkowników" usersTotal: "Łącznie # użytkowników" activeUsers: "Aktywni użytkownicy" - notesIncDec: "Różnica w liczbie wpisów" - notesTotal: "Całkowita liczba wpisów" - filesIncDec: "Różnica w liczbie plików" - filesTotal: "Całkowita liczba plików" - storageUsageIncDec: "Różnica w wykorzystaniu pamięci" - storageUsageTotal: "Całkowite wykorzystanie pamięci" _instanceCharts: requests: "Żądania" - users: "Różnica w liczbie użytkowników" - notes: "Różnica w liczbie wpisów" notesTotal: "Łącznie # wpisów" ff: "Różnica w # obserwujących" ffTotal: "Łączna liczba # obserwujących" @@ -1457,6 +1245,9 @@ _pages: newPage: "Utwórz stronę" editPage: "Edytuj tę stronę" readPage: "Aktywowano widok źródła" + created: "Pomyślnie utworzono stronę!" + updated: "Pomyślnie zaktualizowano stronę!" + deleted: "Strona została usunięta" pageSetting: "Ustawienia strony" nameAlreadyExists: "Określony adres URL strony już istnieje" invalidNameTitle: "Podany adres URL strony jest nieprawidłowy" @@ -1525,7 +1316,6 @@ _notification: reaction: "Reakcja" receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji" followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji" - login: "Zaloguj się" app: "Powiadomienia z aplikacji" _actions: followBack: "zaobserwował cię z powrotem" @@ -1557,30 +1347,5 @@ _deck: mentions: "Wspomnienia" direct: "Bezpośredni" _webhookSettings: - createWebhook: "Stwórz Webhook" name: "Nazwa" - secret: "Sekret" active: "Właczono" - _events: - follow: "Po zaobserwowaniu użytkownika" - followed: "Po zostaniu zaobserwowanym" - note: "Po opublikowaniu wpisu" - reply: "Po otrzymaniu odpowiedzi" - renote: "Po udostępnieniu wpisu" - reaction: "Po otrzymaniu reakcji" - mention: "Po zostaniu wspomnianym" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Adres e-mail" -_moderationLogTypes: - suspend: "Zawieś" - resetPassword: "Zresetuj hasło" -_reversi: - total: "Łącznie" -_remoteLookupErrors: - _noSuchObject: - title: "Nie znaleziono" -_search: - searchScopeAll: "Wszystkie" - searchScopeLocal: "Lokalne" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 64b152eccf..29d7de19c2 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -1,167 +1,133 @@ --- _lang_: "Português" headlineMisskey: "Uma rede ligada por notas" -introMisskey: "Bem-vindo! O Misskey é um serviço de microblog descentralizado de código aberto.\nCrie \"notas\" para compartilhar o que está acontecendo agora ou para se expressar com todos à sua volta 📡\nVocê também pode adicionar rapidamente reações às notas de outras pessoas usando a função \"Reações\" 👍\nVamos explorar um novo mundo 🚀" -poweredByMisskeyDescription: "{name} é uma instância da plataforma de código aberto Misskey." +introMisskey: "Bem-vindo! Misskey é um serviço de microblogue descentralizado de código aberto.\nCria \"notas\" e partilha o que te ocorre com todos à tua volta. 📡\nCom \"reações\" podes também expressar logo o que sentes às notas de todos. 👍\nExploremos um novo mundo! 🚀" monthAndDay: "{day}/{month}" -search: "Pesquisar" -reset: "Redefinir" +search: "Buscar" notifications: "Notificações" username: "Nome de usuário" password: "Senha" -initialPasswordForSetup: "Senha para a configuração inicial" -initialPasswordIsIncorrect: "Senha para configuração inicial está incorreta" -initialPasswordForSetupDescription: "Use a senha configurada no arquivo de configuração se você instalou o Misskey manualmente.\nSe você estiver utilizando um serviço de hospedagem, utilize a senha fornecida.\nSe uma senha não foi configurada, deixe em branco e continue." forgotPassword: "Esqueci-me da senha" -fetchingAsApObject: "Buscando no Fediverso..." +fetchingAsApObject: "Buscando no Fediverso" ok: "OK" gotIt: "Entendi" cancel: "Cancelar" -noThankYou: "Não, obrigado" enterUsername: "Digite o nome de usuário" renotedBy: "Repostado por {user}" -noNotes: "Sem notas" +noNotes: "Sem posts" noNotifications: "Sem notificações" instance: "Instância" settings: "Configurações" -notificationSettings: "Configurações de notificação" basicSettings: "Configurações básicas" otherSettings: "Outras configurações" -openInWindow: "Abrir em um janela" +openInWindow: "Abrir numa janela" profile: "Perfil" -timeline: "Linha do tempo" +timeline: "Timeline" noAccountDescription: "Este usuário não tem uma descrição." login: "Iniciar sessão" loggingIn: "Iniciando sessão…" logout: "Sair" signup: "Registrar-se" uploading: "Enviando…" -save: "Salvar" +save: "Guardar" users: "Usuários" addUser: "Adicionar usuário" -favorite: "Adicionar aos favoritos" -favorites: "Favoritos" +favorite: "Favoritar" +favorites: "Favoritar" unfavorite: "Remover dos favoritos" favorited: "Adicionado aos favoritos." alreadyFavorited: "Já adicionado aos favoritos." cantFavorite: "Não foi possível adicionar aos favoritos." -pin: "Fixar no perfil" +pin: "Afixar no perfil" unpin: "Desafixar do perfil" copyContent: "Copiar conteúdos" -copyLink: "Copiar link" -copyRemoteLink: "Copiar endereço remoto" -copyLinkRenote: "Copiar o link da repostagem" -delete: "Excluir" -deleteAndEdit: "Excluir e editar" -deleteAndEditConfirm: "Deseja excluir esta nota e editá-la novamente? Todas as reações, compartilhamentos e respostas a esta nota também serão excluídas." +copyLink: "Copiar hiperligação" +delete: "Eliminar" +deleteAndEdit: "Eliminar e editar" +deleteAndEditConfirm: "Tens a certeza que pretendes eliminar esta nota e editá-la? Irás perder todas as suas reações, renotas e respostas." addToList: "Adicionar a lista" -addToAntenna: "Adicionar à antena" -sendMessage: "Enviar mensagem" -copyRSS: "Copiar RSS" +sendMessage: "Enviar uma mensagem" copyUsername: "Copiar nome de utilizador" -copyUserId: "Copiar ID do utilizador" -copyNoteId: "Copiar ID da publicação" -copyFileId: "Copiar o ID do arquivo" -copyFolderId: "Copiar o ID da pasta" -copyProfileUrl: "Copiar a URL do perfil" -searchUser: "Pesquisar usuário" -searchThisUsersNotes: "Pesquisar as notas desse usuário" +searchUser: "Pesquisar utilizador" reply: "Responder" loadMore: "Carregar mais" showMore: "Ver mais" showLess: "Fechar" youGotNewFollower: "Você tem um novo seguidor" -receiveFollowRequest: "Pedido de seguidor recebido" -followRequestAccepted: "Pedido de seguidor aceito" +receiveFollowRequest: "Pedido de seguimento recebido" +followRequestAccepted: "Pedido de seguir aceito" mention: "Menção" mentions: "Menções" directNotes: "Notas diretas" importAndExport: "Importar/Exportar" import: "Importar" export: "Exportar" -files: "Arquivos" +files: "Ficheiros" download: "Descarregar" -driveFileDeleteConfirm: "Deseja excluir o arquivo '{name}'? Qualquer conteúdo que use este arquivo também será removido." -unfollowConfirm: "Gostaria de deixar de seguir {name}?" -exportRequested: "A sua solicitação de exportação foi enviada. Isso pode levar algum tempo. Assim que a exportação estiver concluída, ela será adicionada ao seu drive." -importRequested: "A sua solicitação de importação foi enviada. Isso pode levar algum tempo." +driveFileDeleteConfirm: "Tens a certeza que pretendes apagar o ficheiro \"{name}\"? As notas que tenham este ficheiro anexado serão também apagadas." +unfollowConfirm: "Tens a certeza que queres deixar de seguir {name}?" +exportRequested: "Pediste uma exportação. Este processo pode demorar algum tempo. Será adicionado à tua Drive após a conclusão do processo." +importRequested: "Pediste uma importação. Este processo pode demorar algum tempo." lists: "Listas" -noLists: "Não possui nenhuma lista" -note: "Publicar" +noLists: "Não tens nenhuma lista" +note: "Post" notes: "Posts" following: "Seguindo" followers: "Seguidores" -followsYou: "Te seguem" +followsYou: "Segue-te" createList: "Criar lista" -manageLists: "Gerenciar listas" +manageLists: "Gerir listas" error: "Erro" somethingHappened: "Ocorreu um erro" -retry: "Tente novamente" +retry: "Tentar novamente" pageLoadError: "Ocorreu um erro ao carregar a página." -pageLoadErrorDescription: "Isso geralmente acontece devido ao cache do navegador ou da rede. Tente limpar o cache ou aguarde um pouco antes de tentar novamente." -serverIsDead: "Não há resposta do servidor. Aguarde um momento e tente novamente." -youShouldUpgradeClient: "Para visualizar esta página, recarregue-a e utilize a nova versão do cliente." +pageLoadErrorDescription: "Isto é normalmente causado por erros de rede ou pela cache do browser. Experimenta limpar a cache e tenta novamente após algum tempo." +serverIsDead: "O servidor não está respondendo. Por favor espere um pouco e tente novamente." +youShouldUpgradeClient: "Para visualizar essa página, por favor recarregue-a para atualizar seu cliente." enterListName: "Insira um nome para a lista" privacy: "Privacidade" -makeFollowManuallyApprove: "Pedidos de seguidores precisam ser aprovados" +makeFollowManuallyApprove: "Pedidos de seguimento precisam ser aprovados" defaultNoteVisibility: "Visibilidade padrão" -follow: "Seguir" -followRequest: "Enviar pedido de seguidor" -followRequests: "Pedidos de seguidor" +follow: "Seguindo" +followRequest: "Mandar pedido de seguimento" +followRequests: "Pedidos de seguimento" unfollow: "Deixar de seguir" -followRequestPending: "Pedido de seguidor pendente" +followRequestPending: "Pedido de seguimento pendente" enterEmoji: "Inserir emoji" renote: "Repostar" -unrenote: "Remover repostagem" +unrenote: "Desmarcar" renoted: "Repostado" -renotedToX: "Repostar em {name}." -cantRenote: "Não é possível repostar esta postagem" +cantRenote: "Não pode repostar" cantReRenote: "Não pode repostar este repost" quote: "Citar" -inChannelRenote: "Repostar no canal" -inChannelQuote: "Citar no canal" -renoteToChannel: "Repostar em canal" -renoteToOtherChannel: "Repostar em outro canal" -pinnedNote: "Nota fixada" -pinned: "Fixar no perfil" +pinnedNote: "Post fixado" +pinned: "Afixar no perfil" you: "Você" clickToShow: "Clique para ver" sensitive: "Conteúdo sensível" add: "Adicionar" reaction: "Reações" reactions: "Reações" -emojiPicker: "Seleção de emoji" -pinnedEmojisForReactionSettingDescription: "Selecionar os emojis que serão fixados e exibidos ao reagir." -pinnedEmojisSettingDescription: "Selecionar os emojis que serão fixos e exibidos na seleção de emoji." -emojiPickerDisplay: "Janela de seleção de emoji" -overwriteFromPinnedEmojisForReaction: "Sobrescrever as opções de reação" -overwriteFromPinnedEmojis: "Sobrescrever as opções gerais" +reactionSetting: "Quais reações a mostrar no selecionador de reações" reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar." rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas" attachCancel: "Remover anexo" -deleteFile: "Excluir arquivo" markAsSensitive: "Marcar como sensível" unmarkAsSensitive: "Desmarcar como sensível" -enterFileName: "Digite o nome do arquivo" +enterFileName: "Digite o nome do ficheiro" mute: "Silenciar" -unmute: "Desmutar" -renoteMute: "Silenciar repostagens" -renoteUnmute: "Reativar repostagens" +unmute: "Dessilenciar" block: "Bloquear" unblock: "Desbloquear" suspend: "Suspender" unsuspend: "Cancelar suspensão" -blockConfirm: "Tem certeza que gostaria de bloquear esta conta?" -unblockConfirm: "Tem certeza que gostaria de desbloquear esta conta?" -suspendConfirm: "Tem certeza que gostaria de suspender esta conta?" -unsuspendConfirm: "Tem certeza que gostaria de cancelar a suspensão desta conta?" -selectList: "Selecione uma lista" -editList: "Editar lista" -selectChannel: "Selecionar canal" -selectAntenna: "Selecione uma antena" -editAntenna: "Editar antena" -createAntenna: "Criar uma antena" -selectWidget: "Selecione um widget" +blockConfirm: "Tem certeza que gostaria de bloquear essa conta?" +unblockConfirm: "Tem certeza que gostaria de desbloquear essa conta?" +suspendConfirm: "Tem certeza que gostaria de suspender essa conta?" +unsuspendConfirm: "Tem certeza que gostaria de cancelar a suspensão dessa conta?" +selectList: "Escolhe uma lista" +selectAntenna: "Escolhe uma antena" +selectWidget: "Escolhe um widget" editWidgets: "Editar widgets" editWidgetsExit: "Pronto" customEmojis: "Emoji personalizado" @@ -171,154 +137,132 @@ emojiName: "Nome do Emoji" emojiUrl: "URL do Emoji" addEmoji: "Adicionar um Emoji" settingGuide: "Guia de configuração" -cacheRemoteFiles: "Cache de arquivos remotos" -cacheRemoteFilesDescription: "Ao desativar esta configuração, os arquivos remotos não serão mais armazenados em cache e serão vinculados diretamente. Isso economizará espaço de armazenamento no servidor, mas os thumbnails não serão gerados, o que pode aumentar o tráfego de dados." -youCanCleanRemoteFilesCache: "Pode excluir todos os caches com o botão 🗑️ de gestão de arquivos." -cacheRemoteSensitiveFiles: "Fazer cache de arquivos remotos sensíveis" -cacheRemoteSensitiveFilesDescription: "Desativar essa configuração faz com que arquivos remotos sensíveis sejam vinculados diretamente em vez de armazenados em cache." +cacheRemoteFiles: "Memória transitória de arquivos remotos" +cacheRemoteFilesDescription: "Se você desabilitar essa configuração, os arquivos remotos não serão armazenados em memória transitória e serão vinculados diretamente. Economiza o armazenamento do servidor, mas não gera miniaturas, o que aumenta o tráfego." flagAsBot: "Marcar conta como robô" -flagAsBotDescription: "Se esta conta for operada por uma aplicação, ative esta opção. Ao ativá-la, ela servirá como um sinalizador para evitar reações em cadeia e ajudar outros desenvolvedores. Além disso, ajustará o tratamento da conta no sistema do Misskey para que se adeque a um Bot." +flagAsBotDescription: "Se esta conta for operada por um programa, ative este sinalizador. Quando ativado, serve como um sinalizador para evitar o encadeamento de reações para outros programadores, e o manuseio do sistema do Misskey é adequado para ‘bots’." flagAsCat: "Marcar conta como gato" -flagAsCatDescription: "Ative esta opção para marcar essa conta como gato" +flagAsCatDescription: "Ative essa opção para marcar essa conta como gato." flagShowTimelineReplies: "Mostrar respostas na linha de tempo" flagShowTimelineRepliesDescription: "Quando ativado, a linha do tempo mostra as respostas às outras notas do utilizador, além da nota do utilizador." autoAcceptFollowed: "Aprove automaticamente os seguidores dos seguintes utilizadores" addAccount: "Adicionar Conta" -reloadAccountsList: "Recarregar lista de contas" -loginFailed: "Falha ao logar" +loginFailed: "Não consegui logar" showOnRemote: "Exibir remotamente" -continueOnRemote: "" -chooseServerOnMisskeyHub: "Escolher um servidor da Misskey Hub" -specifyServerHost: "Especificar uma instância diretamente" -inputHostName: "Insira o domínio" general: "Geral" wallpaper: "Papel de parede" setWallpaper: "Definir papel de parede" removeWallpaper: "Remover papel de parede" searchWith: "Buscar: {q}" youHaveNoLists: "Não tem nenhuma lista" -followConfirm: "Tem certeza que quer seguir {name}?" +followConfirm: "Tem certeza que quer deixar de seguir {name}?" proxyAccount: "Conta proxy" -proxyAccountDescription: "Uma conta de proxy é uma conta que assume o acompanhamento remoto de um usuário sob certas condições específicas. Por exemplo, quando um usuário inclui um usuário remoto em uma lista, mas ninguém na lista está seguindo o usuário remoto, a atividade não é entregue ao servidor. Nesse caso, a conta de proxy entra em ação para seguir o usuário remoto em vez disso." -host: "Host" -selectSelf: "Selecionar a mim" -selectUser: "Selecionar usuário" -recipient: "Destinatário" +proxyAccountDescription: "Uma conta proxy é uma conta que atua como seguidora remota para utilizadores sob determinadas condições. Por exemplo, quando um utilizador lista um utilizador remoto, a atividade não será entregue à instância, a menos que alguém esteja seguindo o utilizador listado, portanto, a conta proxy deve seguir." +host: "hospedeiro" +selectUser: "Selecionar utilizador" +recipient: "Morada" annotation: "Anotação" -federation: "Federação" -instances: "Instâncias" +federation: "União" +instances: "Instância" registeredAt: "Registrado em" -latestRequestReceivedAt: "Última solicitação recebida" +latestRequestReceivedAt: "Recebeu a última solicitação" latestStatus: "Status mais recente" storageUsage: "Uso de armazenamento" -charts: "Gráfico" -perHour: "Por Hora" -perDay: "Por dia" +charts: "gráfico" +perHour: "por hora" +perDay: "por dia" stopActivityDelivery: "Parar a entrega de atividades" blockThisInstance: "Bloquear esta instância" -silenceThisInstance: "Silenciar essa instância" -mediaSilenceThisInstance: "Silenciar a mídia dessa instância" -operations: "Operações" -software: "Software" -softwareName: "Software" -version: "Versão" +operations: "operar" +software: "Programas" +version: "versão" metadata: "Metadados" -withNFiles: "{n} arquivo(s)" +withNFiles: "{n} Um arquivo" monitor: "monitor" -jobQueue: "Fila de tarefas" +jobQueue: "Fila de trabalhos" cpuAndMemory: "CPU e memória" -network: "Rede" -disk: "Disco" +network: "rede" +disk: "disco" instanceInfo: "Informações da instância" -statistics: "Estatísticas" +statistics: "Estatisticas" clearQueue: "Limpar a fila" -clearQueueConfirmTitle: "Deseja limpar a fila?" -clearQueueConfirmText: "As postagens não entregues deixarão de ser enviadas. Geralmente, não é necessário realizar essa operação." -clearCachedFiles: "Limpar o cache" -clearCachedFilesConfirm: "Deseja excluir todos os arquivos remotos em cache?" +clearQueueConfirmTitle: "Quer limpar a fila?" +clearQueueConfirmText: "Postagens não entregues não serão mais entregues. Normalmente você não precisa fazer isso." +clearCachedFiles: "Limpar memória transitória" +clearCachedFilesConfirm: "Tem certeza de que deseja excluir todos os arquivos remotos armazenados em memória transitória?" blockedInstances: "Instância bloqueada" -blockedInstancesDescription: "Configure os hosts dos servidores que deseja bloquear, separando-os por quebras de linha. Os servidores bloqueados não poderão interagir com este servidor, incluindo os subdomínios." -silencedInstances: "Instâncias silenciadas" -silencedInstancesDescription: "Liste o nome de hospedagem dos servidores que você deseja silenciar, separados por linha. Todas as contas desses servidores serão silenciada e poderão enviar solicitações para seguir, mas não poderão mencionar usuários locais sem segui-los. Isso não afetará servidores bloqueados." -mediaSilencedInstances: "Instâncias com mídia silenciadas" -mediaSilencedInstancesDescription: "Liste o nome de hospedagem dos servidores cuja mídia você deseja silenciar, separados por linha. Todas as contas desses servidores serão consideradas sensíveis e não poderão utilizar emojis personalizados. Isso não afetará servidores bloqueados." -federationAllowedHosts: "Servidores com federação permitida" -federationAllowedHostsDescription: "Especifique o endereço dos servidores em que deseja permitir a federação separados por linha." +blockedInstancesDescription: "Defina os anfitriões das instâncias que deseja bloquear, separados por quebras de linha. Uma instância bloqueada não poderá interagir com esta instância." muteAndBlock: "Silenciar e bloquear" -mutedUsers: "Usuários silenciados" -blockedUsers: "Usuários bloqueados" +mutedUsers: "Silenciar utilizador" +blockedUsers: "Utilizadores bloqueados" noUsers: "Sem usuários" editProfile: "Editar Perfil" noteDeleteConfirm: "Deseja excluir esta nota?" -pinLimitExceeded: "Não é possível fixar novas notas" +pinLimitExceeded: "Não consigo mais fixar" +intro: "A instalação do Misskey está completa! Crie uma conta de administrador." done: "Concluído" processing: "Em Progresso" preview: "Pré-visualizar" -default: "Predefinição" -defaultValueIs: "Predefinição: {value}" +default: "Padrão" noCustomEmojis: "Não há emojis" -noJobs: "Não há tarefas" -federating: "Federando" +noJobs: "Sem trabalho" +federating: "federar" blocked: "Bloqueado" -suspended: "Suspenso" +suspended: "Cancelar subscrição" all: "Todos" -subscribing: "Inscrito" -publishing: "Publicando" +subscribing: "Subscrito" +publishing: "Executando" notResponding: "Sem resposta" instanceFollowing: "Seguir a instância" instanceFollowers: "Seguidores da instância" -instanceUsers: "Usuários da instância" +instanceUsers: "Utilizador da instância" changePassword: "Mudar senha" security: "Segurança" -retypedNotMatch: "As informações inseridas não coincidem." -currentPassword: "Senha atual" -newPassword: "Nova senha" -newPasswordRetype: "Nova senha (digite novamente)" +retypedNotMatch: "As entradas não coincidem." +currentPassword: "Palavra-passe atual" +newPassword: "Nova palavra-passe" +newPasswordRetype: "Nova senha (redigite)" attachFile: "Anexar arquivo" more: "Mais!" featured: "Destaques" -usernameOrUserId: "Nome de usuário ou ID do usuário" -noSuchUser: "Usuário não encontrado" -lookup: "Consultar" -announcements: "Avisos" +usernameOrUserId: "Nome de utilizador ou ID de utilizador" +noSuchUser: "Utilizador não encontrado" +lookup: "Buscando" +announcements: "Notícia" imageUrl: "URL da imagem" -remove: "Remover" -removed: "Removido" +remove: "Eliminar" +removed: "Foi deletado" removeAreYouSure: "Deseja excluir \"{x}\"?" deleteAreYouSure: "Deseja excluir \"{x}\"?" -resetAreYouSure: "Deseja reiniciar?" -areYouSure: "Tem certeza?" +resetAreYouSure: "Redefinir agora?" saved: "Salvo" -upload: "Fazer upload" +messaging: "Chat" +upload: "Enviando" keepOriginalUploading: "Manter a imagem original" -keepOriginalUploadingDescription: "Ao fazer o upload de uma imagem, ela será mantida em sua versão original. Caso desative esta opção, o navegador irá gerar uma versão da imagem otimizada para publicação na web durante o upload." -fromDrive: "Do drive" +keepOriginalUploadingDescription: "Mantenha a versão original ao carregar a imagem. Quando desligado, a imagem para publicação na web será gerada no navegador no momento do upload." +fromDrive: "\nDa unidade" fromUrl: "Da URL" -uploadFromUrl: "Enviar por URL" +uploadFromUrl: "Carregamento de URL" uploadFromUrlDescription: "URL do arquivo que você deseja enviar" uploadFromUrlRequested: "Upload solicitado" uploadFromUrlMayTakeTime: "Pode levar algum tempo para que o upload seja concluído." explore: "Explorar" messageRead: "Lida" -noMoreHistory: "Não existe histórico anterior" -startChat: "Iniciar conversa" -nUsersRead: "{n} pessoas leram" +noMoreHistory: "Sem mais história" +startMessaging: "Iniciar conversação" +nUsersRead: "{n} Pessoas leem" agreeTo: "Eu concordo com {0}" -agree: "Concordar" -agreeBelow: "Eu concordo com o seguinte" -basicNotesBeforeCreateAccount: "Observações importantes" -termsOfService: "Termos de Uso" start: "começar" -home: "Início" +home: "casa" remoteUserCaution: "As informações estão incompletas porque é um utilizador remoto." activity: "atividade" images: "imagem" image: "imagem" -birthday: "Aniversário" +birthday: "aniversário" yearsOld: "{age} anos" registeredDate: "Data de registro" -location: "Localização" -theme: "Tema" +location: "Lugar, colocar" +theme: "tema" themeForLightMode: "Temas usados ​​no modo de luz" themeForDarkMode: "Temas usados ​​no modo escuro" light: "Claro" @@ -326,24 +270,21 @@ dark: "Escuro" lightThemes: "Tema claro" darkThemes: "Tema escuro" syncDeviceDarkMode: "Sincronize com o modo escuro do dispositivo" -drive: "Drive" +drive: "Unidades" fileName: "Nome do Ficheiro" selectFile: "Selecione os arquivos" selectFiles: "Selecione os arquivos" selectFolder: "Selecionar uma pasta" selectFolders: "Selecionar uma pasta" -fileNotSelected: "Nenhuma pasta selecionada" renameFile: "Renomear ficheiro" folderName: "Nome da pasta" createFolder: "Criar pasta" renameFolder: "Renomear Pasta" -deleteFolder: "Excluir pasta" -folder: "Pasta" +deleteFolder: "Eliminar Pasta" addFile: "Adicionar arquivo" -showFile: "Mostrar arquivos" -emptyDrive: "O drive está vazio" +emptyDrive: "A unidade está vazia" emptyFolder: "A pasta está vazia" -unableToDelete: "Não é possível excluir" +unableToDelete: "Não é possível eliminar" inputNewFileName: "Por favor, digite um novo nome para a pasta!" inputNewDescription: "Insira uma nova legenda" inputNewFolderName: "Por favor, digite um novo nome para a pasta!" @@ -353,7 +294,6 @@ copyUrl: "Copiar URL" rename: "Renomear" avatar: "Avatar" banner: "Capa" -displayOfSensitiveMedia: "Exibição de mídia sensível" whenServerDisconnected: "Quando a conexão com o servidor é perdida" disconnectedFromServer: "Desconectado do servidor" reload: "Recarregar" @@ -383,10 +323,12 @@ enableLocalTimeline: "Ativar linha do tempo local" enableGlobalTimeline: "Ativar linha do tempo global" disablingTimelinesInfo: "Se você desabilitar essas linhas do tempo, administradores e moderadores ainda poderão usá-las por conveniência." registration: "Registar" +enableRegistration: "Permitir que qualquer pessoa se registre" invite: "Convidar" -driveCapacityPerLocalAccount: "Capacidade do drive por usuário local" -driveCapacityPerRemoteAccount: "Capacidade do drive por usuário remoto" +driveCapacityPerLocalAccount: "Capacidade da unidade por utilizador local" +driveCapacityPerRemoteAccount: "Capacidade da unidade por utilizador remoto" inMb: "Em ‘megabytes’" +iconUrl: "URL da imagem do ícone (favicon, etc.)" bannerUrl: "URL da imagem do ‘banner’" backgroundImageUrl: "URL da imagem de fundo" basicInfo: "Informações básicas" @@ -400,17 +342,10 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Ativar hCaptcha" hcaptchaSiteKey: "Chave do sítio ‘web’" hcaptchaSecretKey: "Chave secreta" -mcaptcha: "mCaptcha" -enableMcaptcha: "Habilitar mCaptcha" -mcaptchaSiteKey: "Chave do sítio ‘web’" -mcaptchaSecretKey: "Chave secreta" -mcaptchaInstanceUrl: "URL do servidor mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Habilitar reCAPTCHA" recaptchaSiteKey: "Chave do sítio ‘web’" recaptchaSecretKey: "Chave secreta" -turnstile: "Controle de acesso" -enableTurnstile: "Ativar controle de acesso" turnstileSiteKey: "Chave do sítio ‘web’" turnstileSecretKey: "Chave secreta" avoidMultiCaptchaConfirm: "O uso de vários captchas pode causar interferência. Deseja desativar outros captchas? Você também pode cancelar e deixar vários captchas ativados." @@ -420,11 +355,9 @@ name: "Nome" antennaSource: "Origem de entrada" antennaKeywords: "Palavras-chave recebidas" antennaExcludeKeywords: "Palavras-chave negativas" -antennaExcludeBots: "Ignorar contas de bot" antennaKeywordsDescription: "Se você separá-lo com um espaço, será uma especificação AND, e se você separá-lo com uma quebra de linha, será uma especificação OR." notifyAntenna: "Notificar novas notas" withFileAntenna: "Apenas notas com arquivos anexados" -excludeNotesInSensitiveChannel: "Excluir notas de canais sensíveis" enableServiceworker: "Ative as notificações push para o seu navegador" antennaUsersDescription: "Especificar nomes de utilizador separados por quebras de linha" caseSensitive: "Maiúsculas e minúsculas" @@ -448,31 +381,20 @@ about: "Informações" aboutMisskey: "Sobre Misskey" administrator: "Administrador" token: "Símbolo" -2fa: "Autenticação de dois fatores" -setupOf2fa: "Configuração de autenticação de dois fatores" -totp: "Aplicativo Autenticador" -totpDescription: "Digite a senha de uso único informado pelo aplicativo autenticador" moderator: "Moderador" -moderation: "Moderação" -moderationNote: "Nota de moderação" -moderationNoteDescription: "Você pode preencher notas que serão compartilhadas apenas com moderadores." -addModerationNote: "Adicionar nota de moderação" -moderationLogs: "Logs de moderação" nUsersMentioned: "Postado por {n} pessoas" -securityKeyAndPasskey: "Chave de segurança / Chave de acesso" securityKey: "Chave de segurança" lastUsed: "Último uso" -lastUsedAt: "Última utilização: {t}" unregister: "Cancelar registro" passwordLessLogin: "Entrar sem senha" -passwordLessLoginDescription: "Faça login apenas com uma chave de segurança / chave de acesso sem utilização de senha" resetPassword: "Redefinir senha" newPasswordIs: "A nova senha é \"{password}\"" reduceUiAnimation: "Reduzir a animação da ‘interface’ do utilizador" share: "Compartilhar" notFound: "Não encontrado" notFoundDescription: "Não havia página correspondente ao URL especificado." -uploadFolder: "Destino de upload padrão" +uploadFolder: "Destino de ‘upload’ padrão" +cacheClear: "Excluir memória transitória" markAsReadAllNotifications: "Marcar todas as notificações como lidas" markAsReadAllUnreadNotes: "Marcar todas as postagens como lidas" markAsReadAllTalkMessages: "Marcar todas as conversas como lidas" @@ -480,63 +402,15 @@ help: "Ajuda" inputMessageHere: "Escrever mensagem aqui" close: "Fechar" invites: "Convidar" -members: "Membros" -transfer: "Transferência" -title: "Título" -text: "Texto" -enable: "Habilitar" -next: "Seguinte" -retype: "Digite novamente" -noteOf: "Publicação de {user}" -quoteAttached: "Com citação" -quoteQuestion: "Anexar como citação?" -attachAsFileQuestion: "O texto na área de transferência é muito longo. Você gostaria de anexá-lo como um arquivo de texto?" -onlyOneFileCanBeAttached: "Apenas um arquivo pode ser anexado a uma mensagem" -signinRequired: "É necessário se inscrever ou fazer login antes de continuar" -signinOrContinueOnRemote: "Para continuar, você precisa mover o seu servidor ou entrar/cadastrar-se nesse servidor." invitations: "Convidar" -invitationCode: "Código de convite" -checking: "Verificando..." -available: "Disponível" -unavailable: "Não disponível" -usernameInvalidFormat: "Pode utilizar letras maiúsculas e minúsculas, números e sublinhado (_)" -tooShort: "Muito curto" -tooLong: "Muito longo" -weakPassword: "Senha fraca" -normalPassword: "Senha normal" -strongPassword: "Senha forte" -passwordMatched: "As senhas coincidem" -passwordNotMatched: "As senhas não coincidem" -signinWith: "Faça login com {x}" -signinFailed: "Não foi possível fazer login. Por favor, verifique o nome de usuário e a senha." -or: "Ou" -language: "Idioma" -uiLanguage: "Idioma de exibição da interface " -aboutX: "Sobre {x}" -emojiStyle: "Estilo de emojis" -native: "Nativo" -menuStyle: "Estilo do menu" -style: "Estilo" -drawer: "Gaveta" -popup: "Pop-up" -showNoteActionsOnlyHover: "Exibir as ações da nota somente ao passar o cursor sobre ela" -showReactionsCount: "Ver o número de reações nas notas" -noHistory: "Ainda não há histórico" -signinHistory: "Histórico de acesso" -enableAdvancedMfm: "Habilitar MFM avançado" -enableAnimatedMfm: "Habilitar MFM animado" -doing: "Processando..." -category: "Categoria" tags: "Etiquetas" docSource: "Fonte deste documento" createAccount: "Criar conta" existingAccount: "Contas existentes" regenerate: "Gerar novamente" fontSize: "Tamanho do texto" -mediaListWithOneImageAppearance: "Altura da lista de mídias com apenas uma imagem" -limitTo: "Até {x}" -noFollowRequests: "Não há pedidos de seguidor pendentes" -openImageInNewTab: "Abrir a imagem em uma nova aba" +noFollowRequests: "Não há aplicação de acompanhamento" +openImageInNewTab: "Abrir a imagem numa nova aba" dashboard: "Painel de controle" local: "Local" remote: "Remoto" @@ -559,8 +433,8 @@ objectStorageBucket: "Bucket" objectStorageBucketDesc: "Especifique o nome do bucket do serviço a ser usado." objectStoragePrefix: "Prefixo" objectStoragePrefixDesc: "Ele é armazenado neste diretório de prefixo." -objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "No caso do S3, deixe em branco; para outros serviços, especifique o endpoint de cada serviço. Informe-o no formato '' ou ':'." +objectStorageEndpoint: "Ponto final" +objectStorageEndpointDesc: "Especifique vazio para S3, caso contrário, especifique o ponto final para cada serviço. Especifique como''ou': '." objectStorageRegion: "Região" objectStorageRegionDesc: "Especifique uma região como 'xx-east-1'. Caso seu serviço não tenha o conceito de região, ele deve estar vazio ou 'us-east-1'." objectStorageUseSSL: "Usar SSL" @@ -568,14 +442,10 @@ objectStorageUseSSLDesc: "Desative-o se não quiser usar https para conexões de objectStorageUseProxy: "Usar proxy" objectStorageUseProxyDesc: "Se você não usa proxy para conexão de API, desative-o." objectStorageSetPublicRead: "Definir 'public-read' ao fazer o upload" -s3ForcePathStyleDesc: "Ao habilitar s3ForcePathStyle, o nome do bucket é especificado como parte do caminho em vez de ser o nome do host na URL. Isso pode ser necessário ao usar serviços auto-hospedados como o Minio." -serverLogs: "Logs do servidor" -deleteAll: "Excluir tudo" +serverLogs: "Registro do servidor" +deleteAll: "Apagar Tudo" showFixedPostForm: "Exibir o formulário de postagem na parte superior da linha do tempo" -showFixedPostFormInChannel: "Exibir o campo de postagem na parte superior da linha do tempo (canais)" -withRepliesByDefaultForNewlyFollowed: "Incluir respostas por usuários recém-seguidos na linha do tempo por padrão" newNoteRecived: "Nova nota recebida" -newNote: "Nova Nota" sounds: "Sons" sound: "Sons" listen: "Ouvir" @@ -584,2005 +454,57 @@ showInPage: "Ver na página" popout: "Sair" volume: "Volume" masterVolume: "volume principal" -notUseSound: "Desabilitar som" -useSoundOnlyWhenActive: "Apenas reproduzir sons quando Misskey estiver aberto." details: "Detalhes" -renoteDetails: "Detalhes da repostagem" -chooseEmoji: "Selecione um emoji" -unableToProcess: "Não é possível concluir a operação" -recentUsed: "Usado recentemente" -install: "Instalar" -uninstall: "Desinstalar" -installedApps: "Aplicativos instalados" -nothing: "Não há nada aqui" -installedDate: "Data de instalação" -lastUsedDate: "Data de última utilização" -state: "Estado" -sort: "Ordenação" -ascendingOrder: "Ascendente" -descendingOrder: "Descendente" -scratchpad: "Bloco de rascunho" -scratchpadDescription: "O Bloco de rascunho fornece um ambiente experimental para AiScript. Permite escrever, executar e verificar os resultados do código para interagir com o Misskey." -uiInspector: "Inspecionador de interface" -uiInspectorDescription: "Você pode ver a lista de servidores de componentes de interface na memória. Componentes da interface serão gerados pela função Ui:C:." output: "Resultado" -script: "Script" -disablePagesScript: "Desabilitar scripts nas páginas" -updateRemoteUser: "Atualizar informações do usuário remoto" -unsetUserAvatar: "Remover avatar" -unsetUserAvatarConfirm: "Você tem certeza de que deseja remover o avatar?" -unsetUserBanner: "Remover banner" -unsetUserBannerConfirm: "Você tem certeza de que deseja remover o banner?" -deleteAllFiles: "Excluir todos os arquivos" -deleteAllFilesConfirm: "Deseja excluir todos os arquivos?" -removeAllFollowing: "Deseja remover todos os seguidores?" -removeAllFollowingDescription: "Deixar de seguir todos de {host}. Faça isso se, por exemplo, o servidor não existir mais." -userSuspended: "Este usuário foi suspenso." -userSilenced: "Este usuário está silenciado." -yourAccountSuspendedTitle: "Esta conta está suspensa" -yourAccountSuspendedDescription: "Esta conta foi suspensa devido a violações dos termos de uso do servidor ou por outros motivos. Para mais detalhes, entre em contato com o administrador. Por favor, não crie uma nova conta." -tokenRevoked: "Token inválido" -tokenRevokedDescription: "Seu token de login expirou. Por favor, faça login novamente." -accountDeleted: "A conta foi removida" -accountDeletedDescription: "Esta conta foi removida." -menu: "Menu\n" -divider: "Separador" -addItem: "Adicionar item" -rearrange: "Reordernar" -relays: "Relays" -addRelay: "Adicionar relay" -inboxUrl: "Inbox URL" -addedRelays: "Relays adicionados" -serviceworkerInfo: "Deve estar habilitado para receber notificações por push." -deletedNote: "Postagem excluída" -invisibleNote: "Notas invisíveis" -enableInfiniteScroll: "Carregar automaticamente" -visibility: "Visibilidade" -poll: "Enquetes" -useCw: "Ocultar conteúdo" -enablePlayer: "Abrir o reprodutor de mídia" -disablePlayer: "Fechar o reprodutor de mídia" -expandTweet: "Expandir tweet" -themeEditor: "Editor de temas" -description: "Descrição" -describeFile: "Adicionar legenda" -enterFileDescription: "Insira uma legenda" -author: "Autor" -leaveConfirm: "Existem alterações não salvas. Deseja descartá-las?" -manage: "Administrar" -plugins: "Plugins" -preferencesBackups: "Definições de Backup" -deck: "Deck" -undeck: "Sair do deck" -useBlurEffectForModal: "Usar efeito de desfoque para modal" -useFullReactionPicker: "Usar o seletor de reações completo" -width: "Largura" -height: "Altura" -large: "Grande" -medium: "Médio" -small: "Pequeno" -generateAccessToken: "Gerar token de acesso" -permission: "Permissões" -adminPermission: "Permissões de administrador" -enableAll: "Habilitar tudo" -disableAll: "Desabilitar tudo" -tokenRequested: "Autorização de acesso à conta" -pluginTokenRequestedDescription: "Este plugin poderá utilizar as permissões definidas aqui." -notificationType: "Tipos de notificação" -edit: "Editar" -emailServer: "Servidor de e-mail" -enableEmail: "Habilitar envio de e-mails" -emailConfigInfo: "Usado para confirmar o seu endereço de e-mail e redefinir sua senha" -email: "E-mail" -emailAddress: "Endereço de e-mail" -smtpConfig: "Configuração do servidor SMTP" -smtpHost: "Host" -smtpPort: "Porta" +smtpHost: "hospedeiro" smtpUser: "Nome de usuário" smtpPass: "Senha" -emptyToDisableSmtpAuth: "Desative a autenticação SMTP deixando o nome de usuário e a senha em branco." -smtpSecure: "Use SSL/TLS implícito para conexões SMTP" -smtpSecureInfo: "Desative esta opção ao utilizar STARTTLS." -testEmail: "Testar envio de e-mail" -wordMute: "Silenciar palavras" -wordMuteDescription: "Minimizar notas que contêm a palavra ou frase especificada. Notas minimizadas são exibidas ao clicá-las." -hardWordMute: "Silenciar palavras (esconder posts)" -showMutedWord: "Exibir palavras silenciadas" -hardWordMuteDescription: "Esconder notas que contêm a palavra ou frase especificada. Diferente do silenciamento de palavras, a nota será completamente escondida." -regexpError: "Erro na expressão regular" -regexpErrorDescription: "Ocorreu um erro na expressão regular na linha {line} da palavra mutada {tab}:" -instanceMute: "Instâncias silenciadas" -userSaysSomething: "{name} disse algo" -userSaysSomethingAbout: "{name} disse algo sobre \"{word}\"" -makeActive: "Ativar" -display: "Visualizar" -copy: "Copiar" -copiedToClipboard: "Copiado à área de transferência" -metrics: "Métricas" -overview: "Visão geral" -logs: "Logs" -delayed: "atrasado" -database: "Banco de dados" -channel: "Canais" -create: "Criar" -notificationSetting: "Configurações de notificação" -notificationSettingDesc: "Selecione o tipo de notificação a ser exibido." -useGlobalSetting: "Utilizar a configuração global" -useGlobalSettingDesc: "Ao ativar, serão utilizadas as configurações de notificação da conta. Ao desativar, você poderá configurar individualmente." -other: "Outros" -regenerateLoginToken: "Gerar novo token de login" -regenerateLoginTokenDescription: "Gera novamente o token interno usado para o login. Normalmente, isso não é necessário. Ao regenerar, você será desconectado de todos os dispositivos." -theKeywordWhenSearchingForCustomEmoji: "Essa é a palavra-chave ao pesquisar por emojis personalizados" -setMultipleBySeparatingWithSpace: "Você pode configurar vários itens separando-os por espaço." -fileIdOrUrl: "ID do arquivo ou URL" -behavior: "Comportamento" -sample: "Exemplo" -abuseReports: "Denúncias" -reportAbuse: "Denunciar" -reportAbuseRenote: "Reportar repostagem" -reportAbuseOf: "Denunciar {name}" -fillAbuseReportDescription: "Por favor, forneça detalhes sobre o motivo da denúncia. Se houver uma nota específica envolvida, inclua também a URL dela." -abuseReported: "Denúncia enviada. Obrigado por sua ajuda." -reporter: "Denunciante" -reporteeOrigin: "Origem da denúncia" -reporterOrigin: "Origem do denunciante" -send: "Enviar" -openInNewTab: "Abrir em nova aba" -openInSideView: "Abrir em visão lateral" -defaultNavigationBehaviour: "Navegação padrão" -editTheseSettingsMayBreakAccount: "Editar essas configurações pode resultar em danos à conta.\"" -instanceTicker: "Informações do servidor das notas" -waitingFor: "Aguardando por {x}" -random: "Aleatório" -system: "Sistema" -switchUi: "Alternar UI" -desktop: "Área de Trabalho" -clip: "Clipe" -createNew: "Criar novo" -optional: "Opcional" -createNewClip: "Criar novo clipe" -unclip: "Remover do clipe" -confirmToUnclipAlreadyClippedNote: "Esta nota já está incluída no clipe \"{name}\". Você deseja remover a nota deste clipe?" -public: "Público" -private: "Privado" -i18nInfo: "Misskey é traduzido para várias línguas por voluntários. Você pode ajudar com as traduções em {link}." -manageAccessTokens: "Gerenciar tokens de acesso" -accountInfo: "Informações da conta" -notesCount: "Número de notas" -repliesCount: "Número de respostas enviadas" -renotesCount: "Número de repostagens feitas" -repliedCount: "Número de respostas recebidas" -renotedCount: "Números de repostagens recebidas" -followingCount: "Número de contas seguidas" -followersCount: "Número de seguidores" -sentReactionsCount: "Número de reações enviadas" -receivedReactionsCount: "Número de reações recebidas" -pollVotesCount: "Número de votos feitos em enquetes" -pollVotedCount: "Número de votos recebidos em enquetes" -yes: "Sim" -no: "Não" -driveFilesCount: "Número de arquivos no drive" -driveUsage: "Capacidade do drive" -noCrawle: "Recusar indexação por crawler" -noCrawleDescription: "Solicitar que os mecanismos de pesquisa externos não indexem o conteúdo de suas páginas de usuário, notas, páginas etc." -lockedAccountInfo: "Mesmo que você defina a aprovação para seguir, a menos que você defina o alcance da nota para 'Apenas seguidores', qualquer pessoa poderá ver suas notas." -alwaysMarkSensitive: "Marcar como sensível por padrão" -loadRawImages: "Exibir as imagens originais ao invés de miniaturas" -disableShowingAnimatedImages: "Não reproduzir imagens animadas" -highlightSensitiveMedia: "Destacar mídia sensível" -verificationEmailSent: "Um e-mail de confirmação foi enviado. Siga o link no e-mail para concluir a verificação." -notSet: "Não definido" -emailVerified: "O endereço de e-mail foi confirmado" -noteFavoritesCount: "Número de notas salvas nos favoritos" -pageLikesCount: "Número de páginas curtidas" -pageLikedCount: "Número de curtidas recebidas nas suas páginas" -contact: "Contato" -useSystemFont: "Utilizar a fonte padrão do sistema" -clips: "Clipe" -experimentalFeatures: "Funcionalidades Experimentais" -experimental: "Experimental" -thisIsExperimentalFeature: "Este é um recurso experimental. As funções podem mudar ou pode não funcionar corretamente." -developer: "Programador" -makeExplorable: "Deixe a sua conta encontrável em \"Explorar\"." -makeExplorableDescription: "Se você desativá-lo, outros usuários não poderão encontrar a sua conta na aba Descoberta." -duplicate: "Duplicar" -left: "Esquerda" -center: "Centralizar" -wide: "Largo" -narrow: "Estreito" -reloadToApplySetting: "As configurações serão refletidas após recarregar a página. Deseja recarregar agora?" -needReloadToApply: "É necessário recarregar a página para refletir as alterações." -needToRestartServerToApply: "É necessário reiniciar o servidor para aplicar as mudanças." -showTitlebar: "Exibir barra de título" -clearCache: "Limpar o cache" -onlineUsersCount: "{n} Pessoas Online" -nUsers: "{n} Usuários" -nNotes: "{n} Notas" -sendErrorReports: "Enviar relatórios de erro" -sendErrorReportsDescription: "Ao ativar essa opção, informações detalhadas de erro serão compartilhadas com o Misskey em caso de problemas, o que pode ajudar a melhorar a qualidade do software. As informações de erro podem incluir a versão do sistema operacional, o tipo de navegador e o sua atividade no Misskey." -myTheme: "Meu tema" -backgroundColor: "Cor de fundo" -accentColor: "Cor de destaque" -textColor: "Cor do texto" -saveAs: "Salvar como" -advanced: "Avançado" -advancedSettings: "Configurações avançadas" -value: "Valor" -createdAt: "Data de criação" -updatedAt: "Última atualização" -saveConfirm: "Deseja salvá-lo?" -deleteConfirm: "Confirma a exclusão?" -invalidValue: "Valor inválido" -registry: "Registo" -closeAccount: "Encerrar conta" -currentVersion: "Versão Atual" -latestVersion: "Última versão" -youAreRunningUpToDateClient: "Você está usando a última versão do cliente" -newVersionOfClientAvailable: "Nova versão do cliente disponível" -usageAmount: "Quantidade utilizada" -capacity: "Capacidade" -inUse: "Em uso" -editCode: "Editar código" -apply: "Aplicar" -receiveAnnouncementFromInstance: "Receba as notificações da instância" -emailNotification: "Notificações por e-mail" -publish: "Publicar" -inChannelSearch: "Pesquisar no canal" -useReactionPickerForContextMenu: "Clique com o botão direito do mouse para abrir o seletor de reações." -typingUsers: "{users} pessoas digitando" -jumpToSpecifiedDate: "Pular para uma data específica" -showingPastTimeline: "Visualizar linha de tempo anterior" -clear: "Limpar" -markAllAsRead: "Marcar todas como lidas" -goBack: "Voltar" -unlikeConfirm: "Deseja realmente deixar de curtir?" -fullView: "Visão completa" -quitFullView: "Sair da visualização completa" -addDescription: "Adicionar descrição" -userPagePinTip: "Notas podem ser mostradas aqui ao clicar em \"Fixar no Perfil\" no menu de notas individuais." -notSpecifiedMentionWarning: "Esta nota menciona usuários que não foram incluídos como recipientes." +clearCache: "Limpar memória transitória" info: "Informações" -userInfo: "Informações do Usuário" -unknown: "Desconhecido" -onlineStatus: "On-line" -hideOnlineStatus: "Ocultar o status on-line." -hideOnlineStatusDescription: "Esconder que está Ativo reduzirá a utilidade de certas funções (como, por exemplo, a Pesquisa)." -online: "Online" -active: "Ativo" -offline: "Inativo" -notRecommended: "Não recomendado" -botProtection: "Proteção contra Bot" -instanceBlocking: "Instâncias bloqueadas" -selectAccount: "Selecionar conta" -switchAccount: "Trocar conta" -enabled: "Ativado" -disabled: "Desativado" -quickAction: "Ações rápidas" -user: "Usuário" -administration: "Administrar" -accounts: "Contas" -switch: "Trocar" -noMaintainerInformationWarning: "A informação de administrador não foi configurada." -noInquiryUrlWarning: "URL de consulta não está definida" -noBotProtectionWarning: "A proteção contra bots não foi configurada." -configure: "Configurar" -postToGallery: "Criar publicação em galeria" -postToHashtag: "Publicar nesta Hashtag" -gallery: "Galeria" -recentPosts: "Notas recentes" -popularPosts: "Notas populares" -shareWithNote: "Compartilhar em Notas" -ads: "Anúncios" -expiration: "Data limite" -startingperiod: "Data de início" -memo: "Nota" -priority: "Prioridade" -high: "Alto" -middle: "Meio" -low: "Baixo" -emailNotConfiguredWarning: "Endereço de e-mail não configurado. " -ratio: "Ratio" -previewNoteText: "Visualizar Nota" -customCss: "CSS Personalizado" -customCssWarn: "Esta configuração só deve ser usada se souber o que está fazendo. Valores impróprios podem causar erros no funcionamento do cliente." -global: "Global" -squareAvatars: "Exibir ícones quadrados" -sent: "Enviar" -received: "Recebido" -searchResult: "Pesquisar" -hashtags: "Hashtags" -troubleshooting: "Resolução de problemas" -useBlurEffect: "Usar efeito de desfoque na UI" -learnMore: "Saiba mais" -misskeyUpdated: "Misskey foi atualizado!" -whatIsNew: "Ver atualizações" -translate: "Traduzir" -translatedFrom: "Traduzido de {x}" -accountDeletionInProgress: "Encerramento de conta em andamento" -usernameInfo: "O nome para identificar exclusivamente a sua conta no servidor. Pode conter letras (az, AZ), números (0~9) e sublinhados (_). O nome de usuário não pode ser alterado posteriormente." -aiChanMode: "Modo AI-chan" -devMode: "Modo de Desenvolvedor" -keepCw: "Manter aviso de conteúdo" -pubSub: "Publicar/Inscrever no perfil" -lastCommunication: "Ultima atualização" -resolved: "Resolvido" -unresolved: "Não resolvido" -breakFollow: "Remover seguidor" -breakFollowConfirm: "Deseja realmente deixar de seguir?" -itsOn: "Ativado" -itsOff: "Desativado" -on: "Ligado" -off: "Desligado" -emailRequiredForSignup: "Tornar o endereço de e-mail obrigatório durante o cadastro" -unread: "Não lido" -filter: "Filtrar" -controlPanel: "Painel de controle" -manageAccounts: "Gerenciar contas" -makeReactionsPublic: "Deixar o histórico de reações em Público" -makeReactionsPublicDescription: "Isto vai deixar o histórico de todas as suas reações visíveis para qualquer um ver." -classic: "Clássico" -muteThread: "Silenciar esta conversa" -unmuteThread: "Desativar silêncio desta conversa" -followingVisibility: "Visibilidade dos usuários seguidos" -followersVisibility: "Visibilidade dos seguidores" -continueThread: "Ver mais desta conversa" -deleteAccountConfirm: "Deseja realmente excluir a conta?" -incorrectPassword: "Senha inválida." -incorrectTotp: "A senha de uso único está incorreta ou expirou." -voteConfirm: "Deseja confirmar o seu voto em \"{choice}\"?" -hide: "Ocultar" -useDrawerReactionPickerForMobile: "Mostrar em formato de gaveta" -welcomeBackWithName: "Bem-vindo de volta, {name}" -clickToFinishEmailVerification: "Clique em [{ok}] para completar a validação do endereço de e-mail." -overridedDeviceKind: "Sobrepor dispositivo" -smartphone: "Celular" -tablet: "Tablet" -auto: "Automático" -themeColor: "Cor do tema" -size: "Tamanho" -numberOfColumn: "Número da coluna" -searchByGoogle: "Pesquisar" -instanceDefaultLightTheme: "Tema diurno padrão para toda a instância" -instanceDefaultDarkTheme: "Tema noturno para toda a instância" -instanceDefaultThemeDescription: "Insira o código do tema em formato de objeto." -mutePeriod: "Duração de silenciamento" -period: "Data limite" -indefinitely: "Indefinitivamente" -tenMinutes: "10 minutos" -oneHour: "1 hora" -oneDay: "1 dia" -oneWeek: "1 semana" -oneMonth: "1 mês" -threeMonths: "3 meses" -oneYear: "1 ano" -threeDays: "3 dias" -reflectMayTakeTime: "As mudanças podem demorar a aparecer." -failedToFetchAccountInformation: "Não foi possível obter informações da conta" -rateLimitExceeded: "Taxa limite excedido" -cropImage: "Recortar imagem" -cropImageAsk: "Deseja recortar esta imagem?" -cropYes: "Recortar" -cropNo: "Manter deste jeito" +user: "Usuários" +searchByGoogle: "Buscar" file: "Ficheiros" -recentNHours: "Últimas {n} horas" -recentNDays: "Últimos {n} dias" -noEmailServerWarning: "Servidor de e-mail não configurado." -thereIsUnresolvedAbuseReportWarning: "Existem denúncias não resolvidas." -recommended: "Recomendado" -check: "Verificar" -driveCapOverrideLabel: "Altere a capacidade do drive para este usuário" -driveCapOverrideCaption: "Altere a capacidade para o valor padrão informando o valor 0 ou inferior." -requireAdminForView: "Para visualizar, é necessário acessar com uma conta de administrador." -isSystemAccount: "É uma conta criada e gerenciada automaticamente pelo sistema." -typeToConfirm: "Para realizar essa operação, digite {x}." -deleteAccount: "Excluir conta" -document: "Documentação" -numberOfPageCache: "Número de cache de página" -numberOfPageCacheDescription: "Aumentar isso melhora a conveniência, mas também resulta em maior carga e uso de memória." -logoutConfirm: "Gostaria de encerrar a sessão?" -logoutWillClearClientData: "Sair irá remover as configurações do cliente do navegador. Para redefinir as configurações ao entrar, você deve habilitar o backup automático de configurações." -lastActiveDate: "Última data de uso" -statusbar: "Barra de status" -pleaseSelect: "Por favor, selecione." -reverse: "Inversão" -colored: "Colorido" -refreshInterval: "Intervalo de atualização" -label: "Etiqueta" -type: "Tipo" -speed: "Velocidade" -slow: "Lento" -fast: "Rápido" -sensitiveMediaDetection: "Detecção de conteúdo sensível" -localOnly: "Apenas local" -remoteOnly: "Apenas remoto" -failedToUpload: "Falha ao enviar" -cannotUploadBecauseInappropriate: "Esse arquivo não pôde ser enviado porque partes dele foram detectadas como potencialmente inapropriadas." -cannotUploadBecauseNoFreeSpace: "Envio falhou devido à falta de capacidade no Drive." -cannotUploadBecauseExceedsFileSizeLimit: "Não é possível realizar o upload deste arquivo porque ele excede o tamanho máximo permitido." -beta: "Beta" -enableAutoSensitive: "Marcar automaticamente como conteúdo sensível" -enableAutoSensitiveDescription: "Quando disponível, a marcação de mídia sensível será automaticamente atribuído ao conteúdo de mídia usando aprendizado de máquina. Mesmo que você desative essa função, em alguns servidores, isso pode ser configurado automaticamente." -activeEmailValidationDescription: "A validação do endereço de e-mail do usuário será realizada de forma mais rigorosa, considerando se é um endereço descartável ou se é possível realizar comunicação efetiva. Se desativado, apenas a validade do formato do endereço será verificada como uma sequência de caracteres." -navbar: "Barra de navegação" -shuffle: "Aleatório" -account: "Contas" -move: "Mover" -pushNotification: "Notificações Push" -subscribePushNotification: "Ativar notificações push" -unsubscribePushNotification: "Desativar notificações push" -pushNotificationAlreadySubscribed: "Notificações push já estão habilitadas" -pushNotificationNotSupported: "Seu navegador ou instância não tem suporte às notificações push" -sendPushNotificationReadMessage: "Apagar notificações push quando elas foram lidas" -sendPushNotificationReadMessageCaption: "Pode aumentar o consumo de energia do dispositivo." -windowMaximize: "Maximizar" -windowMinimize: "Minimizar" -windowRestore: "Restaurar" -caption: "legenda" -loggedInAsBot: "Atualmente conectado como bot" -tools: "Ferramentas" -cannotLoad: "Não foi possível carregar" -numberOfProfileView: "Visualizações do perfil" -like: "Curtir" -unlike: "Remover curtida" -numberOfLikes: "Número de curtidas" -show: "Visualizar" -neverShow: "Não exibir novamente" -remindMeLater: "Lembrar mais tarde" -didYouLikeMisskey: "Você gostou do Misskey?" -pleaseDonate: "O Misskey é um software gratuito utilizado por {host}. Para que possamos continuar o desenvolvimento, pedimos que considerem fazer doações. A sua contribuição é muito importante!" -correspondingSourceIsAvailable: "O código-fonte correspondente está disponível em {anchor}" -roles: "Cargos" -role: "Cargo" -noRole: "Nenhum cargo" -normalUser: "Usuários padrão" -undefined: "Indefinido" -assign: "Atribuir" -unassign: "Remover" -color: "Cor" -manageCustomEmojis: "Gerenciar Emojis customizados" -manageAvatarDecorations: "Gerenciar decorações de avatar" -youCannotCreateAnymore: "Você atingiu o limite de criação." -cannotPerformTemporary: "Ação temporariamente indisponível" -cannotPerformTemporaryDescription: "Esta ação não pôde ser concluída devido ao excesso de pedidos em sucessão. Tente novamente em alguns momentos." -invalidParamError: "Parâmetros inválidos" -invalidParamErrorDescription: "Parâmetros requisitados inválidos. Isto normalmente acontece devido a um erro, mas também pode ocorrer devido à entrada de valores além do limite ou algo semelhante." -permissionDeniedError: "Operação recusada" -permissionDeniedErrorDescription: "Esta conta não tem permissão para executar esta ação." -preset: "Predefinições" -selectFromPresets: "Escolher de predefinições" -achievements: "Conquistas" -gotInvalidResponseError: "Resposta do servidor inválida" -gotInvalidResponseErrorDescription: "Servidor fora do ar ou em manutenção. Favor tentar mais tarde." -thisPostMayBeAnnoying: "Esta nota pode incomodar outras pessoas." -thisPostMayBeAnnoyingHome: "Postar na linha do tempo inicial" -thisPostMayBeAnnoyingCancel: "Cancelar" -thisPostMayBeAnnoyingIgnore: "Postar mesmo assim" -collapseRenotes: "Ocultar repostagens já visualizadas" -collapseRenotesDescription: "Colapsar notas em que você reagiu ou repostou." -internalServerError: "Erro interno de servidor" -internalServerErrorDescription: "Houve um erro inesperado no servidor." -copyErrorInfo: "Copiar detalhes de erro" -joinThisServer: "Cadastrar-se na instância" -exploreOtherServers: "Buscar outra instância" -letsLookAtTimeline: "Dar uma olhada na linha do tempo" -disableFederationConfirm: "Realmente desabilitar a federação?" -disableFederationConfirmWarn: "Mesmo se defederado, publicações continuarão sendo públicas, a menos que seja definido o contrário. Você geralmente não precisa disso." -disableFederationOk: "Desabilitar" -invitationRequiredToRegister: "Essa instância é apenas para convidados. Você precisa inserir um código válido para se cadastrar." -emailNotSupported: "O envio de e-mails não é suportado nesta instância" -postToTheChannel: "Publicar ao canal" -cannotBeChangedLater: "Isso não pode ser alterado." -reactionAcceptance: "Aceitação de Reações" -likeOnly: "Apenas curtidas" -likeOnlyForRemote: "Tudo (somente curtidas remotas)" -nonSensitiveOnly: "Apenas não-sensível" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Apenas não sensíveis (somente curtidas remotas)" -rolesAssignedToMe: "Cargos atribuídos a mim" -resetPasswordConfirm: "Deseja realmente mudar a sua senha?" -sensitiveWords: "Palavras sensíveis" -sensitiveWordsDescription: "A visibilidade de todas as notas contendo as palavras configuradas será colocadas como \"Início\" automaticamente. Você pode listar várias delas separando-as por linha." -sensitiveWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)" -prohibitedWords: "Palavras proibidas" -prohibitedWordsDescription: "Habilita um erro ao tentar publicar uma nota contendo as palavras escolhidas. Várias palavras podem ser escolhidas, separando-as por linha." -prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)" -hiddenTags: "Hashtags escondidas" -hiddenTagsDescription: "Selecione tags que não serão exibidas na lista de destaques. Várias tags podem ser escolhidas, separadas por linha." -notesSearchNotAvailable: "A pesquisa de notas está indisponível." -license: "Licença" -unfavoriteConfirm: "Deseja realmente remover dos favoritos?" -myClips: "Meus clipes" -drivecleaner: "Limpeza do drive" -retryAllQueuesNow: "Tentar novamente todas as pendências" -retryAllQueuesConfirmTitle: "Gostaria de tentar novamente agora?" -retryAllQueuesConfirmText: "Isso irá temporariamente aumentar a carga do servidor." -enableChartsForRemoteUser: "Gerar gráficos estatísticos de usuários remotos" -enableChartsForFederatedInstances: "Gerar gráficos estatísticos de instâncias remotas" -enableStatsForFederatedInstances: "Receber estatísticas de servidores remotos" -showClipButtonInNoteFooter: "Adicionar \"Clip\" ao menu de ação de notas" -reactionsDisplaySize: "Tamanho de exibição das reações" -limitWidthOfReaction: "Limita o comprimento máximo de reações e as exibe em tamanho reduzido" -noteIdOrUrl: "ID ou URL de nota" -video: "Vídeo" -videos: "Vídeos" -audio: "Áudio" -audioFiles: "Áudio" -dataSaver: "Economia de Dados" -accountMigration: "Migração da Conta" -accountMoved: "Esse usuário moveu-se para uma nova conta:" -accountMovedShort: "Essa conta foi migrada." -operationForbidden: "Operação proibída" -forceShowAds: "Sempre mostrar propagandas" -addMemo: "Adicionar memorando" -editMemo: "Editar memorando" -reactionsList: "Reações" -renotesList: "Repostagens" -notificationDisplay: "Notificações" -leftTop: "Superior esquerdo" -rightTop: "Superior direito" -leftBottom: "Inferior esquerdo" -rightBottom: "Inferior direito" -stackAxis: "Eixo de empilhamento" -vertical: "Vertical" -horizontal: "Exibir painel lateral inteiro" -position: "Posição" -serverRules: "Regras do servidor" -pleaseConfirmBelowBeforeSignup: "Para cadastrar-se no servidor, você precisa ler e concordar como seguinte:" -pleaseAgreeAllToContinue: "Você precisa concordar com todos os campos acima para continuar." -continue: "Continuar" -preservedUsernames: "Nomes de usuário reservados" -preservedUsernamesDescription: "Liste os nomes de usuário que deseja reservar, separando-os por quebras de linha. Os nomes de usuário especificados aqui não poderão ser utilizados durante a criação de contas. No entanto, esta restrição não se aplica quando a conta é criada por um administrador. Além disso, as contas que já existem não serão afetadas." -createNoteFromTheFile: "Compor nota a partir desse arquivo" -archive: "Arquivo" -archived: "Arquivado" -unarchive: "Desarquivar" -channelArchiveConfirmTitle: "Deseja realmente arquivar {name}?" -channelArchiveConfirmDescription: "Um canal arquivado não irá aparecer na lista de canais e nem resultados de pesquisa. Novas publicações não poderão mais ser adicionadas." -thisChannelArchived: "Esse canal foi arquivado." -displayOfNote: "Exibição de nota" -initialAccountSetting: "Configuração inicial do perfil" -youFollowing: "Seguindo" -preventAiLearning: "Rejeitar uso de Aprendizado de Máquina (IA Generativa)" -preventAiLearningDescription: "Solicita-se que o conteúdo de notas e imagens enviadas não seja usado como objeto de aprendizado por sistemas externos de geração de texto ou imagens. Isso é alcançado incluindo a flag 'noai' na resposta HTML. No entanto, o cumprimento dessa solicitação depende do próprio sistema de IA, portanto, não é garantia total de prevenção de aprendizado." -options: "Opções" -specifyUser: "Usuário específico" -lookupConfirm: "Deseja buscar?" -openTagPageConfirm: "Deseja abrir a uma página de hashtag?" -specifyHost: "Especificar um hospedeiro" -failedToPreviewUrl: "Não foi possível carregar prévia" -update: "Atualizar" -rolesThatCanBeUsedThisEmojiAsReaction: "Cargos que podem utilizar este emoji como reação" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Se nenhum cargo for especificado, qualquer pessoa pode usar este emoji como reação." -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Estes cargos devem ser públicos." -cancelReactionConfirm: "Realmente excluir a sua reação?" -changeReactionConfirm: "Realmente mudar a sua reação?" -later: "Talvez mais tarde" -goToMisskey: "Ao Misskey" -additionalEmojiDictionary: "Dicionários adicionais de emoji" -installed: "Instalado" -branding: "Marca" -enableServerMachineStats: "Publicar estatísticas do hardware do servidor" -enableIdenticonGeneration: "Habilitar geração de identicon de usuário" -turnOffToImprovePerformance: "Desligar isso pode melhorar o desempenho." -createInviteCode: "Gerar convite" -createWithOptions: "Criar com opções" -createCount: "Número de convites" -inviteCodeCreated: "Convite gerado" -inviteLimitExceeded: "Você excedeu o limite de convites que podem ser gerados." -createLimitRemaining: "Limite de convites: {limit}" -inviteLimitResetCycle: "Esse limite irá tornar-se {limit} em {time}." -expirationDate: "Data de expiração" -noExpirationDate: "Sem expiração" -inviteCodeUsedAt: "Código de convite usado em" -registeredUserUsingInviteCode: "Convite usado por" -waitingForMailAuth: "Verificação de e-mail pendente " -inviteCodeCreator: "Convite criado por" -usedAt: "Usado em" -unused: "Não foi usado" -used: "Usado" -expired: "Expirado" -doYouAgree: "Concorda?" -beSureToReadThisAsItIsImportant: "Por favor, leia essa informação importante." -iHaveReadXCarefullyAndAgree: "Eu li o texto \"{x}\" e concordo." -dialog: "Diálogo" -icon: "Avatar" -forYou: "Para você" -currentAnnouncements: "Anúncios atuais" -pastAnnouncements: "Anúncios passados" -youHaveUnreadAnnouncements: "Há anúncios não lidos." -useSecurityKey: "Por favor, siga as instruções do seu navegador ou dispositivo para utilizar uma chave de acesso." -replies: "Responder" -renotes: "Repostar" -loadReplies: "Mostrar respostas" -loadConversation: "Mostrar conversa" -pinnedList: "Lista fixada" -keepScreenOn: "Manter a tela do dispositivo sempre ligada" -verifiedLink: "A autoria do link foi verificada" -notifyNotes: "Notificar sobre novas notas" -unnotifyNotes: "Deixar de notificar sobre novas notas" -authentication: "Autenticação" -authenticationRequiredToContinue: "Por favor, autentique-se para continuar" -dateAndTime: "Data e Hora" -showRenotes: "Exibir reposts" -edited: "Editado" -notificationRecieveConfig: "Configurações de Notificação" -mutualFollow: "Seguidor mútuo" -followingOrFollower: "Seguidor ou usuário seguido" -fileAttachedOnly: "Apenas notas com arquivos" -showRepliesToOthersInTimeline: "Mostrar respostas aos outros na linha do tempo" -hideRepliesToOthersInTimeline: "Esconder respostas dos outros na linha do tempo" -showRepliesToOthersInTimelineAll: "Mostrar respostas aos outros, mas apenas de quem você segue, na linha do tempo" -hideRepliesToOthersInTimelineAll: "Esconder respostas de todos que você segue na linha do tempo" -confirmShowRepliesAll: "Essa operação é irreversível. Você gostaria de mostrar respostas a todos que você segue na sua linha do tempo?" -confirmHideRepliesAll: "Essa operação é irreversível. Você gostaria de esconder respostas a todos que você segue na sua linha do tempo?" -externalServices: "Serviços Externos" -sourceCode: "Código-fonte" -sourceCodeIsNotYetProvided: "Código-fonte está indisponível. Contate o administrador para resolver esse problema." -repositoryUrl: "URL do repositório" -repositoryUrlDescription: "Se você estiver utilizando Misskey como está (sem mudanças no código-fonte), insira https://github.com/misskey-dev/misskey" -repositoryUrlOrTarballRequired: "Se você não publicou um repositório, você precisa providenciar uma tarball em seu lugar. Veja .config/example.yml para mais informações." -feedback: "Feedback" -feedbackUrl: "Link para Feedback" -impressum: "Impressum" -impressumUrl: "URL de 'Impressum'" -impressumDescription: "Em alguns países, como a Alemanha, a inclusão de informação de contato do operador de um serviço é legalmente exigida para websites comerciais." -privacyPolicy: "Política de Privacidade" -privacyPolicyUrl: "URL da Política de Privacidade" -tosAndPrivacyPolicy: "Termos de Serviço e Política de Privacidade" -avatarDecorations: "Decorações de avatar" -attach: "Anexar" -detach: "Remover" -detachAll: "Remover Tudo" -angle: "Ângulo" -flip: "Inversão" -showAvatarDecorations: "Exibir decorações de avatar" -releaseToRefresh: "Solte para atualizar" -refreshing: "Atualizando..." -pullDownToRefresh: "Puxe para baixo para atualizar" -useGroupedNotifications: "Agrupar notificações" -signupPendingError: "Houve um problema ao verificar o endereço de email. O link pode ter expirado." -cwNotationRequired: "Se \"Esconder conteúdo\" está habilitado, uma descrição deve ser adicionada." -doReaction: "Adicionar reação" -code: "Código" -reloadRequiredToApplySettings: "É necessário reiniciar para aplicar as configurações." -remainingN: "Restante: {n}" -overwriteContentConfirm: "Você tem certeza de que deseja sobrescrever o conteúdo atual?" -seasonalScreenEffect: "Efeito de Tela Sazonal" -decorate: "Decorar" -addMfmFunction: "Adicionar MFM" -enableQuickAddMfmFunction: "Exibir seleção avançada de MFM" -bubbleGame: "Bubble Game" -sfx: "Efeitos Sonoros" -soundWillBePlayed: "Sons serão reproduzidos" -showReplay: "Ver Replay" -replay: "Replay" -replaying: "Mostrando Replay" -endReplay: "Sair do Replay" -copyReplayData: "Copiar dados de Replay" -ranking: "Ranking" -lastNDays: "Últimos {n} dias" -backToTitle: "Voltar à página inicial" -hemisphere: "Onde você se localiza" -withSensitive: "Incluir notas com arquivos sensíveis" -userSaysSomethingSensitive: "Publicação de {name} contém conteúdo sensível" -enableHorizontalSwipe: "Arraste para mudar de aba" -loading: "Carregando" -surrender: "Cancelar" -gameRetry: "Tentar Novamente" -notUsePleaseLeaveBlank: "Deixe em branco caso inutilizado" -useTotp: "Digite a senha de uso único" -useBackupCode: "Usar códigos de “backup”" -launchApp: "Iniciar aplicação" -useNativeUIForVideoAudioPlayer: "Utilizar UI do navegador ao reproduzir vídeo e áudio" -keepOriginalFilename: "Manter nome original do arquivo" -keepOriginalFilenameDescription: "Se você desabilitar essa opção, os nomes de arquivos serão substituídos por uma sequência aleatória ao enviar arquivos." -noDescription: "Não há descrição" -alwaysConfirmFollow: "Sempre confirmar ao seguir" -inquiry: "Contato" -tryAgain: "Por favor, tente novamente mais tarde" -confirmWhenRevealingSensitiveMedia: "Confirmar ao revelar mídia sensível" -sensitiveMediaRevealConfirm: "Essa mídia pode ser sensível. Deseja revelá-la?" -createdLists: "Listas criadas" -createdAntennas: "Antenas criadas" -fromX: "De {x}" -genEmbedCode: "Gerar código de embed" -noteOfThisUser: "Notas por este usuário" -clipNoteLimitExceeded: "Não é possível adicionar mais notas ao clipe." -performance: "Desempenho" -modified: "Modificado" -discard: "Descartar" -thereAreNChanges: "Há {n} mudança(s)" -signinWithPasskey: "Entrar com Passkey" -unknownWebAuthnKey: "Passkey desconhecida" -passkeyVerificationFailed: "A verificação com Passkey falhou." -passkeyVerificationSucceededButPasswordlessLoginDisabled: "A verificação com Passkey teve êxito, mas a entrada sem senha está desabilitada." -messageToFollower: "Mensagem aos seguidores" -target: "Alvo" -testCaptchaWarning: "Essa função é utilizada apenas para testar CAPTCHA. Não a use num ambiente de produção." -prohibitedWordsForNameOfUser: "Palavras proibidas para nomes de usuário" -prohibitedWordsForNameOfUserDescription: "Se quaisquer palavras dessa lista forem incluídas no nome de usuário, seu uso será negado. Usuários com privilégios de moderador não serão afetados pela restrição." -yourNameContainsProhibitedWords: "O seu nome possui palavras proibidas" -yourNameContainsProhibitedWordsDescription: "Se você deseja utilizar esse nome, entre em contato com o administrador do servidor." -thisContentsAreMarkedAsSigninRequiredByAuthor: "O autor exige que você esteja cadastrado para ver" -lockdown: "Lockdown" -pleaseSelectAccount: "Selecione uma conta" -availableRoles: "Cargos disponíveis" -acknowledgeNotesAndEnable: "Ative após compreender as precauções." -federationSpecified: "Esse servidor opera com uma lista branca de federação. Interagir com servidores diferentes daqueles designados pela administração não é permitido." -federationDisabled: "Federação está desabilitada nesse servidor. Você não pode interagir com usuários de outros servidores." -confirmOnReact: "Confirmar ao reagir" -reactAreYouSure: "Você deseja adicionar uma reação \"{emoji}\"?" -markAsSensitiveConfirm: "Você deseja definir essa mídia como sensível?" -unmarkAsSensitiveConfirm: "Você deseja remover a definição dessa mídia como sensível?" -preferences: "Preferências" -accessibility: "Acessibilidade" -preferencesProfile: "Perfil de preferências" -copyPreferenceId: "Copiar ID de preferências" -resetToDefaultValue: "Reverter ao padrão" -overrideByAccount: "Sobrescrever pela conta" -untitled: "Sem título" -noName: "Sem nome" -skip: "Pular" -restore: "Redefinir" -syncBetweenDevices: "Sincronizar entre dispositivos" -preferenceSyncConflictTitle: "O valor configurado já existe no servidor." -preferenceSyncConflictText: "As preferências com a sincronização ativada irão salvar os seus valores no servidor. Porém, já existem valores no servidor. Qual conjunto de valores você deseja sobrescrever?" -preferenceSyncConflictChoiceServer: "Valor configurado no servidor" -preferenceSyncConflictChoiceDevice: "Valor configurado no dispositivo" -preferenceSyncConflictChoiceCancel: "Cancelar a habilitação de sincronização" -paste: "Colar" -emojiPalette: "Paleta de emojis" -postForm: "Campo de postagem" -textCount: "Contagem de caracteres" -information: "Informações" -chat: "Conversas" -migrateOldSettings: "Migrar configurações antigas de cliente" -migrateOldSettings_description: "Isso deve ser feito automaticamente. Caso o processo de migração tenha falhado, você pode acioná-lo manualmente. As informações atuais de migração serão substituídas." -compress: "Comprimir" -right: "Direita" -bottom: "Inferior" -top: "Superior" -embed: "Embed" -settingsMigrating: "Configurações estão sendo migradas, aguarde... (Você pode migrar manualmente em Configurações→Outros→Migrar configurações antigas de cliente)" -readonly: "Ler apenas" -goToDeck: "Voltar ao Deck" -federationJobs: "Tarefas de Federação" -driveAboutTip: "No Drive, uma lista de arquivos enviados no passado será exibida.
\nVocê pode reutilizar esses arquivos anexando-os às notas, ou você pode enviar arquivos para publicar posteriormente.
\nCuidado ao excluir um arquivo, pois ele será removido de quaisquer outros lugares onde está sendo utilizado (notas, páginas, avatares, banners, etc.)
\nVocê também pode criar pastas para organizar seus arquivos." -scrollToClose: "Role a página para fechar" -advice: "Dica" -realtimeMode: "Modo tempo-real" -turnItOn: "Ativar" -turnItOff: "Desativar" -emojiMute: "Silenciar emoji" -emojiUnmute: "Reativar emoji" -muteX: "Silenciar {x}" -unmuteX: "Reativar {x}" -_chat: - noMessagesYet: "Ainda não há mensagens" - newMessage: "Nova mensagem" - individualChat: "Conversa Particular" - individualChat_description: "Ter uma conversa particular com outra pessoa." - roomChat: "Conversa de Grupo" - roomChat_description: "Uma sala de conversas com várias pessoas. Você pode adicionar pessoas que não permitem conversas privadas se elas aceitarem o convite." - createRoom: "Criar Sala" - inviteUserToChat: "Convide usuários para começar a conversar" - yourRooms: "Salas criadas" - joiningRooms: "Salas ingressadas" - invitations: "Convidar" - noInvitations: "Sem convites" - history: "Histórico" - noHistory: "Ainda não há histórico" - noRooms: "Nenhuma sala encontrada" - inviteUser: "Convidar Usuários" - sentInvitations: "Convites Enviados" - join: "Entrar" - ignore: "Ignorar" - leave: "Deixar sala" - members: "Membros" - searchMessages: "Pesquisar mensagens" - home: "Início" - send: "Enviar" - newline: "Nova linha" - muteThisRoom: "Silenciar sala" - deleteRoom: "Excluir sala" - chatNotAvailableForThisAccountOrServer: "Conversas não estão habilitadas nesse servidor ou para essa conta." - chatIsReadOnlyForThisAccountOrServer: "Conversas são apenas para leitura nesse servidor ou para essa conta. Não é possível escrever novas mensagens ou criar/ingressar novas conversas." - chatNotAvailableInOtherAccount: "A função de conversas está desabilitadas para o outro usuário." - cannotChatWithTheUser: "Não é possível conversar com esse usuário." - cannotChatWithTheUser_description: "Conversas estão indisponíveis ou o outro usuário não as habilitou." - youAreNotAMemberOfThisRoomButInvited: "Você não é um participante da sala, mas recebeu um convite. Por favor, aceite o convite para entrar." - doYouAcceptInvitation: "Aceita o convite?" - chatWithThisUser: "Conversar com usuário" - thisUserAllowsChatOnlyFromFollowers: "Esse usuário aceita conversar apenas com seguidores." - thisUserAllowsChatOnlyFromFollowing: "Esse usuário aceita conversar apenas com quem segue." - thisUserAllowsChatOnlyFromMutualFollowing: "Esse usuário aceita conversar apenas com seguidores mútuos." - thisUserNotAllowedChatAnyone: "Esse usuário não aceita conversar com ninguém." - chatAllowedUsers: "Com quem permitir conversas" - chatAllowedUsers_note: "Você pode conversar com qualquer um com quem tenha iniciado uma conversa independente dessa configuração." - _chatAllowedUsers: - everyone: "Todos" - followers: "Seus seguidores" - following: "Quem você segue" - mutual: "Seguidores mútuos" - none: "Ninguém" -_emojiPalette: - palettes: "Paleta" - enableSyncBetweenDevicesForPalettes: "Sincronizar paleta entre dispositivos" - paletteForMain: "Paleta principal" - paletteForReaction: "Paleta de reações" -_settings: - driveBanner: "Você consegue administrar e configurar o drive, conferir o seu uso e configurar as opções de envio de arquivos." - pluginBanner: "Você pode ampliar as funções do cliente com plugins. Você pode instalar plugins, configurar e administrar individualmente." - notificationsBanner: "Você pode configurar os tipos e intervalo das notificações do servidor, além de notificações push." - api: "API" - webhook: "Webhook" - serviceConnection: "Integração de serviço" - serviceConnectionBanner: "Administre e configure tokens de acesso e webhooks para interagir com aplicações e serviços externos." - accountData: "Dados da conta" - accountDataBanner: "Exportar e importar dados da conta." - muteAndBlockBanner: "Você pode configurar meios para esconder conteúdo e restringir ações de certos usuários." - accessibilityBanner: "Você pode personalizar o visual e comportamento do cliente, além de configurar modos de otimizar o uso." - privacyBanner: "Você pode configurar a privacidade da conta por meio da visibilidade do conteúdo, capacidade de descoberta e aprovação manual de seguidores." - securityBanner: "Você pode configurar a segurança da conta em ajustes como senha, meios de entrada, aplicativos de autenticação e chaves de acesso." - preferencesBanner: "Você pode configurar o comportamento geral do cliente segundo as suas preferências." - appearanceBanner: "Você pode configurar a aparência do cliente e ajustes de tela segundo as suas preferências." - soundsBanner: "Você pode configurar a reprodução de sons no cliente." - timelineAndNote: "Notas e linha do tempo" - makeEveryTextElementsSelectable: "Tornar todos os elementos de texto selecionáveis" - makeEveryTextElementsSelectable_description: "Habilitar isso pode reduzir a usabilidade em algumas situações" - useStickyIcons: "Fazer ícones acompanharem a rolagem da tela" - enableHighQualityImagePlaceholders: "Exibir prévias para imagens de alta qualidade" - uiAnimations: "Animações de UI" - showNavbarSubButtons: "Mostrar sub-botões na barra de navegação" - ifOn: "Quando ligado" - ifOff: "Quando desligado" - enableSyncThemesBetweenDevices: "Sincronizar temas instalados entre dispositivos" - enablePullToRefresh: "Puxe para atualizar" - enablePullToRefresh_description: "Quando estiver utilizando um mouse, arraste enquanto aperta a roda de rolagem." - realtimeMode_description: "Estabelece uma conexão com o servidor e atualiza o conteúdo em tempo real. Isso pode aumentar o tráfego e uso de memória." - contentsUpdateFrequency: "Frequência da obtenção de conteúdo" - contentsUpdateFrequency_description: "Quanto maior o valor, mais o conteúdo atualiza. Porém, há uma diminuição do desempenho e aumento do tráfego e consumo de memória." - contentsUpdateFrequency_description2: "Quando o modo tempo-real está ativado, o conteúdo é atualizado em tempo real, ignorando essa opção." - _chat: - showSenderName: "Exibir nome de usuário do remetente" - sendOnEnter: "Pressionar Enter para enviar" -_preferencesProfile: - profileName: "Nome do perfil" - profileNameDescription: "Defina o nome que identifica esse dispositivo." - profileNameDescription2: "Exemplo: \"Computador Principal\", \"Celular\"" - manageProfiles: "Gerenciar Perfis" -_preferencesBackup: - autoBackup: "Backup automático" - restoreFromBackup: "Restaurar backup" - noBackupsFoundTitle: "Nenhum backup encontrado" - noBackupsFoundDescription: "Nenhum backup automático foi encontrado. Se você salvou um arquivo de backup manualmente, você pode importá-lo e restaurá-lo." - selectBackupToRestore: "Selecionar um backup para restaurar" - youNeedToNameYourProfileToEnableAutoBackup: "Um nome de perfil deve ser definido para habilitar o backup automático." - autoPreferencesBackupIsNotEnabledForThisDevice: "Backup automático de configurações não está habilitado no dispositivo." - backupFound: "Backup de configurações encontrado" -_accountSettings: - requireSigninToViewContents: "Exigir cadastro para ver o conteúdo" - requireSigninToViewContentsDescription1: "Exigir cadastro para ver todas as notas e outro conteúdo que você criou. Isso previne 'crawlers' de coletar os seus dados." - requireSigninToViewContentsDescription2: "Conteúdo não será exibido nas prévias de URL (OGP), incorporado em outras páginas web ou em servidores que não têm suporte a citações." - requireSigninToViewContentsDescription3: "Essas restrições podem não ser aplicadas a conteúdo federado de outros servidores." - makeNotesFollowersOnlyBefore: "Tornar notas passadas visíveis apenas para seguidores." - makeNotesFollowersOnlyBeforeDescription: "Com essa função ativada, apenas seguidores podem ver as notas anteriores à data e hora marcadas. Se isso for desativado, o status de publicação da nota será reestabelecido." - makeNotesHiddenBefore: "Tornar notas passadas privadas" - makeNotesHiddenBeforeDescription: "Com essa função ativada, apenas você poderá ver as notas anteriores à data e hora marcadas. Se isso for desativado, o status de publicação da nota será reestabelecido." - mayNotEffectForFederatedNotes: "Notas federadas a servidores remotos podem não ser afetadas." - mayNotEffectSomeSituations: "Essas restrições são simplificadas. Elas podem não ser aplicadas em algumas situações, como ao visualizar num servidor remoto ou durante a moderação." - notesHavePassedSpecifiedPeriod: "Notas que duraram um tempo específico." - notesOlderThanSpecifiedDateAndTime: "Notas antes do tempo específico." -_abuseUserReport: - forward: "Encaminhar" - forwardDescription: "Encaminhar a denúncia ao servidor remoto como uma conta anônima do sistema." - resolve: "Resolver" - accept: "Aceitar" - reject: "Rejeitar" - resolveTutorial: "Se a denúncia for legítima em conteúdo, selecione \"Aceitar\" para marcar o caso como resolvido afirmativamente.\nSe a denúncia for ilegítima em conteúdo, selecione \"Rejeitar\" para marcar o caso como resolvido negativamente." -_delivery: - status: "Estado de entrega" - stop: "Suspenso" - resume: "Continuar entrega" - _type: - none: "Publicando" - manuallySuspended: "Suspenso manualmente" - goneSuspended: "Servidor foi suspenso devido ao seu apagamento" - autoSuspendedForNotResponding: "Servidor foi suspenso por não responder" - softwareSuspended: "Suspenso, pois esse software não está recebendo conteúdo" -_bubbleGame: - howToPlay: "Como jogar" - hold: "Próximos" - _score: - score: "Pontuação" - scoreYen: "Dinheiro recebido" - highScore: "Melhor pontuação" - maxChain: "Número máximo de encadeamentos" - yen: "{yen} Yen" - estimatedQty: "{qty} Peças" - scoreSweets: "{onigiriQtyWithUnit} Onigiri" - _howToPlay: - section1: "Ajuste a posição e solte o objeto na caixa." - section2: "Quando dois objetos do mesmo tipo tocam-se, eles tornam-se outro objeto e você ganha pontos." - section3: "O jogo acaba quando objetos transbordam da caixa. Busque uma pontuação alta ao fundir objetos enquanto evita transbordar a caixa." -_announcement: - forExistingUsers: "Apenas aos usuários existente" - forExistingUsersDescription: "Se habilitado, esse anúncio será exibido apenas para usuários existentes no tempo de publicação. Se desabilitado, novos usuários também o receberão. " - needConfirmationToRead: "Exigir confirmação de leitura" - needConfirmationToReadDescription: "Um lembrete adicional será exibido para confirmar a leitura do anúncio. Esse anúncio também será excluído de qualquer forma de \"Marcar tudo como lido\"." - end: "Arquivar anúncio" - tooManyActiveAnnouncementDescription: "O excesso de anúncios pode atrapalhar a experiência do usuário. Considere arquivar anúncios obsoletos." - readConfirmTitle: "Marcar como lido?" - readConfirmText: "Isso marcará o conteúdo de \"{title}\" como lido." - shouldNotBeUsedToPresentPermanentInfo: "É preferível utilizar anúncios para publicar informações atuais e de curto prazo, e não informações que serão relevantes por muito tempo." - dialogAnnouncementUxWarn: "O uso de duas ou mais notificações de diálogo simultaneamente pode impactar significativamente a experiência de usuário. Portanto, utilize-as cuidadosamente." - silence: "Sem notificação" - silenceDescription: "Habilitar isso irá pular a notificação desse anúncio e o usuário não precisará lê-lo." -_initialAccountSetting: - accountCreated: "A sua conta foi criada com sucesso!" - letsStartAccountSetup: "Em primeiro lugar, vamos configurar o seu perfil." - letsFillYourProfile: "Primeiramente, vamos configurar o seu perfil." - profileSetting: "Configurações do perfil" - privacySetting: "Configurações de privacidade" - theseSettingsCanEditLater: "Você pode alterar estas configurações mais tarde." - youCanEditMoreSettingsInSettingsPageLater: "Há mais configurações na página \"Configurações\". Não se esqueça de visitá-la mais tarde." - followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo." - pushNotificationDescription: "Habilitar notificações push o possibilitará receber notificações de {name} diretamente no seu dispositivo." - initialAccountSettingCompleted: "Configuração de perfil completa!" - haveFun: "Aproveite {name}!" - youCanContinueTutorial: "Você pode iniciar um tutorial de como utilizar {name} (Misskey) ou pode sair da configuração e começar o uso imediatamente." - startTutorial: "Iniciar Tutorial" - skipAreYouSure: "Deseja pular a configuração de perfil?" - laterAreYouSure: "Deseja adiar a configuração de perfil?" -_initialTutorial: - launchTutorial: "Iniciar Tutorial" - title: "Tutorial" - wellDone: "Ótimo!" - skipAreYouSure: "Sair do Tutorial?" - _landing: - title: "Bem-vindo ao Tutorial!" - description: "Aqui, você pode aprender o básico de como usar o Misskey e as suas funções." - _note: - title: "O que é uma Nota?" - description: "Publicações no Misskey chamam-se 'Notas'. Notas são organizadas cronologicamente na linha do tempo e atualizam em tempo real." - reply: "Clique nesse botão para responder a uma mensagem. Também é possível responder respostas, continuando a conversa como uma \"thread\"." - renote: "Você pode compartilhar essa nota na sua linha do tempo. Você também pode citá-la com os seus comentários." - reaction: "Você pode adicionar reações à nota. Mais detalhes serão explicados na próxima página." - menu: "Você pode ver detalhes da nota, copiar links e realizar outras ações." - _reaction: - title: "O que são Reações?" - description: "É possível reagir às notas com diversos emojis. Reações permitem que você expresse sutilezas que não são possíveis apenas com uma curtida." - letsTryReacting: "Reações podem ser adicionadas clicando no botão \"+\". Tente reagir à nota de exemplo." - reactToContinue: "Adicione uma reação para continuar." - reactNotification: "Você receberá notificações em tempo real quando alguém reagir à sua nota." - reactDone: "Você pode desfazer uma reação ao selecionar o botão \"-\"." - _timeline: - title: "O Conceito das Linhas do Tempo" - description1: "Misskey providencia diversas linhas do tempo baseadas na sua utilidade (algumas podem não estar disponíveis a partir das configurações da instância)." - home: "Você pode ver as notas das contas seguidas. " - local: "Você pode ver notas de todos os usuários dessa instância." - social: "Notas da linha do tempo Início e Local serão exibidas." - global: "Você pode ver notas de todos os servidores conectados." - description2: "Você pode alterar dentre as linhas do tempo no todo da tela a qualquer momento." - description3: "Adicionalmente, há \"listas\" e \"canais\". Para mais informações, acesse {link}." - _postNote: - title: "Opções de Postagem de Nota" - description1: "Ao postar uma nota no Misskey, diversas opções estão disponíveis. A ficha de publicação parece com isto: " - _visibility: - description: "Você pode limitar quem vê a sua nota." - public: "Sua nota será visível a todos os usuários." - home: "Publicar apenas na linha do tempo Início. Pessoas visitando seu perfil, seja seguindo ou por um repost poderão vê-los." - followers: "Visível apenas para seguidores. Apenas seguidores podem vê-la e mais ninguém, e ela não pode ser repostada pelos demais." - direct: "Visível apenas para usuários específicos, e o destinatário será notificado. Pode ser usado como uma alternativa às mensagens diretas." - doNotSendConfidencialOnDirect1: "Tenha cuidado ao enviar informações sensíveis!" - doNotSendConfidencialOnDirect2: "Administradores do servidor podem ver o que foi escrito. Cuidado, também, ao enviar notas diretas a usuários de servidores não confiáveis." - localOnly: "Publicar com essa opção não federará a nota com outros servidores. Usuários desses servidores não poderão ver essas notas diretamente, independente das opções de visibilidade acima. " - _cw: - title: "Aviso de Conteúdo" - description: "Ao invés do corpo do texto, o conteúdo escrito na caixa \"anotação\" será exibido. Apertar \"Carregar mais\" irá revelar o corpo." - _exampleNote: - cw: "Isso irá te esfomear!" - note: "Acabei de comer um donut coberto de chocolate! 🍩😋" - useCases: "Isso pode ser usado caso seja exigido, pelas diretrizes do servidor, o cuidado com algum tópico ou ao publicar conteúdo sensível ou spoilers." - _howToMakeAttachmentsSensitive: - title: "Como Marcar Anexos como Sensíveis?" - description: "Para anexos cujo conteúdo é considerado sensível pelas diretrizes do servidor ou quando pretende-se esconder o seu conteúdo, adicione o sinal \"sensível\"." - tryThisFile: "Tente marcar a imagem anexada como sensível!" - _exampleNote: - note: "Opa, me atrapalhei abrindo a tampa do natô..." - method: "Para marcar um anexo como sensível, clique na sua miniatura, abra o menu e clique \"Marcar como sensível\"." - sensitiveSucceeded: "Ao anexar arquivos, por favor atribua uma sensibilidade coerente com as diretrizes da instância." - doItToContinue: "Marque o anexo como sensível para prosseguir." - _done: - title: "Você completou o tutorial! 🎉" - description: "As funções apresentadas aqui são apenas uma pequena parte. Para um conhecimento mais detalhado do uso do Misskey, acesse {link}." -_timelineDescription: - home: "Na linha do tempo Início, você verá notas dos usuários que você segue." - local: "Na linha do tempo Local, você verá notas de todos os usuários da instância." - social: "Na linha do tempo Social, você verá notas do Início e Local." - global: "Na linha do tempo Global, você verá notas de todas as instâncias conectadas." -_serverRules: - description: "Um grupo de regras a ser exibido antes de um cadastro. É recomendado que se faça um resumo dos Termos de Serviço." -_serverSettings: - iconUrl: "URL do ícone" - appIconDescription: "Especifica o ícone utilizado quando {host} é exibido como um app." - appIconUsageExample: "Exemplo: Como PWA, ou quando exibido num marcador de páginas ou na tela inicial de um celular" - appIconStyleRecommendation: "Como o ícone pode ser cortado para um quadrado ou círculo, é recomendado adicionar um fundo colorido na imagem." - appIconResolutionMustBe: "A resolução mínima é {resolution}." - manifestJsonOverride: "Sobrescrever manifest.json" - shortName: "Abreviação" - shortNameDescription: "Uma abreviação do nome da instância que pode ser exibido caso o nome oficial completo seja muito longo." - fanoutTimelineDescription: "Melhora significativamente a performance do retorno da linha do tempo e reduz o impacto no banco de dados quando habilitado. Em contrapartida, o uso de memória do Redis aumentará. Considere desabilitar em casos de baixa disponibilidade de memória ou instabilidade do servidor." - fanoutTimelineDbFallback: "\"Fallback\" ao banco de dados" - fanoutTimelineDbFallbackDescription: "Quando habilitado, a linha do tempo irá recuar ao banco de dados caso consultas adicionais sejam feitas e ela não estiver em cache. Quando desabilitado, o impacto no servidor será reduzido ao eliminar o recuo, mas limita a quantidade de linhas do tempo que podem ser recebidas." - reactionsBufferingDescription: "Quando ativado, o desempenho durante a criação de uma reação será melhorado substancialmente, reduzindo a carga do banco de dados. Porém, a o uso de memória do Redis irá aumentar." - inquiryUrl: "URL de inquérito" - inquiryUrlDescription: "Especifique um URL para um formulário de inquérito para a administração ou uma página web com informações de contato." - openRegistration: "Abrir a criação de contas" - openRegistrationWarning: "Abrir cadastros contém riscos. É recomendado apenas habilitá-los se houver um sistema de monitoramento contínuo e resolução imediata de problemas." - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Se nenhuma atividade da moderação for detectada por um tempo, essa configuração será desativada para prevenir spam." - deliverSuspendedSoftware: "Software Suspenso" - deliverSuspendedSoftwareDescription: "Você pode especificar uma faixa de nomes e versões do software de servidores para cancelar o envio de conteúdo por motivos como vulnerabilidades. Essa informação da versão é providenciada pelo servidor e pode não ser confiável. Uma faixa semver pode ser utilizada para especificar a versão, mas colocar '>= 2024.3.1' não incluirá versões personalizadas como '2024.3.1-custom.0'. Logo, é recomendado inserir uma especificação como '>= 2024.3.1-0'" - singleUserMode: "Modo de usuário único" - singleUserMode_description: "Se você é o único usuário desse servidor, habilitar esse modo irá otimizar a performance." - signToActivityPubGet: "Assinar solicitações GET do ActivityPub" - signToActivityPubGet_description: "Normalmente, isso deve ser habilitado. Desabilitar pode melhorar o desempenho na federação, mas também pode cortar a federação com alguns servidores." - proxyRemoteFiles: "Passar arquivos remotos por proxy" - proxyRemoteFiles_description: "Se habilitado, o servidor irá servir arquivos remotos através de um proxy. Isso é útil para gerar prévias de imagens e proteger a privacidade do usuário." - allowExternalApRedirect: "Permitir redirecionamento de conteúdo pelo ActivityPub" - allowExternalApRedirect_description: "Se habilitado, outros servidores podem solicitar conteúdo de terceiros através desse servidor, o que pode resultar em falsificação de conteúdo (spoofing)." - userGeneratedContentsVisibilityForVisitor: "Visibilidade de conteúdo dos usuários para visitantes" - userGeneratedContentsVisibilityForVisitor_description: "Isso é útil para prevenir problemas causados por conteúdo inapropriado de usuários remotos de servidores com pouca ou nenhuma moderação, que pode ser hospedado na internet a partir desse servidor." - userGeneratedContentsVisibilityForVisitor_description2: "Publicar todo o conteúdo do servidor para a internet pode ser arriscado. Isso é especialmente importante para visitantes que desconhecem a natureza distribuída do conteúdo na internet, pois eles podem acreditar que o conteúdo remoto é criado por usuários desse servidor." - _userGeneratedContentsVisibilityForVisitor: - all: "Tudo é público" - localOnly: "Conteúdo local é publicado, conteúdo remoto é privado" - none: "Tudo é privado" -_accountMigration: - moveFrom: "Migrar outra conta para essa" - moveFromSub: "Criar um 'alias' a outra conta" - moveFromLabel: "Conta original #{n}" - moveFromDescription: "Se você deseja migrar de outra conta para esta, é necessário criar um alias aqui. Por favor, insira a conta de origem da migração no seguinte formato: @username@server.example.com. Para excluir o alias, deixe o campo em branco e clique em salvar (não recomendado)." - moveTo: "Migrar dessa conta para outra" - moveToLabel: "Conta para a qual se mover:" - moveCannotBeUndone: "A migração de conta não pode ser desfeita." - moveAccountDescription: "Você está migrando para uma nova conta.\n ・Seus seguidores irão automaticamente seguir a nova conta.\n ・Todas as suas conexões de seguidores nesta conta serão removidas.\n ・Você não poderá mais criar novas notas nesta conta.\n\nA migração dos seguidores é automática, mas a migração das pessoas que você segue deve ser feita manualmente. Antes de migrar, exporte quem você está seguindo nesta conta e, assim que migrar, importe essa lista na nova conta.\nO mesmo se aplica para listas, silenciamentos e bloqueios, que também devem ser migrados manualmente.\n\n(Esta descrição se refere ao comportamento do servidor Misskey v13.12.0 ou posterior. Outros softwares ActivityPub, como Mastodon, podem ter comportamentos diferentes.)" - moveAccountHowTo: "Para realizar a migração da conta, primeiro crie um alias para esta conta no destino da migração. Após criar o alias, insira a conta de destino da migração no seguinte formato: @username@server.example.com." - startMigration: "Migrar" - migrationConfirm: "Tem certeza de que deseja migrar esta conta para '{account}'? Uma vez migrada, não poderá ser desfeita e não será possível usar esta conta novamente em seu estado original." - movedAndCannotBeUndone: "Essa conta foi migrada. A migração não pode ser desfeita." - postMigrationNote: "A remoção dos seguidores desta conta será realizada 24 horas após a operação de migração. O número de seguidores e seguidos desta conta se tornará zero. Os seguidores não serão removidos, portanto, eles continuarão a ver as postagens destinadas aos seguidores desta conta." - movedTo: "Conta para a qual se mover:" -_achievements: - earnedAt: "Data de aquisição" - _types: - _notes1: - title: "Configurando o meu misskey" - description: "Poste uma nota pela primeira vez" - flavor: "Divirta-se com o Misskey!" - _notes10: - title: "Algumas notas" - description: "Poste 10 notas" - _notes100: - title: "Um monte de notas" - description: "Poste 100 notas" - _notes500: - title: "Coberto por notas" - description: "Poste 500 notas" - _notes1000: - title: "Uma montanha de notas" - description: "Poste 1 000 notas" - _notes5000: - title: "Enxurrada de notas" - description: "Poste 5000 notas" - _notes10000: - title: "Supernota" - description: "Poste 10 000 notas" - _notes20000: - title: "Preciso... de mais... notas..." - description: "Poste 20 000 notas" - _notes30000: - title: "Notas, Notas, NOTAS!" - description: "Poste 30 000 notas" - _notes40000: - title: "Fábrica de notas" - description: "Poste 40 000 notas" - _notes50000: - title: "Planeta de notas" - description: "Poste 50 000 notas" - _notes60000: - title: "Quasar de notas" - description: "Poste 60 000 notas" - _notes70000: - title: "Buraco negro de notas" - description: "Poste 70 000 notas" - _notes80000: - title: "Galáxia de notas" - description: "Poste 80 000 notas" - _notes90000: - title: "Universo de notas" - description: "Poste 90 000 notas" - _notes100000: - title: "ALL YOUR NOTE ARE BELONG TO US" - description: "Poste 100 000 notas" - flavor: "Você realmente tem muita coisa para escrever" - _login3: - title: "Iniciante I" - description: "Faça login por um total de 3 dias" - flavor: "De hoje em diante, me chame apenas de Misskist" - _login7: - title: "Iniciante II" - description: "Faça login por um total de 7 dias" - flavor: "Pegando o jeito da coisa?" - _login15: - title: "Iniciante III" - description: "Faça login por um total de 15 dias" - _login30: - title: "Misskist I" - description: "Faça login por um total de 30 dias" - _login60: - title: "Misskist II" - description: "Faça login por um total de 60 dias" - _login100: - title: "Misskist III" - description: "Faça login por um total de 100 dias" - flavor: "Misskist violento" - _login200: - title: "Freguês I" - description: "Faça login por um total de 200 dias" - _login300: - title: "Freguês II" - description: "Faça login por um total de 300 dias" - _login400: - title: "Freguês III" - description: "Faça login por um total de 400 dias" - _login500: - title: "Veterano I" - description: "Faça login por um total de 500 dias" - flavor: "Cavalheiros, tudo o que peço são notas" - _login600: - title: "Veterano II" - description: "Faça login por um total de 600 dias" - _login700: - title: "Veterano III" - description: "Faça login por um total de 700 dias" - _login800: - title: "Mestre das Notas I" - description: "Faça login por um total de 800 dias" - _login900: - title: "Mestre das Notas II" - description: "Faça login por um total de 900 dias" - _login1000: - title: "Mestre das Notas III" - description: "Faça login por um total de 1 000 dias" - flavor: "Obrigado por utilizar o Misskey!" - _noteClipped1: - title: "Preciso... clipar..." - description: "Adicione a um clipe a sua primeira nota" - _noteFavorited1: - title: "Astrônomo Amador" - description: "Adicione uma nota aos favoritos pela primeira vez" - _myNoteFavorited1: - title: "Cabeça nas estrelas" - description: "Tenha uma das suas notas adicionada aos favoritos de alguém" - _profileFilled: - title: "Tudo Pronto" - description: "Configure o seu perfil" - _markedAsCat: - title: "Eu Sou Um Gato" - description: "Marque a sua conta como um gato" - flavor: "Ainda não tenho um nome." - _following1: - title: "Primeira vez seguindo alguém" - description: "Siga um usuário pela primeira vez" - _following10: - title: "Circulando, circulando" - description: "Siga 10 usuários" - _following50: - title: "Muitos amigos" - description: "Siga 50 usuários" - _following100: - title: "100 Amigos" - description: "Siga 100 usuários" - _following300: - title: "Sobrecarga de amigos" - description: "Siga 300 usuários" - _followers1: - title: "Primeiro seguidor" - description: "Ganhe o seu primeiro seguidor" - _followers10: - title: "Sigam-me os bons!" - description: "Ganhe 10 seguidores" - _followers50: - title: "Aos montes" - description: "Ganhe 50 seguidores" - _followers100: - title: "Popular" - description: "Ganhe 100 seguidores" - _followers300: - title: "Em fila única, por favor" - description: "Ganhe 300 seguidores" - _followers500: - title: "Torre de celular" - description: "Ganhe 500 seguidores" - _followers1000: - title: "Influencer" - description: "Ganhe 1 000 seguidores" - _collectAchievements30: - title: "Coletor de Conquistas" - description: "Ganhe 30 conquistas" - _viewAchievements3min: - title: "Curte Conquistas" - description: "Olhe para a sua lista de conquistas por pelo menos 3 minutos" - _iLoveMisskey: - title: "Eu Amo Misskey" - description: "Poste \"I ❤ #Misskey\"" - flavor: "A equipe de desenvolvimento do Misskey aprecia profundamente o seu apoio!" - _foundTreasure: - title: "Caça ao Tesouro" - description: "Você achou o tesouro escondido" - _client30min: - title: "Pausinha" - description: "Deixe o Misskey aberto por pelo menos 30 minutos" - _client60min: - title: "Sem falta" - description: "Deixe o Misskey aberto por pelo menos 60 minutos" - _noteDeletedWithin1min: - title: "Deixa pra lá" - description: "Exclua a postagem dentro de 1 minuto após a ter publicado" - _postedAtLateNight: - title: "Noturno" - description: "Poste uma nota tarde da noite" - flavor: "Tá na hora de ir dormir." - _postedAt0min0sec: - title: "Relógio Falante" - description: "Poste uma nota à meia-noite em ponto" - flavor: "Tic-Tac-Tic-Tac" - _selfQuote: - title: "Autorreferência" - description: "Cite sua própria nota" - _htl20npm: - title: "Linha do Tempo Fluida" - description: "Faça a velocidade da linha do tempo exceder 20 npm (notas por minuto)" - _viewInstanceChart: - title: "Analista" - description: "Veja os infográficos da instância" - _outputHelloWorldOnScratchpad: - title: "Olá, Mundo!" - description: "Produza \"hello world\" no Scratchpad" - _open3windows: - title: "Múlti-Janelas" - description: "Tenha ao mínimo 3 janelas abertas simultaneamente." - _driveFolderCircularReference: - title: "Referência circular" - description: "Tente criar uma pasta recursiva no Drive." - _reactWithoutRead: - title: "Você leu tudo isso?" - description: "Reaja a uma nota com mais de 100 caracteres dentro de 3 segundos após a sua publicação." - _clickedClickHere: - title: "Clique aqui" - description: "Você clicou aqui" - _justPlainLucky: - title: "Pura Sorte" - description: "Tem uma chance de ser obtido com uma probabilidade de 0.005% a cada 10 segundos." - _setNameToSyuilo: - title: "Complexo de Deus" - description: "Colocar seu nome como \"syuilo\"" - _passedSinceAccountCreated1: - title: "Aniversário de Um Ano" - description: "Um ano passou-se desde a criação da conta" - _passedSinceAccountCreated2: - title: "Aniversário de Dois Anos" - description: "Dois anos passaram-se desde a criação da conta" - _passedSinceAccountCreated3: - title: "Aniversário de Três Anos" - description: "Três anos passaram-se desde a criação da conta" - _loggedInOnBirthday: - title: "Feliz Aniversário" - description: "Entre no dia do seu aniversário" - _loggedInOnNewYearsDay: - title: "Feliz Ano Novo!" - description: "Entre no primeiro dia do ano" - flavor: "Para outro ótimo ano nessa instância" - _cookieClicked: - title: "Um jogo onde você clica em cookies" - description: "Clicou o cookie" - flavor: "Pera, você tá no website correto?" - _brainDiver: - title: "Brain Diver" - description: "Poste o link do Brain Diver" - flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "Teste de Transbordamento" - description: "Ative o teste de notificações repetidamente dentro de um curto período de tempo" - _tutorialCompleted: - title: "Diploma de Ensino Fundamental Misskey" - description: "Complete o tutorial" - _bubbleGameExplodingHead: - title: "🤯" - description: "O maior objeto no Bubble Game" - _bubbleGameDoubleExplodingHead: - title: "🤯 Duplo" - description: "Dois dos maiores objetos do Bubble Game ao mesmo tempo." - flavor: "Dá para encher uma lancheira com esses 🤯🤯." -_role: - new: "Novo cargo" - edit: "Editar cargo" - name: "Nome do Cargo" - description: "Descrição do cargo" - permission: "Permissões do cargo" - descriptionOfPermission: "Moderador permite que você execute operações básicas relacionadas à moderação.\nAdministradores podem alterar todas as configurações do servidor." - assignTarget: "Atribuir" - descriptionOfAssignTarget: "Manual para gerenciar manualmente quem está incluído neste cargo.\nCondicional define uma condição e os usuários que corresponderem a ela serão incluídos automaticamente." - manual: "Manual" - manualRoles: "Cargos manuais" - conditional: "Condicional" - conditionalRoles: "Cargos condicionais" - condition: "Condição" - isConditionalRole: "Este é um cargo condicional." - isPublic: "Cargo público" - descriptionOfIsPublic: "Este cargo será exibido no perfil do usuário." - options: "Opções" - policies: "Políticas" - baseRole: "Cargo padrão" - useBaseValue: "Usar o valor do cargo padrão" - chooseRoleToAssign: "Selecionar o cargo a ser atribuído" - iconUrl: "URL do ícone" - asBadge: "Exibir como insígnia" - descriptionOfAsBadge: "Quando ativado, o ícone do cargo será exibido ao lado do nome de usuário" - isExplorable: "Fazer o cargo explorável" - descriptionOfIsExplorable: "Ao ativar, a lista de membros será pública na seção 'Explorar' e a linha do tempo do cargo ficará disponível." - displayOrder: "Ordenação" - descriptionOfDisplayOrder: "Quanto maior o número, maior a posição de destaque na interface do usuário." - preserveAssignmentOnMoveAccount: "Preservar a associação de cargos durante a migração" - preserveAssignmentOnMoveAccount_description: "Quando ligado, esse cargo será encaminhado para a conta final quando houver migração de um usuário." - canEditMembersByModerator: "Permitir a edição de membros deste cargo por moderadores" - descriptionOfCanEditMembersByModerator: "Quando ativado, os moderadores também poderão atribuir/remover usuários deste papel, além dos administradores. Quando desativado, apenas os administradores poderão fazê-lo." - priority: "Prioridade" - _priority: - low: "Baixa" - middle: "Médio" - high: "Alta" - _options: - gtlAvailable: "Visualizar Linha do Tempo Global" - ltlAvailable: "Visualizar Linha do Tempo Local" - canPublicNote: "Permitir postagem pública" - mentionMax: "Número máximo de menções em uma nota" - canInvite: "Permitir a criação de códigos de convites para a instância" - inviteLimit: "Limite de códigos de convite" - inviteLimitCycle: "Intervalo de emissão do código de convite" - inviteExpirationTime: "Prazo de validade do código de convite" - canManageCustomEmojis: "Permitir gerenciar emojis personalizados" - canManageAvatarDecorations: "Gerenciar decorações de avatar" - driveCapacity: "Capacidade do drive" - maxFileSize: "Tamanho máximo de envio de arquivos" - alwaysMarkNsfw: "Sempre marcar arquivos como NSFW" - canUpdateBioMedia: "Permitir a edição de ícone ou imagem do banner." - pinMax: "Número máximo de notas fixadas" - antennaMax: "Número máximo de antenas" - wordMuteMax: "Número máximo de caracteres nas palavras silenciadas" - webhookMax: "Número máximo de webhooks" - clipMax: "Número máximo de clipes" - noteEachClipsMax: "Número máximo de notas em um clipe" - userListMax: "Número máximo de listas de usuários" - userEachUserListsMax: "Número máximo de usuários em uma lista" - rateLimitFactor: "Taxa de limitação" - descriptionOfRateLimitFactor: "Valores menores são menos restritivos, valores maiores são mais restritivos." - canHideAds: "Permitir ocultar anúncios" - canSearchNotes: "Permitir a busca de notas" - canUseTranslator: "Uso do tradutor" - avatarDecorationLimit: "Número máximo de decorações de avatar que podem ser aplicadas" - canImportAntennas: "Permitir importação de antenas" - canImportBlocking: "Permitir importação de bloqueios" - canImportFollowing: "Permitir importação de usuários seguidos" - canImportMuting: "Permitir importação de silenciamentos" - canImportUserLists: "Permitir importação de listas" - chatAvailability: "Permitir Conversas" - _condition: - roleAssignedTo: "Atribuído a cargos manuais" - isLocal: "Usuário local" - isRemote: "Usuário remoto" - isCat: "Usuários Gatinho" - isBot: "Usuários Bot" - isSuspended: "Usuário suspenso" - isLocked: "Contas privadas" - isExplorable: "Encontrável em \"Explorar\"" - createdLessThan: "Menos de X passados desde a criação da conta" - createdMoreThan: "Mais de X passados desde a criação da conta" - followersLessThanOrEq: "Possui X ou menos seguidores" - followersMoreThanOrEq: "Possui X ou mais seguidores" - followingLessThanOrEq: "Segue X ou menos contas" - followingMoreThanOrEq: "Segue X ou mais contas" - notesLessThanOrEq: "A quantidade de postagens é menor ou igual a" - notesMoreThanOrEq: "A quantidade de postagens é maior ou igual a" - and: "~ E ~ (Condicional)" - or: "~ OU ~ (Condicional)" - not: "Não ~ (Condicional)" -_sensitiveMediaDetection: - description: "Use o aprendizado de máquina para detectar automaticamente mídias sensíveis para moderação. Isso pode aumentar ligeiramente a carga no servidor." - sensitivity: "Detecção de sensibilidade" - sensitivityDescription: "Ao reduzir a sensibilidade, as detecções incorretas (falsos positivos) diminuem. Ao aumentar a sensibilidade, as falhas de detecção (falsos negativos) diminuem." - setSensitiveFlagAutomatically: "Marcar como sensível" - setSensitiveFlagAutomaticallyDescription: "Os resultados da detecção interna serão mantidos mesmo se essa opção estiver desligada." - analyzeVideos: "Habilitar análise de vídeos" - analyzeVideosDescription: "Analisa vídeos em adição a imagens. Isso irá aumentar levemente a carga do servidor." -_emailUnavailable: - used: "O endereço de e-mail informado já está sendo utilizado" - format: "Formado de e-mail inválido" - disposable: "Endereços de e-mail descartáveis não devem ser utilizados" - mx: "O servidor de informado é inválido" - smtp: "O servidor de e-mail não está respondendo" - banned: "Você não pode se cadastrar com esse endereço de email" -_ffVisibility: - public: "Público" - followers: "Visível apenas para seguidores" - private: "Privado" -_signup: - almostThere: "Quase pronto" - emailAddressInfo: "Por favor, insira o seu endereço de e-mail. Ele não será divulgado." - emailSent: "Um e-mail de confirmação foi enviado para o endereço de e-mail fornecido ({email}). Acesse o link fornecido no e-mail para concluir a criação de sua conta." -_accountDelete: - accountDelete: "Remover Conta" - mayTakeTime: "A exclusão de uma conta é um processo que requer muito recurso, portanto, se você tiver muito conteúdo criados ou arquivos enviados, pode levar algum tempo até ser concluída." - sendEmail: "Quando a exclusão da conta estiver concluída, enviaremos uma notificação para o endereço de e-mail registrado." - requestAccountDelete: "Solicitar exclusão de conta" - started: "O processo de exclusão foi iniciado." - inProgress: "A exclusão está em andamento" -_ad: - back: "Voltar" - reduceFrequencyOfThisAd: "Diminuir frequência deste anúncio" - hide: "Não exibir anúncios" - timezoneinfo: "O dia da semana é determinado pelo fuso horário do servidor." - adsSettings: "Configurações de propaganda" - notesPerOneAd: "Intervalo de notas entre o anúncio nas atualizações em tempo real." - setZeroToDisable: "Selecione o valor 0 para desabilitar anúncios nas atualizações em tempo real." - adsTooClose: "O intervalo atual de anúncio pode impactar negativamente a experiência de usuário por ser muito baixo." -_forgotPassword: - enterEmail: "Por favor, insira o endereço de e-mail usado no cadastro de sua conta. Um link para redefinição de senha será enviado para esse endereço." - ifNoEmail: "Caso você não tenha registrado um endereço de e-mail, por favor, entre em contato com o administrador." - contactAdmin: "Essa instância não possui suporte ao uso de endereços de email, contate seu administrador para mudar a sua senha." -_gallery: - my: "Minha Galeria" - liked: "Postagens curtidas" - like: "Curtir" - unlike: "Remover curtida" _email: _follow: title: "Você tem um novo seguidor" - _receiveFollowRequest: - title: "Você recebeu um pedido de seguidor" -_plugin: - install: "Instalar plugins" - installWarn: "Por favor, não instale plugins duvidosos." - manage: "Gerenciar plugins" - viewSource: "Ver código-fonte" - viewLog: "Mostrar registo" -_preferencesBackups: - list: "Backups criados" - saveNew: "Salvar novo backup" - loadFile: "Carregar de arquivo" - apply: "Aplicar a este dispositivo" - save: "Salvar mudanças" - inputName: "Insira um nome para esse backup" - cannotSave: "Não foi possível salvar" - nameAlreadyExists: "Um backup chamado \"{name}\" já existe. Por favor, insira outro nome." - applyConfirm: "Deseja aplicar o backup '{name}' ao dispositivo atual? As configurações atuais do dispositivo serão perdidas." - saveConfirm: "Salvar backup como \"{name}\"?" - deleteConfirm: "Deseja excluir {name}?" - renameConfirm: "Renomear esse backup de \"{old}\" para \"{new}\"?" - noBackups: "Não há backups. Você pode configurar suas configurações de cliente nesse servidor ao selecionar \"Criar novo backup\"." - createdAt: "Criado em: {date} {time}" - updatedAt: "Atualizado em: {date} {time}" - cannotLoad: "Não foi possível carregar" - invalidFile: "Formato de arquivo inválido" -_registry: - scope: "Escopo" - key: "Chave" - keys: "Chave" - domain: "Domínio" - createKey: "Criar chave" -_aboutMisskey: - about: "Misskey é um software de código aberto desenvolvido por syulio desde 2014." - contributors: "Contribuidores principais" - allContributors: "Todos os contribuidores" - source: "Código-fonte" - original: "Original" - thisIsModifiedVersion: "{name} utiliza uma versão modificada do Misskey original." - translation: "Traduza o Misskey" - donate: "Doe para o Misskey" - morePatrons: "Nós apreciamos o apoio de vários outros apoiadores não listados aqui. Obrigado! 🥰" - patrons: "Apoiadores" - projectMembers: "Membros do projeto" -_displayOfSensitiveMedia: - respect: "Esconder mídia marcada como sensível" - ignore: "Exibir mídia marcada como sensível" - force: "Esconder toda mídia" -_instanceTicker: - none: "Nunca mostrar" - remote: "Mostrar para usuários remotos" - always: "Sempre mostrar" -_serverDisconnectedBehavior: - reload: "Recarregar automaticamente" - dialog: "Exibir diálogo de aviso de conteúdo" - quiet: "Exibir aviso de conteúdo discreto" -_channel: - create: "Criar canal" - edit: "Editar canal" - setBanner: "Definir banner" - removeBanner: "Remover banner" - featured: "Destaques" - owned: "Autoral" - following: "Seguindo" - usersCount: "{n} usuários ativos" - notesCount: "{n} notas" - nameAndDescription: "Nome e descrição" - nameOnly: "Apenas o nome" - allowRenoteToExternal: "Permitir repostagens e citações de fora do canal" -_menuDisplay: - sideFull: "Exibir painel lateral inteiro" - sideIcon: "Lateral (Ícones)" - top: "Exibir barra superior" - hide: "Ocultar" -_wordMute: - muteWords: "Palavras silenciadas" - muteWordsDescription: "Separe com espaços para uma condicional AND (&&) ou por linha para uma condicional OR (||)." - muteWordsDescription2: "Cercar palavras-chave com barras para usar expressões regulares (RegEx)." -_instanceMute: - instanceMuteDescription: "Todas as notas e repostagens do servidor configurado serão silenciados, incluindo respostas aos usuários do servidor mutado." - instanceMuteDescription2: "Separar por linha" - title: "Esconder notas das instâncias listadas. " - heading: "Lista de instâncias a serem silenciadas" _theme: - explore: "Explorar Temas" - install: "Instalar um tema" - manage: "Gerenciar temas" - code: "Código do tema" - description: "Descrição" - installed: "{name} foi instalado" - installedThemes: "Temas instalados" - builtinThemes: "Temas nativos" - instanceTheme: "Tema do servidor" - alreadyInstalled: "Esse tema já foi instalado" - invalid: "O formato desse tema é invalido" - make: "Fazer um tema" - base: "Base" - addConstant: "Adicionar constante" - constant: "Constante" - defaultValue: "Valor padrão" - color: "Cor" - refProp: "Referenciar uma propriedade" - refConst: "Referenciar uma constante" - key: "Chave" - func: "Funções" - funcKind: "Tipo de função" - argument: "Argumento" - basedProp: "Propriedade referenciada" - alpha: "Opacidade" - darken: "Escurecer" - lighten: "Esclarecer" - inputConstantName: "Insira um nome para essa constante" - importInfo: "Se você inserir o código do tema aqui, você pode importá-lo no editor de temas" - deleteConstantConfirm: "Confirma a exclusão da constante {const}?" keys: - accent: "Cor de destaque" - bg: "Plano de fundo" - fg: "Texto" - focus: "Foco" - indicator: "Indicador" - panel: "Painel" - shadow: "Sombra" - header: "Cabeçalho" - navBg: "Plano de fundo da barra lateral" - navFg: "Texto da barra lateral" - navActive: "Texto da coluna lateral (Ativa)" - navIndicator: "Indicador da coluna lateral" - link: "Link" - hashtag: "Hashtag" mention: "Menção" - mentionMe: "Menciona (a mim)" renote: "Repostar" - modalBg: "Plano de fundo modal" - divider: "Separador" - scrollbarHandle: "Alça da barra de rolagem (Selecionada)" - scrollbarHandleHover: "Alça da barra de rolagem (Selecionada)" - dateLabelFg: "Texto do rótulo de data" - infoBg: "Plano de fundo de informações" - infoFg: "Texto de informações" - infoWarnBg: "Plano de fundo de avisos" - infoWarnFg: "Texto de avisos" - toastBg: "Plano de fundo de notificações" - toastFg: "Texto da notificação" - buttonBg: "Plano de fundo de botão" - buttonHoverBg: "Plano de fundo de botão (Selecionado)" - inputBorder: "Borda de campo digitável" - badge: "Emblema" - messageBg: "Plano de fundo do chat" - fgHighlighted: "Texto Destacado" _sfx: note: "Posts" - noteMy: "Própria nota" notification: "Notificações" - reaction: "Ao selecionar uma reação" - chatMessage: "Mensagens em Conversas" -_soundSettings: - driveFile: "Usar um arquivo de áudio do Drive." - driveFileWarn: "Selecione um arquivo de áudio do Drive." - driveFileTypeWarn: "Esse arquivo não é compatível" - driveFileTypeWarnDescription: "Selecione um arquivo de áudio" - driveFileDurationWarn: "O áudio é muito longo." - driveFileDurationWarnDescription: "Áudios longos podem atrapalhar o funcionamento do Misskey. Deseja continuar?" - driveFileError: "Não foi possível carregar o som. Por favor, altere a configuração." -_ago: - future: "Futuro" - justNow: "Agora mesmo" - secondsAgo: "{n}s atrás" - minutesAgo: "{n}m atrás" - hoursAgo: "{n}h atrás" - daysAgo: "{n}d atrás" - weeksAgo: "{n} semanas atrás" - monthsAgo: "{n} meses atrás" - yearsAgo: "{n} anos atrás" - invalid: "Não há nada aqui" -_timeIn: - seconds: "Em {n}s" - minutes: "Em {n}m" - hours: "Em {n}h" - days: "Em {n}d" - weeks: "Em {n} semanas" - months: "Em {n} meses" - years: "Em {n} anos" -_time: - second: "Segundo(s)" - minute: "Minuto(s)" - hour: "Hora(s)" - day: "Dia(s)" -_2fa: - alreadyRegistered: "Você já cadastrou um dispositivo de autenticação de dois fatores." - registerTOTP: "Cadastrar aplicativo autenticador" - step1: "Inicialmente, instale um aplicativo autenticador (como {a} ou {b}) em seu dispositivo." - step2: "Então, escaneie o código QR exibido na tela." - step2Uri: "Acesse o seguinte URI se você estiver utilizando um aplicativo no computador" - step3Title: "Insira o código de autenticação" - step3: "Insira o código de autenticação (token) providenciado pelo seu aplicativo para terminar a configuração." - setupCompleted: "Configuração completa" - step4: "De agora em diante, quaisquer solicitações de entrada pedirão pelo código." - securityKeyNotSupported: "O seu navegador não é compatível com chaves de segurança." - registerTOTPBeforeKey: "Por favor, configure um aplicativo autenticador para registrar uma chave de segurança." - securityKeyInfo: "Além da autenticação por impressão digital ou PIN, você também pode configurar a autenticação por chaves de segurança de hardware compatível com FIDO2 para proteger ainda mais a sua conta." - registerSecurityKey: "Registre um código de segurança" - securityKeyName: "Insira um nome para a chave" - tapSecurityKey: "Por favor, siga as instruções do navegador para registrar o código de segurança" - removeKey: "Remover código de segurança" - removeKeyConfirm: "Deseja excluir {name}?" - whyTOTPOnlyRenew: "O autenticador não pode ser removido enquanto há códigos de segurança registrados." - renewTOTP: "Reconfigurar autenticador" - renewTOTPConfirm: "Isso interromperá o funcionamento dos códigos de aplicativos anteriores " - renewTOTPOk: "Reconfigurar" - renewTOTPCancel: "Não, obrigado" - checkBackupCodesBeforeCloseThisWizard: "Antes de fechar essa janela, anote os códigos de backup a seguir." - backupCodes: "Códigos de backup" - backupCodesDescription: "Você pode utilizar esses códigos para ganhar acesso à conta caso sua autenticação de dois fatores esteja indisponível. Cada código pode ser utilizado apenas uma vez. Por favor, guarde-os em um local seguro." - backupCodeUsedWarning: "Um código de backup foi utilizado. Por favor, reconfigure a autenticação de dois fatores o quanto antes, caso não consiga utilizá-la." - backupCodesExhaustedWarning: "Todos os códigos de backup foram utilizados. Caso perca acesso à autenticação de dois fatores, você perderá o acesso à conta. Por favor, reconfigure a autenticação de dois fatores." - moreDetailedGuideHere: "Aqui está um guia detalhado" -_permissions: - "read:account": "Visualizar informações da conta" - "write:account": "Editar informações da conta" - "read:blocks": "Visualizar a sua lista de usuários bloqueados" - "write:blocks": "Editar a sua lista de usuários bloqueados" - "read:drive": "Visualizar os seus arquivos e pastas do drive" - "write:drive": "Editar ou excluir os seus arquivos e pastas do drive" - "read:favorites": "Visualizar a sua lista de favoritos" - "write:favorites": "Editar a sua lista de favoritos" - "read:following": "Visualizar informações de quem você segue" - "write:following": "Seguir ou deixar de seguir outras contas" - "read:messaging": "Visualizar os seus chats" - "write:messaging": "Compor ou editar mensagens de chat" - "read:mutes": "Visualizar a sua lista de usuários silenciados" - "write:mutes": "Editar a sua lista de usuários silenciados" - "write:notes": "Compor ou excluir notas" - "read:notifications": "Visualizar as suas notificações" - "write:notifications": "Gerenciar as suas notificações" - "read:reactions": "Visualizar as suas reações" - "write:reactions": "Editar as suas reações" - "write:votes": "Votar em enquetes" - "read:pages": "Visualizar as suas páginas" - "write:pages": "Editar ou excluir as suas páginas" - "read:page-likes": "Visualizar as suas curtidas em páginas" - "write:page-likes": "Editar as suas curtidas em páginas" - "read:user-groups": "Visualizar os seus grupos de usuários" - "write:user-groups": "Editar ou excluir os seus grupos de usuários" - "read:channels": "Visualizar os seus canais" - "write:channels": "Editar os seus canais" - "read:gallery": "Visualizar a sua galeria" - "write:gallery": "Editar sua galeria" - "read:gallery-likes": "Visualizar a sua lista de curtidas da galeria" - "write:gallery-likes": "Editar a sua lista de curtidas da galeria" - "read:flash": "Ver Play" - "write:flash": "Editar Plays" - "read:flash-likes": "Ver lista de Plays curtidas" - "write:flash-likes": "Editar lista de Plays curtidas" - "read:admin:abuse-user-reports": "Ver relatórios de usuário" - "write:admin:delete-account": "Excluir conta de usuário" - "write:admin:delete-all-files-of-a-user": "Excluir todos os arquivos de um usuário" - "read:admin:index-stats": "Ver estatísticas do índice do banco de dados" - "read:admin:table-stats": "Ver estatísticas da tabela do banco de dados" - "read:admin:user-ips": "Ver endereços IP do usuário" - "read:admin:meta": "Ver metadados da instância" - "write:admin:reset-password": "Mudar a senha do usuário" - "write:admin:resolve-abuse-user-report": "Resolver relatório de usuário" - "write:admin:send-email": "Enviar email" - "read:admin:server-info": "Ver informações do servidor" - "read:admin:show-moderation-log": "Ver log de moderação" - "read:admin:show-user": "Ver informações privadas do usuário" - "write:admin:suspend-user": "Suspender usuário" - "write:admin:unset-user-avatar": "Remover avatar do usuário" - "write:admin:unset-user-banner": "Remover banner do usuário" - "write:admin:unsuspend-user": "Cancelar a suspensão do usuário" - "write:admin:meta": "Gerenciar os metadados da instância" - "write:admin:user-note": "Gerenciar a nota de moderação" - "write:admin:roles": "Gerenciar cargos" - "read:admin:roles": "Ver cargos" - "write:admin:relays": "Gerenciar relays" - "read:admin:relays": "Ver relays" - "write:admin:invite-codes": "Gerenciar códigos de convite" - "read:admin:invite-codes": "Ver códigos de convite" - "write:admin:announcements": "Gerenciar anúncios" - "read:admin:announcements": "Ver anúncios" - "write:admin:avatar-decorations": "Gerenciar decorações de avatar" - "read:admin:avatar-decorations": "Ver decorações de avatar" - "write:admin:federation": "Gerenciar dados de federação" - "write:admin:account": "Gerenciar conta de usuário" - "read:admin:account": "Ver conta de usuário" - "write:admin:emoji": "Gerenciar emoji" - "read:admin:emoji": "Ver emoji" - "write:admin:queue": "Gerenciar trabalhos pendentes" - "read:admin:queue": "Ver informações de trabalhos pendentes" - "write:admin:promo": "Gerenciar notas de promoção" - "write:admin:drive": "Gerenciar Drive de usuário" - "read:admin:drive": "Ver informações de Drive de usuário" - "read:admin:stream": "Utilizar WebSocket API para Admin" - "write:admin:ad": "Gerenciar propagandas" - "read:admin:ad": "Ver propagandas" - "write:invite-codes": "Criar códigos de convite" - "read:invite-codes": "Obter códigos de convite" - "write:clip-favorite": "Gerenciar clipes favoritados" - "read:clip-favorite": "Ver Clipes favoritados" - "read:federation": "Ver dados de federação" - "write:report-abuse": "Reportar violação" - "write:chat": "Compor ou editar mensagens de chat" - "read:chat": "Navegar Conversas" -_auth: - shareAccessTitle: "Conceder permissões do aplicativo" - shareAccess: "Você gostaria de autorizar \"{name}\" para acessar essa conta?" - shareAccessAsk: "Você tem certeza de que gostaria de conceder ao aplicativo o acesso à conta?" - permission: "{name} solicita as seguintes permissões" - permissionAsk: "O aplicativo solicita as seguintes permissões" - pleaseGoBack: "Por favor, volte ao aplicativo" - callback: "Retornando ao aplicativo" - accepted: "Acesso permitido" - denied: "Acesso negado" - scopeUser: "Operar como o usuário a seguir" - pleaseLogin: "Por favor, entre para autorizar aplicativos." - byClickingYouWillBeRedirectedToThisUrl: "Quando o acesso for permitido, você será redirecionado para o seguinte endereço" -_antennaSources: - all: "Todas as notas" - homeTimeline: "Notas de usuários seguidos" - users: "Notas de usuários específicos" - userList: "Notas de uma lista específica de usuários" - userBlacklist: "Todas as notas, exceto as de um ou mais usuários específicos" -_weekday: - sunday: "Domingo" - monday: "Segunda-feira" - tuesday: "Terça-feira" - wednesday: "Quarta-feira" - thursday: "Quinta-feira" - friday: "Sexta-feira" - saturday: "Sábado" + chat: "Chat" _widgets: profile: "Perfil" instanceInfo: "Informações da instância" - memo: "Notas adesivas" notifications: "Notificações" - timeline: "Linha do tempo" - calendar: "Calendário" - trends: "Destaques" - clock: "Relógio" - rss: "Leitor de RSS" - rssTicker: "Ticker RSS" - activity: "Atividades" - photos: "Fotos" - digitalClock: "Relógio digital" - unixClock: "Hora UNIX" - federation: "Federação" - instanceCloud: "Nuvem de instâncias" - postForm: "Campo de postagem" - slideshow: "Apresentação de slides" - button: "Botão" - onlineUsers: "Usuários Online" - jobQueue: "Fila de tarefas" - serverMetric: "Métricas do servidor" - aiscript: "Console AiScript" - aiscriptApp: "AiScript App" - aichan: "Ai" - userList: "Lista de usuários" + timeline: "Timeline" + activity: "atividade" + federation: "União" + jobQueue: "Fila de trabalhos" _userList: - chooseList: "Selecione uma lista" - clicker: "Clicker" - birthdayFollowings: "Usuários de aniversário hoje" - chat: "Conversas" + chooseList: "Escolhe uma lista" _cw: - hide: "Esconder" show: "Carregar mais" - chars: "{count} caracteres" - files: "{count} arquivo(s)" -_poll: - noOnlyOneChoice: "São necessárias, no mínimo, duas escolhas" - choiceN: "Escolha {n}" - noMore: "Você não pode adicionar mais escolhas" - canMultipleVote: "Permitir múltipla seleção" - expiration: "Encerrar enquete" - infinite: "Nunca" - at: "Terminar em..." - after: "Terminar após..." - deadlineDate: "Data de término" - deadlineTime: "Tempo" - duration: "Duração" - votesCount: "{n} votos" - totalVotes: "{n} votos totais" - vote: "Votar em enquetes" - showResult: "Ver resultados" - voted: "Votada" - closed: "Encerrada" - remainingDays: "{d} dia(s) {h} hora(s) restantes" - remainingHours: "{h} hora(s) {m} minuto(s) restantes" - remainingMinutes: "{m} minuto(s) {s} segundo(s) restantes" - remainingSeconds: "{s} segundo(s) restantes" _visibility: - public: "Público" - publicDescription: "Sua nota será visível para todos os usuários" - home: "Início" - homeDescription: "Publicar apenas na linha do tempo Início" + home: "casa" followers: "Seguidores" - followersDescription: "Tornar visível apenas para os meus seguidores" - specified: "Mensagem Direta" - specifiedDescription: "Tornar visível apenas para usuários específicos" - disableFederation: "Defederar" - disableFederationDescription: "Não transmitir às outras instâncias" -_postForm: - replyPlaceholder: "Responder a essa nota..." - quotePlaceholder: "Citar essa nota..." - channelPlaceholder: "Postar em canal..." - _placeholders: - a: "Como vão as coisas?" - b: "O que está rolando por aí?" - c: "No que está pensando?" - d: "Do que você quer falar?" - e: "Comece a digitar..." - f: "Esperando você digitar..." _profile: name: "Nome" username: "Nome de usuário" - description: "Bio" - youCanIncludeHashtags: "Você pode incluir hashtags em sua bio." - metadata: "Informações Adicionais" - metadataEdit: "Editar informações adicionais" - metadataDescription: "Aqui, você pode exibir campos adicionais de informação no seu perfil." - metadataLabel: "Rótulo" - metadataContent: "Conteúdo" - changeAvatar: "Mudar avatar" - changeBanner: "Mudar banner" - verifiedLinkDescription: "Ao inserir um URL que contém um link para essa conta, um ícone de verificação será exibido ao lado do campo" - avatarDecorationMax: "Você pode adicionar até {max} decorações." - followedMessage: "Mensagem exibida quando alguém segue você" - followedMessageDescription: "Você pode definir uma curta mensagem que será exibida aos usuários que seguirem você." - followedMessageDescriptionForLockedAccount: "Se você aceita pedidos de seguidor manualmente, isso será exibido quando você aceitá-los." _exportOrImport: - allNotes: "Todas as notas" - favoritedNotes: "Notas nos favoritos" - clips: "Clipe" followingList: "Seguindo" muteList: "Silenciar" blockingList: "Bloquear" userLists: "Listas" - excludeMutingUsers: "Excluir usuários silenciados" - excludeInactiveUsers: "Excluir usuários inativos" - withReplies: "Incluir respostas de usuários importados na linha do tempo" _charts: federation: "União" - apRequest: "Solicitações" - usersIncDec: "Diferença no número de usuários" - usersTotal: "Número total de usuários" - activeUsers: "Usuários ativos" - notesIncDec: "Diferença no número de notas" - localNotesIncDec: "Diferença no número de notas locais" - remoteNotesIncDec: "Diferença no número de notas remotas" - notesTotal: "Número total de notas" - filesIncDec: "Diferença no número de arquivos" - filesTotal: "Número total de arquivos" - storageUsageIncDec: "Diferença no uso de armazenamento" - storageUsageTotal: "Uso total de armazenamento" -_instanceCharts: - requests: "Solicitações" - users: "Diferença no número de usuários" - usersTotal: "Número cumulativo de usuários" - notes: "Diferença no número de notas" - notesTotal: "Número cumulativo de notas" - ff: "Diferença entre número de usuários seguidos/seguidores" - ffTotal: "Número cumulativo de usuários seguidos/seguidores" - cacheSize: "Diferença do tamanho do cache" - cacheSizeTotal: "Tamanho cumulativo do cache" - files: "Diferença no número de arquivos" - filesTotal: "Número cumulativo de arquivos" _timelines: - home: "Início" - local: "Local" - social: "Social" - global: "Global" -_play: - new: "Criar Play" - edit: "Editar Play" - created: "Play criado" - updated: "Play editado" - deleted: "Play foi excluído" - pageSetting: "Configurações do Play" - editThisPage: "Editar este Play" - viewSource: "Ver fonte" - my: "Meus Plays" - liked: "Plays curtidos" - featured: "Popular" - title: "Título" - script: "Script" - summary: "Descrição" - visibilityDescription: "Pôr em privado significa que ele não será visível no perfil, mas qualquer um com o URL poderá acessar" + home: "casa" _pages: - newPage: "Criar uma Página" - editPage: "Editar essa Página" - readPage: "Ver a fonte dessa Página" - pageSetting: "Configurações da página" - nameAlreadyExists: "O URL de Página especificado já existe" - invalidNameTitle: "O URL de Página especificado é inválido" - invalidNameText: "Confira se o título da Página não está vazio" - editThisPage: "Editar essa Página" - viewSource: "Ver código-fonte" - viewPage: "Visualizar as suas páginas" - like: "Curtir" - unlike: "Remover curtida" - my: "Minhas Páginas" - liked: "Páginas curtidas" - featured: "Populares" - inspector: "Inspetor" - contents: "Conteúdo" - content: "Bloco da Página" - variables: "Variáveis" - title: "Título" - url: "URL da Página" - summary: "Resumo da página" - alignCenter: "Centralizar elementos" - hideTitleWhenPinned: "Esconder título da Página quando fixado em perfil" - font: "Fonte" - fontSerif: "Serif" - fontSansSerif: "Sans Serif" - eyeCatchingImageSet: "Escolher miniatura" - eyeCatchingImageRemove: "Excluir miniatura" - chooseBlock: "Adicionar bloco" - enterSectionTitle: "Insira um título à seção" - selectType: "Selecionar um tipo" - contentBlocks: "Conteúdo" - inputBlocks: "Inserir" - specialBlocks: "Especial" blocks: - text: "Texto" - textarea: "Área do texto" - section: "Seção" image: "imagem" - button: "Botão" - dynamic: "Blocos Dinâmicos" - dynamicDescription: "Esse bloco foi abolido. Por favor, use {play} de agora em diante." - note: "Nota embutida" - _note: - id: "ID da nota" - idDescription: "Você também pode colar o URL da nota aqui." - detailed: "Visão detalhada" _relayStatus: requesting: "Pendente" accepted: "Aprovado" @@ -2592,49 +514,22 @@ _notification: youGotMention: "{name} te mencionou" youGotReply: "{name} te respondeu" youGotQuote: "{name} te citou" - youRenoted: "Repostagens de {name}" youWereFollowed: "Você tem um novo seguidor" - youReceivedFollowRequest: "Você recebeu um pedido de seguidor" - yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito" + youReceivedFollowRequest: "Você recebeu um pedido de seguimento" + yourFollowRequestAccepted: "Seu pedido de seguimento foi aceito" pollEnded: "Os resultados da enquete agora estão disponíveis" - newNote: "Nova nota" - unreadAntennaNote: "Antena {name}" - roleAssigned: "Cargo dado" - chatRoomInvitationReceived: "Você foi convidado para uma conversa" emptyPushNotificationMessage: "As notificações de alerta foram atualizadas" - achievementEarned: "Conquista desbloqueada" - testNotification: "Notificação teste" - checkNotificationBehavior: "Verificar aparência da notificação" - sendTestNotification: "Enviar notificação de teste" - notificationWillBeDisplayedLikeThis: "Notificações se parecem com isso" - reactedBySomeUsers: "{n} usuários reagiram" - likedBySomeUsers: "{n} usuários gostaram da nota" - renotedBySomeUsers: "{n} usuários repostaram a nota" - followedBySomeUsers: "{n} usuários te seguiram" - flushNotification: "Limpar notificações" - exportOfXCompleted: "Exportação de {x} foi concluída" - login: "Alguém entrou na conta" - createToken: "Uma token de acesso foi criada" - createTokenDescription: "Se você não faz ideia, exclua o token de acesso através de \"{text}\"." _types: - all: "Todas" - note: "Novas notas" + all: "Todos" follow: "Seguindo" mention: "Menção" reply: "Respostas" renote: "Repostar" - quote: "Citações" + quote: "Citar" reaction: "Reações" pollEnded: "Enquetes terminando" - receiveFollowRequest: "Recebeu pedidos de seguidor" - followRequestAccepted: "Aceitou pedidos de seguidor" - roleAssigned: "Cargo dado" - chatRoomInvitationReceived: "Convite de conversa recebido" - achievementEarned: "Conquista desbloqueada" - exportCompleted: "A exportação foi concluída" - login: "Iniciar sessão" - createToken: "Criar token de acesso" - test: "Notificação teste" + receiveFollowRequest: "Recebeu pedidos de seguimento" + followRequestAccepted: "Aceitou pedidos de seguimento" app: "Notificações de aplicativos conectados" _actions: followBack: "te seguiu de volta" @@ -2643,28 +538,13 @@ _notification: _deck: alwaysShowMainColumn: "Sempre mostrar a coluna principal" columnAlign: "Alinhar colunas" - columnGap: "Margem entre colunas" - deckMenuPosition: "Posição do menu do deck" - navbarPosition: "Posição da barra de navegação" addColumn: "Adicionar coluna" - newNoteNotificationSettings: "Opções de notificação para novas notas" - configureColumn: "Configurar coluna" swapLeft: "Trocar de posição com a coluna à esquerda" swapRight: "Trocar de posição com a coluna à direita" swapUp: "Trocar de posição com a coluna acima" swapDown: "Trocar de posição com a coluna abaixo" - stackLeft: "Empilhar na coluna à esquerda" popRight: "Acoplar coluna à direita" profile: "Perfil" - newProfile: "Novo perfil" - deleteProfile: "Remover perfil" - introduction: "Crie a interface perfeita para você arranjando as colunas livremente!" - introduction2: "Clique no + à direita da tela para adicionar novas colunas quando quiser." - widgetsIntroduction: "Por favor, selecione \"Editar widgets\" no menu em coluna e adicione um widget." - useSimpleUiForNonRootPages: "Usar UI simples para páginas navegadas" - usedAsMinWidthWhenFlexible: "A largura mínima será usada para isso quando o \"Ajuste automático da largura\" estiver ativado" - flexible: "Ajuste automático da largura" - enableSyncBetweenDevicesForProfiles: "Habilitar sincronização das informações do perfil entre dispositivos" _columns: main: "Principal" widgets: "Widgets" @@ -2672,415 +552,7 @@ _deck: tl: "Timeline" antenna: "Antenas" list: "Listas" - channel: "Canais" mentions: "Menções" direct: "Notas diretas" - roleTimeline: "Linha do tempo do cargo" - chat: "Conversas" -_dialog: - charactersExceeded: "Você excedeu o limite de caracteres! Atualmente em {current} de {max}." - charactersBelow: "Você está abaixo do limite mínimo de caracteres! Atualmente em {current} of {min}." -_disabledTimeline: - title: "Linha do tempo desabilitada" - description: "Você não pode acessar essa linha do tempo sob o seu cargo atual." -_drivecleaner: - orderBySizeDesc: "Tamanho descendente" - orderByCreatedAtAsc: "Data ascendente" _webhookSettings: - createWebhook: "Criar Webhook" - modifyWebhook: "Modificar Webhook" name: "Nome" - secret: "Segredo" - trigger: "Gatilho" - active: "Ativado" - _events: - follow: "Quando seguindo um usuário" - followed: "Quando sendo seguido" - note: "Ao postar uma nota" - reply: "Quando receber uma resposta" - renote: "Quando repostado" - reaction: "Quando receber uma reação" - mention: "Quando for mencionado" - _systemEvents: - abuseReport: "Quando receber um relatório de abuso" - abuseReportResolved: "Quando relatórios de abuso forem resolvidos " - userCreated: "Quando um usuário é criado" - inactiveModeratorsWarning: "Quando moderadores estiverem inativos por um tempo" - inactiveModeratorsInvitationOnlyChanged: "Quando um moderador está inativo por um tempo e os cadastros passam a exigir convites" - deleteConfirm: "Você tem certeza de que deseja excluir o Webhook?" - testRemarks: "Clique no botão à direita do interruptor para enviar um Webhook de teste com dados fictícios." -_abuseReport: - _notificationRecipient: - createRecipient: "Adicionar destinatário para relatórios de abuso" - modifyRecipient: "Editar destinatários para relatórios de abuso" - recipientType: "TIpo de notificação" - _recipientType: - mail: "E-mail" - webhook: "Webhook" - _captions: - mail: "Enviar o email aos endereços dos moderadores ao receber relatório de abuso." - webhook: "Enviar uma notificação ao SystemWebhook quando você receber um resolver um relatório de abuso." - keywords: "Palavras-chave" - notifiedUser: "Usuários para notificar" - notifiedWebhook: "Webhook usado" - deleteConfirm: "Você tem certeza de que quer excluir o destinatário da notificação?" -_moderationLogTypes: - createRole: "Cargo criado" - deleteRole: "Cargo excluído" - updateRole: "Cargo atualizado" - assignRole: "Cargo atribuído" - unassignRole: "Cargo removido" - suspend: "Suspender" - unsuspend: "Suspensão cancelada" - addCustomEmoji: "Emoji personalizado adicionado" - updateCustomEmoji: "Emoji personalizado atualizado" - deleteCustomEmoji: "Emoji personalizado removido" - updateServerSettings: "Configurações de servidor atualizadas" - updateUserNote: "Nota de moderação atualizada" - deleteDriveFile: "Arquivo excluído" - deleteNote: "Nota excluída" - createGlobalAnnouncement: "Anúncio global criado" - createUserAnnouncement: "Anúncio de usuário criado" - updateGlobalAnnouncement: "Anúncio global atualizado" - updateUserAnnouncement: "Anúncio de usuário atualizado" - deleteGlobalAnnouncement: "Anúncio global excluído" - deleteUserAnnouncement: "Anúncio de usuário excluído" - resetPassword: "Redefinir senha" - suspendRemoteInstance: "Instância remota suspensa" - unsuspendRemoteInstance: "Suspensão de instância remota removida" - updateRemoteInstanceNote: "Nota de moderação atualizada para instância remota." - markSensitiveDriveFile: "Arquivo marcado como sensível" - unmarkSensitiveDriveFile: "Arquivo desmarcado como sensível" - resolveAbuseReport: "Relatório resolvido" - forwardAbuseReport: "Denúncia encaminhada" - updateAbuseReportNote: "Nota de moderação da denúncia atualizada" - createInvitation: "Convite gerado" - createAd: "Propaganda criada" - deleteAd: "Propaganda excluída" - updateAd: "Propaganda atualizada" - createAvatarDecoration: "Decoração de avatar criada" - updateAvatarDecoration: "Decoração de avatar atualizada" - deleteAvatarDecoration: "Decoração de avatar removida" - unsetUserAvatar: "Remover avatar de usuário" - unsetUserBanner: "Remover banner de usuário" - createSystemWebhook: "Criar SystemWebhook" - updateSystemWebhook: "Atualizar SystemWebhook" - deleteSystemWebhook: "Remover SystemWebhook" - createAbuseReportNotificationRecipient: "Criar um destinatário para relatórios de abuso" - updateAbuseReportNotificationRecipient: "Atualizar destinatários para relatórios de abuso" - deleteAbuseReportNotificationRecipient: "Remover um destinatário para relatórios de abuso" - deleteAccount: "Remover conta" - deletePage: "Remover página" - deleteFlash: "Remover Play" - deleteGalleryPost: "Remover a publicação da galeria" - deleteChatRoom: "Sala de Conversas Excluída" - updateProxyAccountDescription: "Atualizar descrição da conta de proxy" -_fileViewer: - title: "Detalhes do arquivo" - type: "Tipo de arquivo" - size: "Tamanho do arquivo" - url: "URL" - uploadedAt: "Adicionado em" - attachedNotes: "Notas anexadas" - thisPageCanBeSeenFromTheAuthor: "Essa página só pode ser vista pelo usuário que enviou esse arquivo." -_externalResourceInstaller: - title: "Instalar de site externo" - checkVendorBeforeInstall: "Tenha certeza de que o distribuidor desse recurso é confiável antes da instalação." - _plugin: - title: "Deseja instalar esse plugin?" - _theme: - title: "Deseja instalar esse tema?" - _meta: - base: "Paleta de cores base" - _vendorInfo: - title: "Informações do distribuidor" - endpoint: "Endpoint referenciado" - hashVerify: "Verificação de hashes" - _errors: - _invalidParams: - title: "Parâmetros inválidos" - description: "Não há informações suficientes para carregar dados do site externo. Por favor, confirme o URL inserido." - _resourceTypeNotSupported: - title: "Esse recurso externo é incompatível" - description: "Esse tipo de recuso externo é incompatível. Por favor, comunique o administrador do site." - _failedToFetch: - title: "Não foi possível obter dados" - fetchErrorDescription: "Houve um erro ao comunicar com o site externo. Se tentar novamente não resolver o problema, contate o administrador do site." - parseErrorDescription: "Houve um erro processando os dados do site externo. Por favor, contate o administrador do site." - _hashUnmatched: - title: "Verificação de dados falhou" - description: "Houve um erro verificando a integridade do conteúdo obtido. Como medida de segurança, a instalação foi interrompida. Por favor, contate o administrador do site." - _pluginParseFailed: - title: "Erro AiScript" - description: "Os dados solicitados foram obtidos com sucesso, mas houve um erro na leitura do AiScript. Por favor, contate o autor do plugin. Detalhes de erro podem ser vistos no console Javascript." - _pluginInstallFailed: - title: "A instalação do plugin falhou." - description: "Houve um problema na instalação do plugin. Por favor, tente novamente. Detalhes de erro podem ser vistos no console Javascript." - _themeParseFailed: - title: "Erro na leitura do tema" - description: "Os dados solicitados foram obtidos com sucesso, mas houve um erro na leitura do tema. Por favor, contate o autor do tema. Detalhes de erro podem ser vistos no console Javascript." - _themeInstallFailed: - title: "Falha ao instalar tema" - description: "Houve um problema na instalação do tema. Por favor, tente novamente. Detalhes do erro podem ser vistos no console Javascript." -_dataSaver: - _media: - title: "Carregando mídia" - description: "Previne que mídia seja carregada automaticamente. Mídias escondidas serão carregadas quando selecionadas." - _avatar: - title: "Imagem do avatar" - description: "Parar animação de avatares. Imagens animadas podem ter um arquivo mais pesado do que imagens normais, potencialmente levando a reduções no tráfego de dados." - _code: - title: "Destaque de código" - description: "Se as notações de formatação de código forem utilizadas em MFM, elas não irão carregar até serem selecionadas. Destaque de código exige baixar arquivos de alta definição para cada linguagem de programação. Logo, desabilitar o carregamento automático desses arquivos diminui a quantidade de informação comunicada." -_hemisphere: - N: "Hemisfério Norte" - S: "Hemisfério Sul" - caption: "Utilizado em algumas configurações de aplicativo para determinar a estação do ano." -_reversi: - reversi: "Reversi" - gameSettings: "Configurações de jogo" - chooseBoard: "Escolha um tabuleiro" - blackOrWhite: "Preto/Branco" - blackIs: "{name} é as peças Pretas" - rules: "Regras" - thisGameIsStartedSoon: "O jogo começará em breve" - waitingForOther: "Esperando o turno do oponente" - waitingForMe: "Esperando o seu turno" - waitingBoth: "Prepare-se" - ready: "Pronto" - cancelReady: "Não pronto" - opponentTurn: "Turno do oponente" - myTurn: "Seu turno" - turnOf: "É o turno de {name}" - pastTurnOf: "Turno de {name}" - surrender: "Desistir" - surrendered: "Desistiu" - timeout: "Fim do tempo" - drawn: "Empate" - won: "{name} venceu" - black: "Preto" - white: "Branco" - total: "Total" - turnCount: "Turno {count}" - myGames: "Meus jogos" - allGames: "Todos os jogos" - ended: "Terminado" - playing: "Atualmente jogando" - isLlotheo: "Aquele com menos pedras vence (Llotheo)" - loopedMap: "Mapa em ‘loop’" - canPutEverywhere: "É possível pôr em qualquer lugar" - timeLimitForEachTurn: "Tempo limite por turno" - freeMatch: "Partida Livre" - lookingForPlayer: "À procura de adversários..." - gameCanceled: "A partida foi cancelada." - shareToTlTheGameWhenStart: "Compartilhar jogo na linha do tempo ao iniciar" - iStartedAGame: "O jogo começou! #MisskeyReversi" - opponentHasSettingsChanged: "O oponente alterou as configurações dele" - allowIrregularRules: "Regras irregulares (completamente livre)" - disallowIrregularRules: "Sem regras irregulares" - showBoardLabels: "Exibir numeração de linha e coluna no tabuleiro" - useAvatarAsStone: "Utilizar avatares de usuário como as pedras" -_offlineScreen: - title: "Offline - não foi possível conectar ao servidor" - header: "Não foi possível conectar ao servidor" -_urlPreviewSetting: - title: "Configurações da prévia de URL" - enable: "Habilitar prévia de URL" - timeout: "Tempo máximo para obter a prévia (ms)" - timeoutDescription: "Se demorar mais que esse valor para obter uma prévia, ela não será gerada." - maximumContentLength: "Content-Length máximo (em bytes)" - maximumContentLengthDescription: "Se o Content-Length for maior que esse valor, a prévia não será gerada." - requireContentLength: "Gerar previu apenas se houver cabeçalho Content-Length disponível na solicitação" - requireContentLengthDescription: "Se o outro servidor não retornar um cabeçalho Content-Length, a prévia não será gerada." - userAgent: "User-Agent" - userAgentDescription: "Define o User-Agent a ser usado ao gerar prévias. Se for deixado em branco, será usado o User-Agent padrão." - summaryProxy: "Endpoints do Proxy que geram prévias" - summaryProxyDescription: "Fora do Misskey, gerar prévias usando o Sumally Proxy." - summaryProxyDescription2: "Os parâmetros a seguir são vinculados ao proxy como um 'query string'. Se o proxy não os suportar, os valores serão ignorados." -_mediaControls: - pip: "Picture-in-Picture" - playbackRate: "Velocidade de Reprodução" - loop: "Reprodução em Loop" -_contextMenu: - title: "Menu de contexto" - app: "Aplicativo" - appWithShift: "Aplicativo com a tecla shift" - native: "Nativo" -_gridComponent: - _error: - requiredValue: "Esse valor é necessário" - columnTypeNotSupport: "Validação de expressões regulares (RegEx) só é permitida em colunas type:text." - patternNotMatch: "Esse valor não se encaixa no padrão de {pattern}" - notUnique: "Valor deve ser único" -_roleSelectDialog: - notSelected: "Não selecionado" -_customEmojisManager: - _gridCommon: - copySelectionRows: "Copiar linhas selecionadas" - copySelectionRanges: "Copiar seleção" - deleteSelectionRows: "Excluir linhas selecionadas" - deleteSelectionRanges: "Excluir valores selecionados" - searchSettings: "Opções de busca" - searchSettingCaption: "Definir critérios detalhados de busca." - searchLimit: "Limite de busca" - sortOrder: "Ordem de classificação" - registrationLogs: "Histórico de registros" - registrationLogsCaption: "Atualizações e remoções de emoji serão gravadas no histórico. Atualizar, remover, mover a uma nova página ou recarregar limpará o histórico" - alertEmojisRegisterFailedDescription: "Não foi possível atualizar ou remover emojis. Por favor, confira o histórico de registro para mais detalhes." - _logs: - showSuccessLogSwitch: "Exibir sucessos no histórico" - failureLogNothing: "Não há registro de falhas." - logNothing: "Não há registros." - _remote: - selectionRowDetail: "Detalhes da linha selecionada" - importSelectionRows: "Importar linhas selecionadas" - importSelectionRangesRows: "Importar linhas no intervalo" - importEmojisButton: "Importar Emojis selecionados" - confirmImportEmojisTitle: "Importar Emojis" - confirmImportEmojisDescription: "Importar {count} Emoji(s) recebidos de um servidor remoto. Por favor, preste atenção na licença do Emoji. Tem certeza que deseja continuar?" - _local: - tabTitleList: "Emojis registrados" - tabTitleRegister: "Registro de Emoji" - _list: - emojisNothing: "Não há Emojis registrados." - markAsDeleteTargetRows: "Marcar linhas selecionadas para remoção" - markAsDeleteTargetRanges: "Marcar linhas no intervalo para remoção" - alertUpdateEmojisNothingDescription: "Não há Emojis atualizados." - alertDeleteEmojisNothingDescription: "Não há Emojis marcados para remoção." - confirmMovePage: "Deseja mudar de página?" - confirmChangeView: "Deseja mudar de seção?" - confirmUpdateEmojisDescription: "Atualizando {count} Emoji(s). Deseja continuar?" - confirmDeleteEmojisDescription: "Removendo {count} Emoji(s) marcado(s). Deseja continuar?" - confirmResetDescription: "Todas as mudanças serão redefinidas." - confirmMovePageDesciption: "Mudanças foram feitas nos Emojis dessa página. Se você sair sem salvar, todas serão descartadas." - dialogSelectRoleTitle: "Buscar por cargo que pode usar esse Emoji" - _register: - uploadSettingTitle: "Configurações de envio" - uploadSettingDescription: "Nessa tela, você pode configurar o comportamento ao enviar Emojis." - directoryToCategoryLabel: "Transformar as pastas em categorias" - directoryToCategoryCaption: "Quando você arrastar um diretório, converter o caminho das pastas no campo \"categoria\"." - confirmRegisterEmojisDescription: "Registrando os Emojis da lista como novos Emojis personalizados. Deseja continuar? (Para evitar sobrecarga, apenas {count} Emoji(s) podem ser registrados em uma única operação)" - confirmClearEmojisDescription: "Descartando edições e limpando Emojis da lista. Deseja continuar?" - confirmUploadEmojisDescription: "Enviando {count} arquivo(s) arrastados ao drive. Deseja continuar?" -_embedCodeGen: - title: "Personalizar código do embed" - header: "Exibir cabeçalho" - autoload: "Carregar mais automaticamente (obsoleto)" - maxHeight: "Altura máxima" - maxHeightDescription: "Colocar em 0 desabilita a altura máxima. Especifique um valor para prevenir uma expansão vertical contínua." - maxHeightWarn: "O limite de altura máxima está desabilitado (0). Se isso não for intencional, insira um valor para a altura máxima." - previewIsNotActual: "A exibição difere do embed original porque ela excede o tamanho da tela de prévia." - rounded: "Tornar arredondado" - border: "Adicionar uma borda ao quadro externo" - applyToPreview: "Aplicar para a prévia" - generateCode: "Gerar código de embed" - codeGenerated: "O código foi gerado" - codeGeneratedDescription: "Coloque o código no seu website para incorporar o conteúdo." -_selfXssPrevention: - warning: "AVISO" - title: "\"Cole algo nessa tela\" é uma fraude" - description1: "Se você colar algo aqui, um usuário malicioso pode sabotar a sua conta ou roubar informações pessoais." - description2: "Se você não entender exatamente o que está colando, %cpare agora e feche essa janela." - description3: "Para mais informação, clique no link. {link}" -_followRequest: - recieved: "Aplicação recebida" - sent: "Aplicação enviada" -_remoteLookupErrors: - _federationNotAllowed: - title: "Não foi possível se comunicar com o servidor" - description: "Comunicação com esse servidor pode ter sido desabilitada ou o servidor pode ter sido bloqueado.\nPor favor, entre em contato com o administrador do servidor." - _uriInvalid: - title: "Endereço inválido" - description: "Há um problema com o endereço inserido. Por favor, confira se você não inseriu caracteres inválidos." - _requestFailed: - title: "Solicitação falhou" - description: "Comunicação com esse servidor falhou. O servidor pode estar inativo. Além disso, confira se você não inseriu um endereço inválido ou inexistente." - _responseInvalid: - title: "Resposta inválida" - description: "Foi possível comunicar com o servidor, porém os dados obtidos foram incorretos." - _noSuchObject: - title: "Não encontrado" - description: "O recurso solicitado não foi encontrado, confira o endereço." -_captcha: - verify: "Por favor, verifique o CAPTCHA" - testSiteKeyMessage: "Você pode conferir a prévia inserindo valores de teste para o site e chaves secretas.\nVeja a página seguinte para mais detalhes." - _error: - _requestFailed: - title: "O pedido do CAPTCHA falhou" - text: "Por favor, tente novamente ou verifique as configurações." - _verificationFailed: - title: "A validação do CAPTCHA falhou" - text: "Por favor, verifique se as configurações estão corretas." - _unknown: - title: "Erro CAPTCHA" - text: "Houve um erro inexperado." -_bootErrors: - title: "Falha ao carregar" - serverError: "Se o problema persistir após esperar um momento e recarregar, contate a administração da instância com o seguinte ID de erro." - solution: "O seguinte pode resolver o problema." - solution1: "Atualize seu navegador e sistema operacional para a última versão." - solution2: "Desative o bloqueador de anúncios" - solution3: "Limpe o cache do navegador" - solution4: "Defina dom.webaudio.enabled como verdadeiro no Navegador Tor" - otherOption: "Outras opções" - otherOption1: "Excluir ajustes de cliente e cache" - otherOption2: "Iniciar o cliente simples" - otherOption3: "Iniciar ferramenta de reparo" -_search: - searchScopeAll: "Todos" - searchScopeLocal: "Local" - searchScopeServer: "Servidor específico" - searchScopeUser: "Usuário específico" - pleaseEnterServerHost: "Insira o endereço do servidor" - pleaseSelectUser: "Selecione um usuário" - serverHostPlaceholder: "Exemplo: misskey.example.com" -_serverSetupWizard: - installCompleted: "Instalação do Misskey concluída!" - firstCreateAccount: "Para iniciar, crie uma conta de administrador." - accountCreated: "Conta de administrador foi criada!" - serverSetting: "Configurações de Servidor" - youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "O assistente facilita a configuração do servidor." - settingsYouMakeHereCanBeChangedLater: "Configurações alteradas pelo assistente podem ser ajustadas posteriormente." - howWillYouUseMisskey: "Como você usará o Misskey?" - _use: - single: "Servidor de Usuário Único" - single_description: "Utilizar servidor sozinho." - single_youCanCreateMultipleAccounts: "Múltiplas contas podem ser criadas se necessário, mesmo operando como servidor de usuário único." - group: "Servidor de Grupo" - group_description: "Convide outros usuários confiáveis para utilizar com mais de um usuário" - open: "Servidor Público" - open_description: "Permitir registro de todos." - openServerAdvice: "Aceitar um número alto de pessoas desconhecidas pode envolve um risco. Recomendamos que você opere com um sistema de moderação confiável para resolver quaisquer problemas." - openServerAntiSpamAdvice: "Para prevenir que o seu servidor se torne alvo de spam, é essencial cuidar da segurança habilitando recursos antibot como o reCAPTCHA." - howManyUsersDoYouExpect: "Quantos usuários você espera?" - _scale: - small: "Menos que 100 (pequeno porte)" - medium: "Entre 100 e 1000 usuários (médio porte)" - large: "Mais que 1000 usuários (larga escala)" - largeScaleServerAdvice: "Servidores de larga escala podem precisar de conhecimento avançado de infraestrutura, como balanceamento de carga e replicação de banco de dados." - doYouConnectToFediverse: "Você deseja conectar-se com o Fediverso?" - doYouConnectToFediverse_description1: "Quando conectado com uma rede distribuída de servidores (Fediverso), o conteúdo pode ser trocado com outros servidores." - doYouConnectToFediverse_description2: "Conectar com o Fediverso também é chamado de \"federação\"" - youCanConfigureMoreFederationSettingsLater: "Configurações adicionais como especificar servidores para conectar-se com podem ser feitas posteriormente" - adminInfo: "Informações da administração" - adminInfo_description: "Define as informações do administrador usadas para receber consultas." - adminInfo_mustBeFilled: "Deve ser preenchido se o servidor é público ou se a federação está ativa." - followingSettingsAreRecommended: "As configurações a seguir são recomendadas" - applyTheseSettings: "Aplicar essas configurações" - skipSettings: "Pular configuração" - settingsCompleted: "Instalação concluída!" - settingsCompleted_description: "Obrigado pelo seu tempo. Agora que tudo está pronto, você pode começar a utilizar o servidor." - settingsCompleted_description2: "As configurações do servidor podem ser alteradas no \"Painel de Controle\"" - donationRequest: "Solicitação de Doação" - _donationRequest: - text1: "Misskey é software aberto desenvolvido por voluntários." - text2: "Nós apreciaríamos o seu apoio para podermos continuar o desenvolvimento desse software no futuro." - text3: "Também há benefícios especiais para apoiadores!" -_clientPerformanceIssueTip: - title: "Dicas de desempenho" - makeSureDisabledAdBlocker: "Desative o seu bloqueador de anúncios" - makeSureDisabledAdBlocker_description: "Bloqueadores de anúncios podem afetar o desempenho. Certifique-se que eles não estão habilitados no seu sistema ou nos recursos/extensões do navegador. " - makeSureDisabledCustomCss: "Desabilite CSS personalizado" - makeSureDisabledCustomCss_description: "Substituir o estilo da página pode afetar o desempenho. Certifique-se que o CSS personalizado ou extensões que modifiquem o estilo da página estejam desabilitados." - makeSureDisabledAddons: "Desabilite extensões" - makeSureDisabledAddons_description: "Algumas extensões podem afetar comportamentos do cliente e afetar o desempenho. Por favor, desative as extensões do seu navegador e veja se isso melhora a situação." diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index abfaac7121..2d3099858d 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -1,30 +1,23 @@ --- _lang_: "Română" headlineMisskey: "O rețea conectată prin note" -introMisskey: "Bine ai venit! Misskey este un serviciu de microblogging open source și decentralizat.\nCreează \"note\" cu care să îți poți împărțasi gândurile cu oricine din jurul tău. 📡\nCu \"reacții\" îți poți exprima rapid părerea despre notele oricui. 👍\nHai să explorăm o lume nouă! 🚀" -poweredByMisskeyDescription: "{name} este unul dintre serviciile care se folosește de platforma open source Misskey." +introMisskey: "Bine ai venit! Misskey este un serviciu de microblogging open source și decentralizat.\nCreează \"note\" cu care să îți poți împărți gândurile cu oricine din jurul tău. 📡\nCu \"reacții\" îți poți expirma rapid părerea despre notele oricui. 👍\nHai să explorăm o lume nouă! 🚀" monthAndDay: "{day}/{month}" search: "Caută" -reset: "Resetează." notifications: "Notificări" username: "Nume de utilizator" password: "Parolă" -initialPasswordForSetup: "Parola pentru a începe configurarea inițială." -initialPasswordIsIncorrect: "Parola inițială este incorectă." -initialPasswordForSetupDescription: "Dacă ai instalat singur Misskey, utilizează parola pe care ai introdus-o în fișierul de configurare.\n\nDacă utilizezi un serviciu de găzduire(hosting) precum Misskey, te rugăm să utilizezi parola furnizată.\n\nDacă nu ai setat o parolă, las-o necompletată și mergi mai departe." forgotPassword: "Am uitat parola" -fetchingAsApObject: "Se preia din Fediverse..." +fetchingAsApObject: "Se aduce din Fediverse..." ok: "OK" gotIt: "Am înțeles!" cancel: "Anulează" -noThankYou: "Nu, mulțumesc." enterUsername: "Introdu numele de utilizator" renotedBy: "Re-notat de {user}" noNotes: "Nicio notă" noNotifications: "Nicio notificare" instance: "Instanță" settings: "Setări" -notificationSettings: "Setări notificări" basicSettings: "Setări generale" otherSettings: "Alte Setări" openInWindow: "Deschide într-o fereastră" @@ -49,28 +42,18 @@ pin: "Fixează pe profil" unpin: "Anulati fixare" copyContent: "Copiază conținutul" copyLink: "Copiază link-ul" -copyRemoteLink: "Copiază sursa externă." -copyLinkRenote: "Copiază linkul pentru re-notare" delete: "Şterge" deleteAndEdit: "Șterge și editează" -deleteAndEditConfirm: "Ești sigur(ă) că vrei să ștergi această notă și să o editezi? Vei pierde reacțiile, Re-Notele și răspunsurile acestora." +deleteAndEditConfirm: "Ești sigur că vrei să ștergi această notă și să o editezi? Vei pierde reacțiile, re-notele și răspunsurile acesteia." addToList: "Adaugă în listă" -addToAntenna: "Adaugă la antenă" sendMessage: "Trimite un mesaj" -copyRSS: "Copiază RSS" copyUsername: "Copiază numele de utilizator" -copyUserId: "Copiază ID-ul de utilizator" -copyNoteId: "Copiază ID-ul notiței" -copyFileId: "Copiază ID-ul fișierului" -copyFolderId: "Copiază ID-ul folderului" -copyProfileUrl: "Copiază URL-ul profilului " searchUser: "Caută un utilizator" -searchThisUsersNotes: "Caută în notele acestui utilizator." reply: "Răspunde" loadMore: "Incarcă mai mult" showMore: "Arată mai mult" showLess: "Închide" -youGotNewFollower: "Te-a urmărit" +youGotNewFollower: "te-a urmărit" receiveFollowRequest: "Cerere de urmărire primită" followRequestAccepted: "Cerere de urmărire acceptată" mention: "Mențiune" @@ -81,21 +64,21 @@ import: "Importă" export: "Exportă" files: "Fișiere" download: "Descarcă" -driveFileDeleteConfirm: "Ești sigur(ă) că vrei să ștergi fișierul \"{name}\"? Notele atașate fișierului vor fi și ele șterse." -unfollowConfirm: "Ești sigur(ă) că vrei să nu mai urmărești pe {name}?" +driveFileDeleteConfirm: "Ești sigur ca vrei să ștergi fișierul \"{name}\"? Notele atașate fișierului vor fi șterse și ele." +unfollowConfirm: "Ești sigur ca vrei să nu mai urmărești pe {name}?" exportRequested: "Ai cerut un export. S-ar putea să ia un pic. Va fi adăugat in Drive-ul tău odată completat." importRequested: "Ai cerut un import. S-ar putea să ia un pic." lists: "Liste" -noLists: "Nu ai nicio listă" +noLists: "Nu ai nici o listă" note: "Notă" notes: "Note" -following: "Îl urmărești" +following: "Urmărești" followers: "Urmăritori" followsYou: "Te urmărește" createList: "Creează listă" manageLists: "Gestionează listele" error: "Eroare" -somethingHappened: "A apărut o eroare" +somethingHappened: "A survenit o eroare" retry: "Reîncearcă" pageLoadError: "A apărut o eroare la încărcarea paginii." pageLoadErrorDescription: "De obicei asta este cauzat de o eroare de rețea sau cache-ul browser-ului. Încearcă să cureți cache-ul și apoi să încerci din nou puțin mai târziu." @@ -105,23 +88,18 @@ enterListName: "Introdu un nume pentru listă" privacy: "Confidenţialitate" makeFollowManuallyApprove: "Fă cererile de urmărire să necesite aprobare" defaultNoteVisibility: "Vizibilitate implicită" -follow: "Urmărește" +follow: "Urmărești" followRequest: "Trimite cerere de urmărire" followRequests: "Cereri de urmărire" unfollow: "Nu mai urmări" followRequestPending: "Cerere de urmărire în așteptare" enterEmoji: "Introdu un emoji" -renote: "Re-Notează" -unrenote: "Anulează re-nota" +renote: "Re-notează" +unrenote: "Ia înapoi re-nota" renoted: "Re-notat." -renotedToX: "Re-notă către {name}." cantRenote: "Această postare nu poate fi re-notată." cantReRenote: "O re-notă nu poate fi re-notată." quote: "Citează" -inChannelRenote: "Re-Notează în canal" -inChannelQuote: "Citează în canal" -renoteToChannel: "Re-notă către alte canale." -renoteToOtherChannel: "Re-notă către alte canale." pinnedNote: "Notă fixată" pinned: "Fixat pe profil" you: "Tu" @@ -130,52 +108,37 @@ sensitive: "NSFW" add: "Adaugă" reaction: "Reacție" reactions: "Reacție" -emojiPicker: "Selectator de emoji" -pinnedEmojisForReactionSettingDescription: "Poți seta emoji-urile să fie fixate atunci când reacționați." -pinnedEmojisSettingDescription: "Poți seta emoji-urile să fie fixate și afișate la introducerea emoji-urilor." -emojiPickerDisplay: "Meniu de selectare ale reacțiilor." -overwriteFromPinnedEmojisForReaction: "Ignoră din setările de reacție." -overwriteFromPinnedEmojis: "Ignoră din setările generale." +reactionSetting: "Reacții care să apară in selectorul de reacții" reactionSettingDescription2: "Trage pentru a rearanja, apasă pe \"+\" pentru a adăuga." rememberNoteVisibility: "Amintește setarea de vizibilitate a notelor" attachCancel: "Înlătură atașament" -deleteFile: "Șterge fișierul." markAsSensitive: "Marchează ca NSFW" unmarkAsSensitive: "Demarchează ca NSFW" -enterFileName: "Introdu numele fişierului" +enterFileName: "Introduceţi numele fişierului" mute: "Amuțește" unmute: "Înlătură amuțirea" -renoteMute: "Re-notări pe modul silențios" -renoteUnmute: "Scoate renotările de pe modul silențios" block: "Blochează" unblock: "Deblochează" suspend: "Suspendă" unsuspend: "Anulează suspendare" -blockConfirm: "Ești sigur(ă) că vrei să blochezi acest cont?" -unblockConfirm: "Ești sigur(ă) că vrei să deblochezi acest cont?" -suspendConfirm: "Ești sigur(ă) că vrei să suspendezi acest cont?" -unsuspendConfirm: "Ești sigur că vrei să nu mai suspendezi acest cont?" +blockConfirm: "Ești sigur că vrei să blochezi acest cont?" +unblockConfirm: "Ești sigur ca vrei să deblochezi acest cont?" +suspendConfirm: "Ești sigur ca vrei să suspendezi acest cont?" +unsuspendConfirm: "Ești sigur ca vrei să nu mai suspendezi acest cont?" selectList: "Selectează o listă" -editList: "Editează lista" -selectChannel: "Selectează canalul" selectAntenna: "Selectează o antenă" -editAntenna: "Editează antena" -createAntenna: "Creează o antenă." -selectWidget: "Alege un widget" +selectWidget: "Selectați un widget" editWidgets: "Editează widget-urile" editWidgetsExit: "Terminat" -customEmojis: "Emoji personalizate" +customEmojis: "Emoji personalizat" emoji: "Emoji" emojis: "Emoji-uri" emojiName: "Numele emoji-ului" emojiUrl: "URL-ul emoji-ului" addEmoji: "Adaugă un emoji" settingGuide: "Setări recomandate" -cacheRemoteFiles: "Reţine fișierele externe in memoria cache." -cacheRemoteFilesDescription: "Când această setare este dezactivată, fișierele externe sunt încărcate direct din instanța externă. Dezactivarea va scădea utilizarea spațiului de stocare, dar va crește traficul, deoarece miniaturile nu vor fi generate." -youCanCleanRemoteFilesCache: "Poți goli cache-ul prin a apăsa pe butonul de 🗑️ din fereastra de gestionare a fișierelor." -cacheRemoteSensitiveFiles: "Memorează în cache fișierele sensibile la distanță." -cacheRemoteSensitiveFilesDescription: "Dacă dezactivezi această setare, fișierele sensibile externe vor fi conectate direct și nu stocate în cache." +cacheRemoteFiles: "Ține fișierele externe in cache" +cacheRemoteFilesDescription: "Când această setare este dezactivată, fișierele externe sunt încărcate direct din instanța externă. Dezactivarea va scădea utilizarea spațiului de stocare, dar va crește traficul, deoarece thumbnail-urile nu vor fi generate." flagAsBot: "Marchează acest cont ca bot" flagAsBotDescription: "Activează această opțiune dacă acest cont este controlat de un program. Daca e activată, aceasta va juca rolul unui indicator pentru dezvoltatori pentru a preveni interacțiunea în lanțuri infinite cu ceilalți boți și ajustează sistemele interne al Misskey pentru a trata acest cont drept un bot." flagAsCat: "Marchează acest cont ca pisică" @@ -184,24 +147,18 @@ flagShowTimelineReplies: "Arată răspunsurile în cronologie" flagShowTimelineRepliesDescription: "Dacă e activată vor fi arătate în cronologie răspunsurile utilizatorilor către alte notele altor utilizatori." autoAcceptFollowed: "Aprobă automat cererile de urmărire de la utilizatorii pe care îi urmărești" addAccount: "Adaugă un cont" -reloadAccountsList: "Reîncarcă informațiile din lista de conturi" loginFailed: "Autentificare eșuată" showOnRemote: "Vezi mai multe pe instanța externă" -continueOnRemote: "Continuă de pe sursa externa." -chooseServerOnMisskeyHub: "Selectează un server din Hub-ul Misskey." -specifyServerHost: "Specifică un server gazdă(host)." -inputHostName: "Introdu numele gazdă(hostname)." general: "General" wallpaper: "Imagine de fundal" -setWallpaper: "Setează imaginea de fundal" +setWallpaper: "Setați imaginea de fundal" removeWallpaper: "Șterge imagine de fundal" searchWith: "Caută: {q}" youHaveNoLists: "Nu ai nici o listă" -followConfirm: "Ești sigur(ă) că vrei să urmărești pe {name}?" +followConfirm: "Ești sigur ca vrei să urmărești pe {name}?" proxyAccount: "Cont proxy" proxyAccountDescription: "Un cont proxy este un cont care se comportă ca un urmăritor extern pentru utilizatorii puși sub anumite condiții. De exemplu, când un cineva adaugă un utilizator extern intr-o listă, activitatea utilizatorului extern nu va fi adusă în instanță daca nici un utilizator local nu urmărește acel utilizator, așa că în schimb contul proxy îl va urmări." host: "Gazdă" -selectSelf: "Selectează-te pe tine însuți." selectUser: "Selectează un utilizator" recipient: "Destinatar" annotation: "Adnotări" @@ -216,8 +173,6 @@ perHour: "Pe oră" perDay: "Pe zi" stopActivityDelivery: "Nu mai trimite activități" blockThisInstance: "Blochează această instanță" -silenceThisInstance: "Ascunde acest server." -mediaSilenceThisInstance: "Ascunde conținutul media din acest server." operations: "Operațiuni" software: "Software" version: "Versiune" @@ -231,30 +186,24 @@ disk: "Disk" instanceInfo: "Informații despre instanță" statistics: "Statistici" clearQueue: "Șterge coada" -clearQueueConfirmTitle: "Ești sigur(ă) că vrei să cureți coada?" +clearQueueConfirmTitle: "Ești sigur că vrei să cureți coada?" clearQueueConfirmText: "Orice notă rămasă în coadă nu va fi federată. De obicei această operație nu este necesară." clearCachedFiles: "Golește cache-ul" -clearCachedFilesConfirm: "Ești sigur(ă) că vrei să ștergi toate fișierele externe din cache?" +clearCachedFilesConfirm: "Ești sigur că vrei să ștergi toate fișierele externe din cache?" blockedInstances: "Instanțe blocate" -blockedInstancesDescription: "Scrie numele gazdă(hostname) ale serverelor pe care dorești să le blochezi. Serverele listate nu vor mai putea să comunice cu acest server." -silencedInstances: "Servere ascunse." -silencedInstancesDescription: "Listează numele de gazdă(hostname) ale serverelor pe care dorești să le ascunzi, separate printr-o nouă linie de spațiere. Toate conturile care aparțin serverelor enumerate vor fi tratate ca fiind ascunse și pot face doar solicitări de urmărire și nu pot menționa conturi locale dacă nu sunt urmate. Acest lucru nu va afecta serverele blocate." -mediaSilencedInstances: "Servere cu conținutul media ascuns." -mediaSilencedInstancesDescription: "Setați numele de gazdă(hostname-urile) ale serverelor pe care dorești să le ascunzi, separate de o linie noua de spațiere. Orice fișier din conturile de pe un server cu sunet media vor fi tratate ca fiind sensibile și nu vor putea folosi emoji-uri personalizate. Nu are niciun efect asupra serverelor blocate." -federationAllowedHosts: "Servere permise pentru federare" -federationAllowedHostsDescription: "Specifica numele de gazdă ale serverelor pe care dorești să le permiți federarea, separate prin spații noi." +blockedInstancesDescription: "Scrie hostname-urile instanțelor pe care dorești să le blochezi. Instanțele listate nu vor mai putea să comunice cu această instanță." muteAndBlock: "Amuțiri și Blocări" mutedUsers: "Utilizatori amuțiți" blockedUsers: "Utilizatori blocați" noUsers: "Niciun utilizator" editProfile: "Editează profilul" -noteDeleteConfirm: "Ești sigur(ă) că vrei să ștergi această notă?" +noteDeleteConfirm: "Ești sigur că vrei să ștergi această notă?" pinLimitExceeded: "Nu poți mai fixa mai multe note" +intro: "Misskey s-a instalat! Te rog crează un utilizator admin." done: "Gata" processing: "Se procesează" preview: "Previzualizare" default: "Prestabilit" -defaultValueIs: "Valori implicite: {value}" noCustomEmojis: "Nu e niciun emoji" noJobs: "Nu e niciun job" federating: "Federație" @@ -265,7 +214,7 @@ subscribing: "Abonare" publishing: "Publicare" notResponding: "Nu răspunde" instanceFollowing: "Urmărind în instanță" -instanceFollowers: "Urmăritori al instanței" +instanceFollowers: "Urmăritori ai instanței" instanceUsers: "Utilizatori ai acestei instanțe" changePassword: "Schimbă parolă" security: "Securitate" @@ -283,11 +232,11 @@ announcements: "Anunțuri" imageUrl: "URL-ul imaginii" remove: "Şterge" removed: "Șterș cu succes" -removeAreYouSure: "Ești sigur(ă) că vrei să înlături {x}?" -deleteAreYouSure: "Ești sigur(ă) că vrei să ștergi {x}?" +removeAreYouSure: "Ești sigur că vrei să înlături {x}?" +deleteAreYouSure: "Ești sigur că vrei să ștergi {x}?" resetAreYouSure: "Sigur vrei să resetezi?" -areYouSure: "Ești sigur(ă)?" saved: "Salvat" +messaging: "Chat" upload: "Încarcă" keepOriginalUploading: "Păstrează imaginea originală" keepOriginalUploadingDescription: "Salvează imaginea originala încărcată fără modificări. Dacă e oprită, o versiune pentru afișarea pe web va fi generată la încărcare." @@ -300,13 +249,9 @@ uploadFromUrlMayTakeTime: "S-ar putea să ia puțin până se finalizează înc explore: "Explorează" messageRead: "Citit" noMoreHistory: "Nu există mai mult istoric" -startChat: "Pornește chat-ul" +startMessaging: "Începe un chat nou" nUsersRead: "citit de {n}" agreeTo: "Sunt de acord cu {0}" -agree: "De acord" -agreeBelow: "Sunt de acord cu cele menționate mai jos" -basicNotesBeforeCreateAccount: "Detalii importante" -termsOfService: "Termenii serviciului" start: "Să începem" home: "Acasă" remoteUserCaution: "Deoarece acest utilizator este dintr-o instanță externă, informația afișată poate fi incompletă." @@ -327,24 +272,21 @@ darkThemes: "Teme întunecate" syncDeviceDarkMode: "Sincronizează Modul Întunecat cu setările dispozitivului" drive: "Drive" fileName: "Nume fișier" -selectFile: "Alege un fișier" +selectFile: "Alege un fisier" selectFiles: "Alege fișiere" selectFolder: "Selectează un folder" selectFolders: "Selectează folderele" -fileNotSelected: "Niciun fișier selectat" renameFile: "Redenumește fișier" folderName: "Nume folder" createFolder: "Crează folder" renameFolder: "Redenumește acest folder" deleteFolder: "Șterge acest folder" -folder: "Folder" -addFile: "Adaugă un fișier" -showFile: "Arata fișierele" +addFile: "Adăugați un fișier" emptyDrive: "Drive-ul tău e gol" emptyFolder: "Folder-ul acesta este gol" unableToDelete: "Nu se poate șterge" inputNewFileName: "Introdu un nou nume de fișier" -inputNewDescription: "Introdu o titrare nouă" +inputNewDescription: "Introdu o descriere nouă" inputNewFolderName: "Introdu un nume de folder nou" circularReferenceFolder: "Destinația folderului este un subfolder al folderului pe care dorești să îl muți." hasChildFilesOrFolders: "Acest folder nu este gol, așa că nu poate fi șters." @@ -352,9 +294,8 @@ copyUrl: "Copiază URL" rename: "Redenumește" avatar: "Avatar" banner: "Banner" -displayOfSensitiveMedia: "Afișarea conținutului media sensibil" whenServerDisconnected: "Când pierzi conexiunea cu serverul" -disconnectedFromServer: "Conexiunea cu serverul a fost pierdută" +disconnectedFromServer: "Conecțiunea cu serverul a fost pierdută" reload: "Reîncarcă" doNothing: "Ignoră" reloadConfirm: "Ai dori să reîmprospătezi cronologia?" @@ -382,34 +323,29 @@ enableLocalTimeline: "Activează cronologia locală" enableGlobalTimeline: "Activeaza cronologia globală" disablingTimelinesInfo: "Administratorii și Moderatorii vor avea mereu access la toate cronologiile, chiar dacă nu sunt activate." registration: "Inregistrare" +enableRegistration: "Activează înregistrările pentru utilizatori noi" invite: "Invită" driveCapacityPerLocalAccount: "Capacitatea Drive-ului per utilizator local" driveCapacityPerRemoteAccount: "Capacitatea Drive-ului per utilizator extern" inMb: "În megabytes" +iconUrl: "URL-ul iconiței" bannerUrl: "URL-ul imaginii de banner" backgroundImageUrl: "URL-ul imaginii de fundal" basicInfo: "Informații de bază" pinnedUsers: "Utilizatori fixați" -pinnedUsersDescription: "Scrie utilizatorii, separați prin o linie de rând, care vor fi fixați pe pagina \"Explorează\"." +pinnedUsersDescription: "Scrie utilizatorii, separați prin pauză de rând, care vor fi fixați pe pagina \"Explorează\"." pinnedPages: "Pagini fixate" -pinnedPagesDescription: "Introdu linkurile Paginilor pe care le vrei fixate in vârful paginii acestei instanțe, separate de o linie de spațiere." +pinnedPagesDescription: "Introdu linkurile Paginilor pe care le vrei fixate in vâruful paginii acestei instanțe, separate de pauze de rând." pinnedClipId: "ID-ul clip-ului pe care să îl fixezi" pinnedNotes: "Notă fixată" hcaptcha: "hCaptcha" enableHcaptcha: "Activează hCaptcha" hcaptchaSiteKey: "Site key" hcaptchaSecretKey: "Secret key" -mcaptcha: "mCaptcha" -enableMcaptcha: "Permite mCaptcha" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret key" -mcaptchaInstanceUrl: "URL-ul serverului mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Activează reCAPTCHA" recaptchaSiteKey: "Site key" recaptchaSecretKey: "Secret key" -turnstile: "\nTurnstile" -enableTurnstile: "Permite Turnstile" turnstileSiteKey: "Site key" turnstileSecretKey: "Secret key" avoidMultiCaptchaConfirm: "Folosirea mai multor sisteme Captcha poate cauza interferență între acestea. Ai dori să dezactivezi alte sisteme Captcha acum active? Dacă preferi să rămână activate, apasă Anulare." @@ -419,11 +355,9 @@ name: "Nume" antennaSource: "Sursa antenei" antennaKeywords: "Cuvinte cheie ascultate" antennaExcludeKeywords: "Cuvinte cheie excluse" -antennaExcludeBots: "Exclude conturi tip bot" -antennaKeywordsDescription: "Separă cu spații pentru o condiție ''AND'' sau cu o linie de spațiere nouă pentru o condiție ''OR''." +antennaKeywordsDescription: "Separă cu spații pentru o condiție ȘI sau cu o întrerupere de rând pentru o condiție SAU." notifyAntenna: "Notifică-mă pentru note noi" withFileAntenna: "Doar note cu fișiere" -excludeNotesInSensitiveChannel: "Exclude note din canale sensibile" enableServiceworker: "Activează ServiceWorker" antennaUsersDescription: "Scrie un nume de utilizator per linie" caseSensitive: "Sensibil la majuscule și minuscule" @@ -432,13 +366,13 @@ connectedTo: "Următoarele conturi sunt conectate" notesAndReplies: "Note și răspunsuri" withFiles: "Incluzând fișiere" silence: "Amuțește" -silenceConfirm: "Ești sigur(ă) că vrei să amuțești acest utilizator?" +silenceConfirm: "Ești sigur că vrei să amuțești acest utilizator?" unsilence: "Anulează amuțirea" -unsilenceConfirm: "Ești sigur(ă) că vrei să anulezi amuțirea acestui utilizator?" +unsilenceConfirm: "Ești sigur că vrei să anulezi amuțirea acestui utilizator?" popularUsers: "Utilizatori populari" recentlyUpdatedUsers: "Utilizatori activi recent" recentlyRegisteredUsers: "Utilizatori ce s-au alăturat recent" -recentlyDiscoveredUsers: "Utilizatori recent descoperiți" +recentlyDiscoveredUsers: "Utilizatori descoperiți recent" exploreUsersCount: "Aici sunt {count} utilizatori" exploreFediverse: "Explorează Fediverse-ul" popularTags: "Taguri populare" @@ -447,24 +381,12 @@ about: "Despre" aboutMisskey: "Despre Misskey" administrator: "Administrator" token: "Token" -2fa: "Autentificare cu doi factori" -setupOf2fa: "Configurează autentificarea cu doi factori" -totp: "Aplicația de autentificare" -totpDescription: "Folosește o aplicație de autentificare pentru a putea utiliza parole de unica folosință" moderator: "Moderator" -moderation: "Moderare" -moderationNote: "Note de moderare" -moderationNoteDescription: "Poți completa note care vor fi partajate doar între moderatori." -addModerationNote: "Adaugă o notă de moderare" -moderationLogs: "Jurnal de moderare" nUsersMentioned: "Menționat de {n} utilizatori" -securityKeyAndPasskey: "Cheie de securitate - cheie de acces " securityKey: "Cheie de securitate" lastUsed: "Ultima utilizată" -lastUsedAt: "Ultima utilizare: {t}" unregister: "Dezînregistrează" passwordLessLogin: "Autentificare fără parolă" -passwordLessLoginDescription: "Permite autentificare fără parolă folosind doar o cheie de securitate sau o cheie de acces" resetPassword: "Resetează parola" newPasswordIs: "Noua parolă este \"{password}\"" reduceUiAnimation: "Redu animațiile interfeței" @@ -472,6 +394,7 @@ share: "Distribuie" notFound: "Nu a fost găsit" notFoundDescription: "N-a fost găsită nicio pagină cu acest URL." uploadFolder: "Folder implicit pentru încărcări" +cacheClear: "Golește cache-ul" markAsReadAllNotifications: "Marchează toate notificările drept citit" markAsReadAllUnreadNotes: "Marchează toate notele drept citit" markAsReadAllTalkMessages: "Marchează toate mesajele drept citit" @@ -489,10 +412,10 @@ retype: "Introdu din nou" noteOf: "Notă de {user}" quoteAttached: "Citat" quoteQuestion: "Vrei să adaugi ca citat?" -attachAsFileQuestion: "Textul clipboard-ului este lung. Dorești să-l atașezi ca fișier text?" +noMessagesYet: "Niciun mesaj încă" +newMessageExists: "Ai mesaje noi" onlyOneFileCanBeAttached: "Poți atașa un singur fișier la un mesaj" signinRequired: "Te rog autentifică-te" -signinOrContinueOnRemote: "Pentru a continua, trebuie să mergi la serverul dvs. sau să te înregistrezi și să te conectezi la acest server." invitations: "Invită" invitationCode: "Cod de invitație" checking: "Se verifică..." @@ -507,23 +430,14 @@ strongPassword: "Parolă puternică" passwordMatched: "Se potrivește!" passwordNotMatched: "Nu se potrivește" signinWith: "Autentifică-te cu {x}" -signinFailed: "Nu se poate autentifica. Numele de utilizator sau parola introdusă e incorectă." +signinFailed: "Nu se poate autentifica. Numele de utilizator sau parola introduse sunt incorecte." or: "Sau" language: "Limbă" uiLanguage: "Limba interfeței" aboutX: "Despre {x}" -emojiStyle: "Stil emoji" -native: "Nativ" -menuStyle: "Stilul meniului" -style: "Stil" -drawer: "Sertar" -popup: "Pop up" -showNoteActionsOnlyHover: "Afișează acțiunile de notare numai la trecerea cursorului" -showReactionsCount: "Afișează numărul de reacții la note" +disableDrawer: "Nu folosi meniuri în stil sertar" noHistory: "Nu există istoric" signinHistory: "Istoric autentificări" -enableAdvancedMfm: "Permite autentificarea multiplă(MFM) avansată" -enableAnimatedMfm: "Permite autentificarea multiplă(MFM) animată" doing: "Se procesează..." category: "Categorie" tags: "Etichete" @@ -532,8 +446,6 @@ createAccount: "Creează un cont" existingAccount: "Cont existent" regenerate: "Regenerează" fontSize: "Mărimea fontului" -mediaListWithOneImageAppearance: "Înălțimea listelor media cu o singură imagine" -limitTo: "Limitează până la {x}" noFollowRequests: "Nu ai nicio cerere de urmărire în așteptare" openImageInNewTab: "Deschide imaginile în taburi noi" dashboard: "Panou de control" @@ -567,12 +479,9 @@ objectStorageUseSSLDesc: "Oprește această opțiune dacă nu vei folosi HTTPS p objectStorageUseProxy: "Conectează-te prin Proxy" objectStorageUseProxyDesc: "Oprește această opțiune dacă vei nu folosi un Proxy pentru conexiunile API-ului" objectStorageSetPublicRead: "Setează \"public-read\" pentru încărcare" -s3ForcePathStyleDesc: "Dacă s3ForcePathStyle este activat, numele compartimentului trebuie inclus în calea adresei URL, spre deosebire de numele de gazdă(hostname) al adresei URL. Poate fi necesar să activezi această setare atunci când utilizezi servicii precum o instanță Minio găzduită de sine(self-hosted)." serverLogs: "Loguri server" deleteAll: "Șterge tot" showFixedPostForm: "Arată caseta de postare în vârful cronologie" -showFixedPostFormInChannel: "Afișează formularul de postare în partea de sus a cronologiei (Canale)" -withRepliesByDefaultForNewlyFollowed: "Include în mod prestabilit răspunsurile utilizatorilor nou urmăriți în cronologie" newNoteRecived: "Sunt note noi" sounds: "Sunete" sound: "Sunete" @@ -582,51 +491,37 @@ showInPage: "Arată în pagină" popout: "Scoate în afară" volume: "Volum" masterVolume: "Volumul principal" -notUseSound: "Oprește sunetul" -useSoundOnlyWhenActive: "Sunetele se aud numai dacă fereastra de Misskey este activă" details: "Detalii" -renoteDetails: "Detalii de re-notare" chooseEmoji: "Alege un emoji" unableToProcess: "Această operație nu poate fi completată" -recentUsed: "Folosit(e) recent" +recentUsed: "Folosit recent" install: "Instalează" uninstall: "Dezinstalează" installedApps: "Aplicații autorizate" nothing: "Nu e nimic de văzut aici" installedDate: "Autorizat la data de" -lastUsedDate: "Folosit(e) ultima oara la" +lastUsedDate: "Folosit ultima oara la" state: "Stare" sort: "Sortează" ascendingOrder: "Crescător" descendingOrder: "Descrescător" scratchpad: "Scratchpad" scratchpadDescription: "Scratchpad-ul oferă un mediu de experimentare în AiScript. Poți scrie, executa și verifica rezultatele acestuia interacționând cu Misskey în el." -uiInspector: "Inspector UI" -uiInspectorDescription: "Poți vedea lista de servere de componente UI în memorie. Componenta UI va fi generată de funcția Ui:C:." output: "Ieșire" script: "Script" disablePagesScript: "Dezactivează AiScript în Pagini" updateRemoteUser: "Actualizează informațiile utilizatorului extern" -unsetUserAvatar: "Anulează avatarul" -unsetUserAvatarConfirm: "Ești sigur(ă) că vrei sa anulezi avatarul?" -unsetUserBanner: "Avatarul utilizatorului a fost anulat" -unsetUserBannerConfirm: "Ești sigur(ă) că vrei sa anulezi bannerul?" deleteAllFiles: "Șterge toate fișierele" deleteAllFilesConfirm: "Ești sigur că vrei să ștergi toate fișierele?" -removeAllFollowing: "Elimină toți utilizatorii urmăriți" -removeAllFollowingDescription: "Asta va elimina urmărirea tuturor conturilor din {host}. Te rog execută asta numai dacă instanța, de ex., nu mai există." +removeAllFollowing: "Dezurmărește toți utilizatorii urmăriți" +removeAllFollowingDescription: "Asta va dez-urmări toate conturile din {host}. Te rog execută asta numai dacă instanța, de ex., nu mai există." userSuspended: "Acest utilizator a fost suspendat." userSilenced: "Acest utilizator a fost setat silențios." yourAccountSuspendedTitle: "Acest cont a fost suspendat" yourAccountSuspendedDescription: "Acest cont a fost suspendat din cauza încălcării termenilor de serviciu al serverului sau ceva similar. Contactează administratorul dacă ai dori să afli un motiv mai detaliat. Te rog nu crea un cont nou." -tokenRevoked: "Token invalid" -tokenRevokedDescription: "Token-ul a expirat.\nTe rugăm sa te reloghezi." -accountDeleted: "Cont șters." -accountDeletedDescription: "Acest cont a fost eliminat." menu: "Meniu" divider: "Separator" addItem: "Adaugă element" -rearrange: "Rearanjează" relays: "Relee" addRelay: "Adaugă Releu" inboxUrl: "URL-ul inbox-ului" @@ -649,11 +544,9 @@ author: "Autor" leaveConfirm: "Ai schimbări nesalvate. Vrei să renunți la ele?" manage: "Gestionare" plugins: "Pluginuri" -preferencesBackups: "Copii de rezervă ale preferințelor" deck: "Deck" undeck: "Părăsește Deck" useBlurEffectForModal: "Folosește efect de blur pentru modale" -useFullReactionPicker: "Utilizează selectorul de reacții de dimensiune completă" width: "Lăţime" height: "Înălţime" large: "Mare" @@ -661,7 +554,6 @@ medium: "Mediu" small: "Mic" generateAccessToken: "Generează token de acces" permission: "Permisiuni" -adminPermission: "Permisiuni administrator" enableAll: "Actevează tot" disableAll: "Dezactivează tot" tokenRequested: "Acordă acces la cont" @@ -683,26 +575,20 @@ smtpSecure: "Folosește SSL/TLS implicit pentru conecțiunile SMTP" smtpSecureInfo: "Oprește opțiunea asta dacă STARTTLS este folosit" testEmail: "Testează livrarea emailurilor" wordMute: "Cuvinte pe mut" -wordMuteDescription: "Minimizează notele care conțin cuvântul sau expresia specificată. Notele minimizate pot fi afișate făcând clic pe ele." -hardWordMute: "Amuțire pe cuvinte grele" -showMutedWord: "Arata cuvintele amuțite" -hardWordMuteDescription: "Ascunde notele care conțin fraza specificată. Spre deosebire de cuvintele amuțite, notele vor fi complet ascunse." regexpError: "Eroare de Expresie Regulată" regexpErrorDescription: "A apărut o eroare în expresia regulată pe linia {line} al cuvintelor {tab} setate pe mut:" instanceMute: "Instanțe pe mut" userSaysSomething: "{name} a spus ceva" -userSaysSomethingAbout: "{name} a scris ceva despre {name}" makeActive: "Activează" display: "Arată" copy: "Copiază" -copiedToClipboard: "Copiat în clipboard." metrics: "Metrici" overview: "Privire de ansamblu" logs: "Log-uri" delayed: "Întârziate" database: "Baza de date" channel: "Canale" -create: "Creează" +create: "Crează" notificationSetting: "Setări notificări" notificationSettingDesc: "Selectează tipurile de notificări care să fie arătate" useGlobalSetting: "Folosește setările globale" @@ -710,583 +596,58 @@ useGlobalSettingDesc: "Dacă opțiunea e pornită, notificările contului tău v other: "Altele" regenerateLoginToken: "Regenerează token de login" regenerateLoginTokenDescription: "Regenerează token-ul folosit intern în timpul logări. În mod normal asta nu este necesar. Odată regenerat, toate dispozitivele vor fi delogate." -theKeywordWhenSearchingForCustomEmoji: "Acesta este cuvântul cheie atunci când cauți emoji-uri personalizate." setMultipleBySeparatingWithSpace: "Separă mai multe intrări cu spații." fileIdOrUrl: "Introdu ID sau URL" behavior: "Comportament" sample: "exemplu" abuseReports: "Rapoarte" reportAbuse: "Raportează" -reportAbuseRenote: "Raportați Re-nota" reportAbuseOf: "Raportează {name}" fillAbuseReportDescription: "Te rog scrie detaliile legate de acest raport. Dacă este despre o notă specifică, te rog introdu URL-ul ei." abuseReported: "Raportul tău a fost trimis. Mulțumim." reporter: "Raportorul" reporteeOrigin: "Originea raportatului" reporterOrigin: "Originea raportorului" +forwardReport: "Redirecționează raportul către instanța externă" +forwardReportIsAnonymous: "În locul contului tău, va fi afișat un cont anonim, de sistem, ca raportor către instanța externă." send: "Trimite" +abuseMarkAsResolved: "Marchează raportul ca rezolvat" openInNewTab: "Deschide în tab nou" openInSideView: "Deschide în vedere laterală" defaultNavigationBehaviour: "Comportament de navigare implicit" editTheseSettingsMayBreakAccount: "Editarea acestor setări îți pot defecta contul." -instanceTicker: "Informații de instanță ale notelor" waitingFor: "Așteptând pentru {x}" -random: "Aleatoriu" +random: "Aleator" system: "Sistem" switchUi: "Schimbă UI" desktop: "Desktop" -clip: "Clip" -createNew: "Creează ceva nou" -optional: "Opțional" -createNewClip: "Creează un clip nou" -unclip: "Anulează clipul" -confirmToUnclipAlreadyClippedNote: "Această notă face deja parte din clipul „{name}”. Dorești, în schimb, să îl elimini din acest clip?" -public: "Public" -private: "Privat" -i18nInfo: "Misskey este tradusă în diferite limbi de către voluntari. Puteți ajuta accesând {link}." -manageAccessTokens: "Gestionați token-urile de acces" -accountInfo: "Informațiile contului" -notesCount: "Numărul de note" -repliesCount: "Numărul de răspunsuri trimise" -renotesCount: "Numărul de Re-Note trimise" -repliedCount: "Numărul de răspunsuri primite" -renotedCount: "Numărul de Re-Note primite" -followingCount: "Numărul de conturi urmărite" -followersCount: "Numărul de urmăritori" -sentReactionsCount: "Numărul de reacții trimise" -receivedReactionsCount: "Numărul de reacții primite" -pollVotesCount: "Numărul de voturi trimise la sondaj" -pollVotedCount: "Numărul de voturi în sondaj" -yes: "Da" -no: "Nu" -driveFilesCount: "Numărul de fișiere din drive" -driveUsage: "Gestionati spatiul de utilizare a drive-ului" -noCrawle: "Respingeți indexarea prin crawler" -noCrawleDescription: "Cere motoarelor de căutare să nu indexeze pagina de profil, noteele, paginile etc." -lockedAccountInfo: "Dacă nu setați vizibilitatea notei la „Numai persoane interesate”, notele vor fi vizibile pentru oricine, chiar dacă aveți nevoie de aprobarea manuală a persoanelor interesate." -alwaysMarkSensitive: "Marcați ca sensibil în mod prestabilit" -loadRawImages: "Încărcați imagini originale în loc să afișați miniaturile" -disableShowingAnimatedImages: "Nu reda imaginile animate" -highlightSensitiveMedia: "Evidențiază conținutul media sensibil" -verificationEmailSent: "A fost trimis un e-mail de confirmare. Urmează linkul din e-mail pentru a finaliza configurarea." -notSet: "Nesetat" -emailVerified: "E-mailul a fost verificat" -noteFavoritesCount: "Numărul de note preferate" -pageLikesCount: "Numărul de pagini apreciate" -pageLikedCount: "Numărul de aprecieri primite pe pagină" -contact: "Contact" -useSystemFont: "Utilizați fontul implicit al sistemului" -clips: "Clip" -experimentalFeatures: "Funcții experimentale" -experimental: "Experimental" -thisIsExperimentalFeature: "Aceasta este o funcție experimentală. Funcționalitatea sa este supusă modificării și este posibil să nu funcționeze conform intenției." -developer: "Dezvoltator" -makeExplorable: "Fă-ți contul vizibil în secțiunea„Explorați”" -makeExplorableDescription: "Dacă dezactivezi această opțiune, contul dvs. nu va fi vizibil în secțiunea\"Explorați\"." -duplicate: "Duplicat" -left: "Stânga" -center: "Centru" -wide: "Lat" -narrow: "Îngust" -reloadToApplySetting: "Setările vor fi replicate după reîncărcarea paginii." -needReloadToApply: "Este necesară o reîncărcare pentru ca acest lucru să se replice." -showTitlebar: "Afișează bara de titlu" clearCache: "Golește cache-ul" -onlineUsersCount: "{n} de utilizatori online" -nUsers: "{n} Utilizatori" -nNotes: "{n} de note" -sendErrorReports: "Trimite rapoartele de eroare" -sendErrorReportsDescription: "Când este pornit, informațiile detaliate despre erori vor fi partajate cu Misskey atunci când apare o problemă, ajutând la îmbunătățirea calității Misskey.\nAceasta va include informații precum versiunea sistemului de operare, ce browser utilizați, activitatea dvs. în Misskey etc." -myTheme: "Tema mea" -backgroundColor: "Culoare de fundal" -accentColor: "Culoare de accent" -textColor: "Culoarea textului" -saveAs: "Salvează ca..." -advanced: "Avansat" -advancedSettings: "Setări Avansate" -value: "Valoare" -createdAt: "Creat în" -updatedAt: "Actualizat la" -saveConfirm: "Salvezi modificările?" -deleteConfirm: "Sigur vrei să ștergi?" -invalidValue: "Valoare invalidă." -registry: "Registru" -closeAccount: "Șterge contul" -currentVersion: "Versiunea curentă" -latestVersion: "Versiunea cea mai nouă" -youAreRunningUpToDateClient: "Utilizezi cea mai nouă versiune a clientului" -newVersionOfClientAvailable: "Este disponibilă o nouă versiune a clientului." -usageAmount: "Utilizare" -capacity: "Capacitate" -inUse: "Folosit" -editCode: "Editează codul" -apply: "Aplică" -receiveAnnouncementFromInstance: "Primește notificări de la această instanță" -emailNotification: "Notificări prin e-mail" -publish: "Publică" -inChannelSearch: "Caută pe canal" -useReactionPickerForContextMenu: "Deschide selectorul de reacții făcând clic dreapta" -typingUsers: "{users} scriu/e chiar acum..." -jumpToSpecifiedDate: "Sari la o anumită dată" -showingPastTimeline: "În prezent, se afișează o cronologie veche" -clear: "Întoarce-te" -markAllAsRead: "Marchează ca ,,citit”" -goBack: "Înapoi" -unlikeConfirm: "Chiar îți elimini like-ul?" -fullView: "Ecran complet" -quitFullView: "Ieși din ecranul complet" -addDescription: "Adaugă o descriere" -userPagePinTip: "Poți afișa notele aici selectând „fixează pe profil” din meniul individual al fiecărei note " -notSpecifiedMentionWarning: "Există mențiuni ce nu sunt incluse în lista de destinatari" info: "Despre" -userInfo: "Informații despre utilizator" -unknown: "Necunoscut" -onlineStatus: "Stare online" -hideOnlineStatus: "Ascunde starea online" -hideOnlineStatusDescription: "Ascunderea stării dvs. online reduce confortul unor funcții, cum ar fi căutarea." -online: "Online" -active: "Disponibil" -offline: "Offline" -notRecommended: "Nerecomandat" -botProtection: "Protecție boți" -instanceBlocking: "Instanțe blocate/ascunse" -selectAccount: "Selectează un cont" -switchAccount: "Schimbă contul" -enabled: "Activat" -disabled: "Dezactivat" -quickAction: "Acțiuni rapide" user: "Utilizatori" administration: "Gestionare" -accounts: "Conturi" -switch: "Schimbă" -noMaintainerInformationWarning: "Informațiile întreținătorului nu sunt configurate." -noInquiryUrlWarning: "Adresa URL de cereri de informații nu este setata" -noBotProtectionWarning: "Protecția împotriva boților nu este configurată." -configure: "Configurează" -postToGallery: "Creează o postare nouă în galerie" -postToHashtag: "Postează pe acest hashtag" -gallery: "Galerie" -recentPosts: "Postări recente" -popularPosts: "Postări populare" -shareWithNote: "Distribuie cu notă" -ads: "Reclame" -expiration: "Termen limită" -startingperiod: "Start" -memo: "Memo" -priority: "Prioritate" -high: "Ridicată" middle: "Mediu" -low: "Scăzuta" -emailNotConfiguredWarning: "Adresa de e-mail nu este setată." -ratio: "Rație" -previewNoteText: "Afișează previzualizarea" -customCss: "CSS personalizat" -customCssWarn: "Această setare ar trebui folosită numai dacă știi ce face. Introducerea unor valori necorespunzătoare poate determina clientul să nu mai funcționeze normal." -global: "Global" -squareAvatars: "Afișează avatarele pătrate" sent: "Trimite" -received: "Primite" -searchResult: "Rezultate căutare" -hashtags: "Hashtag-uri" -troubleshooting: "Diagnosticare" -useBlurEffect: "Utilizează efecte de estompare în interfața de utilizare" -learnMore: "Află mai multe" -misskeyUpdated: "Misskey a fost actualizat!" -whatIsNew: "Vezi noile modificări" -translate: "Tradu" -translatedFrom: "Tradus din {x}" -accountDeletionInProgress: "Ștergerea contului este în curs de desfășurare" -usernameInfo: "Un nume care vă identifică contul de alții de pe acest server. Poți folosi alfabetul (a~z, A~Z), cifrele (0~9) sau litere de subliniere (_). Numele de utilizator nu pot fi schimbate ulterior." -aiChanMode: "Modul Ai" -devMode: "Modul Dezvoltator" -keepCw: "Păstrează avertismentele de conținut" -pubSub: "Conturi de Pub/Sub" -lastCommunication: "Ultima comunicare" -resolved: "Rezolvat" -unresolved: "Nerezolvat" -breakFollow: "Elimină urmăritorul" -breakFollowConfirm: "Chiar eliminați această urmărire?" -itsOn: "Activat" -itsOff: "Dezactivat" -on: "Pornit" -off: "Oprit" -emailRequiredForSignup: "E nevoie de o adresă de e-mail pentru înregistrare" -unread: "Necitit/e" -filter: "Filtru" -controlPanel: "Panou de Control" -manageAccounts: "Gestionează Conturile" -makeReactionsPublic: "Setați istoricul reacțiilor să fie public" -makeReactionsPublicDescription: "Faceți-vă reacțiile vizibile pentru toată lumea" -classic: "Clasic" -muteThread: "Amuțește thread-ul" -unmuteThread: "Dezmuțește thread-ul" -followingVisibility: "Vizibilitatea celor pe care ii urmărești" -followersVisibility: "Vizibilitatea celor care te urmărește" -continueThread: "Continuă thread-ul" -deleteAccountConfirm: "Acest lucru vă va șterge ireversibil contul. Continui?" -incorrectPassword: "Parolă incorectă." -incorrectTotp: "Parola unică este incorectă sau a expirat." -voteConfirm: "Confirmi votul pentru „{choice}”?" -hide: "Ascunde" -useDrawerReactionPickerForMobile: "Afișează selectorul de reacții ca sertar pe mobil" -welcomeBackWithName: "Bine ai revenit, {name}" -clickToFinishEmailVerification: "Dați clic pe [{ok}] pentru a finaliza verificarea e-mailului." -overridedDeviceKind: "Tipul de dispozitiv" -smartphone: "Smartphone" -tablet: "Tableta" -auto: "Auto" -themeColor: "Culoarea temei" -size: "Dimensiune" -numberOfColumn: "Numărul de coloane" searchByGoogle: "Caută" -instanceDefaultLightTheme: "Tema luminoasă implicită la nivelul întregii instanțe" -instanceDefaultDarkTheme: "Tema întunecată implicită la nivelul întregii instanțe" -instanceDefaultThemeDescription: "Introduceți codul temei în format obiect." -mutePeriod: "Durata amuțire" -period: "Timp limită" -indefinitely: "Permanent" -tenMinutes: "10 minute" -oneHour: "O oră" -oneDay: "O zi" -oneWeek: "O săptămâna" -oneMonth: "O lună" -threeMonths: "Trei luni" -oneYear: "Un an" -threeDays: "Trei zile" -reflectMayTakeTime: "Poate dura ceva timp pentru ca acest lucru să se replice." -failedToFetchAccountInformation: "Nu s-a putut prelua informațiile despre cont" -rateLimitExceeded: "Limita ratei a fost depășită" -cropImage: "Trunchiază imaginea" -cropImageAsk: "Dorești să trunchiezi această imagine?" -cropYes: "Trunchiază" -cropNo: "Utilizează-o așa cum e" file: "Fișiere" -recentNHours: "Ultimele {n} ore" -recentNDays: "Ultimele {n} zile" -noEmailServerWarning: "Serverul de e-mail nu este configurat." -thereIsUnresolvedAbuseReportWarning: "Sunt rapoarte nerezolvate." -recommended: "Recomandat" -check: "Verifică" -driveCapOverrideLabel: "Schimbă capacitatea de stocare a drive-ului pentru acest utilizator" -driveCapOverrideCaption: "Resetează capacitatea la valoarea implicită introducând o valoare de 0 sau mai mică." -requireAdminForView: "Trebuie să te conectezi cu un cont de administrator pentru a vedea această resursă." -isSystemAccount: "Un cont creat și operat automat de sistem." -typeToConfirm: "Introdu {x} pentru a confirma" -deleteAccount: "Șterge contul" -document: "Documentație" -numberOfPageCache: "Număr de pagini stocate cache" -numberOfPageCacheDescription: "Mărirea acestui număr va îmbunătăți conveniența, dar va cauza mai multă sarcină pe măsură ce se utilizează mai multă memorie pe dispozitivul utilizatorului.\n" -logoutConfirm: "Ești sigur(ă) că vrei să te deloghezi?" -logoutWillClearClientData: "Deconectarea va șterge setările clientului din browser. Pentru a putea restabili setările la autentificare, trebuie să activezi copia de rezervă automată a setărilor." -lastActiveDate: "Ultima dată de utilizare" -statusbar: "Bară de stare" -pleaseSelect: "Alege o opțiune" -reverse: "Invers" -colored: "Colorat" -refreshInterval: "Interval de actualizare" -label: "Etichetă" -type: "Tip" -speed: "Viteză" -slow: "Lent" -fast: "Rapid" -sensitiveMediaDetection: "Detectarea conținutului media sensibil" -localOnly: "Beta" -remoteOnly: "Doar externe" -failedToUpload: "Încărcare eșuată" -cannotUploadBecauseInappropriate: "Acest fișier nu a putut fi încărcat deoarece părți din acesta au fost detectate ca potențial neadecvate." -cannotUploadBecauseNoFreeSpace: "Încărcarea a eșuat datorită lipsei spațiului din drive." -cannotUploadBecauseExceedsFileSizeLimit: "Acest fișier nu poate fi încărcat deoarece depășește limita de dimensiune a fișierelor." -beta: "Beta" -enableAutoSensitive: "Marcare automată ca fiind conținut sensibil" -enableAutoSensitiveDescription: "Permite detectarea și marcarea automată a mediilor sensibile prin Machine Learning acolo unde este posibil. Chiar dacă această opțiune este dezactivată ea poate fi, în schimb, activă la nivelul întregii instanțe." -activeEmailValidationDescription: "Permite validarea mai strictă a adreselor de e-mail, care includ verificarea adreselor de unică folosință și dacă pot fi comunicate cu acestea. Când este debifat, este validat doar formatul e-mailului." -navbar: "Bara de navigare" -shuffle: "Amestecă" -account: "Conturi" -move: "Mută" -pushNotification: "Notificări tip „push”" -subscribePushNotification: "Permite notificările tip „push”" -unsubscribePushNotification: "Oprește notificările tip „push”" -pushNotificationAlreadySubscribed: "Notificările tip „push” sunt deja activate" -pushNotificationNotSupported: "Browserul sau instanța dvs. nu acceptă notificările tip „push”" -sendPushNotificationReadMessage: "Șterge notificările tip „push” după ce au fost citite" -sendPushNotificationReadMessageCaption: "Acest lucru poate crește consumul de energie al dispozitivului" -windowMaximize: "Maximizează" -windowMinimize: "Minimizează" -windowRestore: "Restabilește" -caption: "Titrare" -loggedInAsBot: "Conectat în prezent ca bot" -tools: "Unelte" -cannotLoad: "Nu se poate încărca" -numberOfProfileView: "Numărul de vizualizări ale profilului" -like: "Îmi place!" -unlike: "Îmi displace" -numberOfLikes: "Numărul de aprecieri" show: "Arată" -neverShow: "Nu mai afișa" -remindMeLater: "Poate mai târziu" -didYouLikeMisskey: "A început sa îți placa Misskey?" -pleaseDonate: "{host} folosește software-ul gratuit, Misskey. Am aprecia foarte mult donațiile dumneavoastră, astfel încât dezvoltarea Misskey să poată continua!" -correspondingSourceIsAvailable: "Codul sursă corespunzător este disponibil la {anchor}" -roles: "Roluri" -role: "Roluri" -noRole: "Rolul nu a fost găsit" -normalUser: "Utilizator obișnuit" -undefined: "Nedefinit" -assign: "Asignează" -unassign: "Dezasignează" -color: "Culoare" -manageCustomEmojis: "Gestionează emoji-uri personalizate" -manageAvatarDecorations: "Gestionați decorațiunile avatarului" -youCannotCreateAnymore: "Ai atins limita de creație." -cannotPerformTemporary: "Temporar indisponibil" -cannotPerformTemporaryDescription: "Această acțiune nu poate fi efectuată temporar din cauza depășirii limitei de execuție. Te rugăm să aștepți puțin și apoi să încerci din nou." -invalidParamError: "Parametri invalizi" -invalidParamErrorDescription: "Parametrii cererii sunt invalizi. Acest lucru este cauzat în mod normal de o eroare, dar se poate datora și intrărilor care depășesc limitele de dimensiune sau altceva similar." -permissionDeniedError: "Operațiune refuzată" -permissionDeniedErrorDescription: "Acest cont nu are permisiunea de a efectua această acțiune." -preset: "Presetate" -selectFromPresets: "Alege din presetate" -achievements: "Realizări" -gotInvalidResponseError: "Răspunsul serverului este invalid" -gotInvalidResponseErrorDescription: "Serverul poate fi oprit sau e în curs de întreținere. Te rugăm să încerci din nou după un timp." -thisPostMayBeAnnoying: "Această notă îi poate deranja pe alții." -thisPostMayBeAnnoyingHome: "Postează în cronologia de acasă" -thisPostMayBeAnnoyingCancel: "Anulează" -thisPostMayBeAnnoyingIgnore: "Postează oricum" -collapseRenotes: "Restrânge Re-Notările pe care le-ați văzut deja" -collapseRenotesDescription: "Restrânge notările pe care le-ați văzut deja" -internalServerError: "Eroare interna a serverului" -internalServerErrorDescription: "Serverul a întâmpinat o eroare neașteptată." -copyErrorInfo: "Copiază detaliile erorii" -joinThisServer: "Înregistrează-te în această instanță" -exploreOtherServers: "Caută o altă instanță" -letsLookAtTimeline: "Aruncă o privire la cronologie" -disableFederationConfirm: "Sigur vrei sa oprești federarea" -disableFederationConfirmWarn: "Chiar dacă sunt defederate, postările vor continua să fie publice, dacă nu sunt stabilite altfel. De obicei, nu trebuie să faceți acest lucru." -disableFederationOk: "Dezactivează" -invitationRequiredToRegister: "Acest server este în prezent accesibil numai pe bază de invitație. Se pot înregistra doar cei care au cod de invitație." -emailNotSupported: "Această instanță nu acceptă trimiterea de e-mailuri" -postToTheChannel: "Postează pe canal" -cannotBeChangedLater: "Nu poate fi schimbat ulterior" -reactionAcceptance: "Acceptarea reacțiilor" -likeOnly: "Doar aprecieri" -likeOnlyForRemote: "Toate (aplicabil numai pentru instanțe externe)" -nonSensitiveOnly: "Numai conținut non-sensibil" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Numai non-sensibile (aplicabil numai pentru aprecieri de la surse externe)" -rolesAssignedToMe: "Roluri asignate mie" -resetPasswordConfirm: "Sigur vrei sa îți resetezi parola" -sensitiveWords: "Cuvinte sensibile" -sensitiveWordsDescription: "Vizibilitatea tuturor notelor care conțin oricare dintre cuvintele configurate va fi setate automat la „Acasă”. Puteți enumera mai multe, separându-le prin o linie de spațiere nouă." -sensitiveWordsDescription2: "Folosirea spațiilor va crea expresii \"AND\" și înconjurând cuvintele cheie cu bare oblice le vor transforma într-o expresie obișnuită." -prohibitedWords: "Cuvinte interzise" -prohibitedWordsDescription: "Activează o eroare la încercarea de a posta o notă care conține cuvintele setate. Pot fi setate mai multe cuvinte, separate printr-o linie de spațiere nouă." -prohibitedWordsDescription2: "Folosirea spațiilor va crea expresii \"AND\" și înconjurând cuvintele cheie cu bare oblice le vor transforma într-o expresie obișnuită." -hiddenTags: "Hashtag-uri ascunse" -hiddenTagsDescription: "Selectați hashtag-uri care nu vor fi afișate în lista de tendințe.\nMai multe hashtag-uri pot fi înregistrate pe o linie de spațiere noua." -notesSearchNotAvailable: "Căutarea notelor este indisponibilă." -license: "Licență" -unfavoriteConfirm: "Sigur vrei să elimini din favorite?" -myClips: "Clipurile mele" -drivecleaner: "Curățitorul de drive" -retryAllQueuesNow: "Reîncearcă să rulezi toate cozile" -retryAllQueuesConfirmTitle: "Sigur vrei să le reîncerci din nou?" -retryAllQueuesConfirmText: "Acest lucru va crește temporar încărcarea rulării serverului." -enableChartsForRemoteUser: "Generează diagrame cu datele utilizatorilor externi" -enableChartsForFederatedInstances: "Generează diagrame de date ale instanțelor externe" -enableStatsForFederatedInstances: "Primește statistici ale serverelor externe" -showClipButtonInNoteFooter: "Adaugă „Clip” la meniul de acțiuni pentru note" -reactionsDisplaySize: "Dimensiunea afișajului de reacție" -limitWidthOfReaction: "Limitează lățimea maximă a reacțiilor și afișează-le în dimensiuni reduse." -noteIdOrUrl: "ID sau URL-ul notei" -video: "Video" -videos: "Video-uri" -audio: "Audio" -audioFiles: "Audio" -dataSaver: "Economizor de date" -accountMigration: "Migrarea contului" -accountMoved: "Acest utilizator a fost mutat într-un alt cont:" -accountMovedShort: "Acest cont a fost migrat." -operationForbidden: "Operațiune interzisă" -forceShowAds: "Afișează întotdeauna reclame" -addMemo: "Adaugă un memo" -editMemo: "Editează memo-ul" -reactionsList: "Reacții" -renotesList: "Re-Notări" -notificationDisplay: "Notificări" -leftTop: "Stânga-sus" -rightTop: "Dreapta-sus" -leftBottom: "Stânga-jos" -rightBottom: "Dreapta-jos" -stackAxis: "Direcția de stack-are" -vertical: "Vertical" -horizontal: "Orizontal" -position: "Poziție" -serverRules: "Regulamentul serverului" -pleaseConfirmBelowBeforeSignup: "Pentru a te înregistra pe acest server, trebuie să examinezi și să fii de acord cu următoarele:" -pleaseAgreeAllToContinue: "Trebuie să fii de acord cu toate câmpurile de mai sus pentru a continua." -continue: "Continuă" -preservedUsernames: "Nume rezervate de utilizator" -preservedUsernamesDescription: "Listeaza numele de utilizatori pentru a le rezerva, separate prin întreruperi de linie. Acestea vor deveni inutilizabile în timpul creării normale a contului, dar pot fi folosite de administratori pentru a crea conturi manual. Conturile deja existente care folosesc aceste nume de utilizator nu vor fi afectate." -createNoteFromTheFile: "Compuneți o notă din acest fișier" -archive: "Arhivă" -archived: "Arhivat" -unarchive: "Nearhivabil" -channelArchiveConfirmTitle: "Sigur vrei să arhivezi {name}?" -channelArchiveConfirmDescription: "Un canal arhivat nu va mai apărea în lista de canale sau în rezultatele căutării. De asemenea, postările noi nu mai pot fi adăugate la acesta." -thisChannelArchived: "Acest canal a fost arhivat." -displayOfNote: "Afișajul notelor" -initialAccountSetting: "Configurarea Profilului" -youFollowing: "Îl urmărești" -preventAiLearning: "Respinge utilizarea în Machine Learning (IA generativă)" -preventAiLearningDescription: "Solicită crawlerilor să nu folosească textul sau materialul de imagine postat etc. în seturile de date de învățare automată (AI predictivă/generativă). Acest lucru se realizează prin adăugarea unui flag „noai” HTML-Response la conținutul respectiv. Cu toate acestea, o prevenire completă nu poate fi realizată prin acest flag, deoarece poate fi pur și simplu ignorat." -options: "Opțiuni" -specifyUser: "Utilizator specific" -lookupConfirm: "Vrei să cauți?" -openTagPageConfirm: "Vrei să deschizi o pagină cu hashtag?" -specifyHost: "O gazdă(host) specifică" -failedToPreviewUrl: "Nu se poate previzualiza" -update: "Actualizare" -rolesThatCanBeUsedThisEmojiAsReaction: "Roluri care pot folosi acest emoji ca reacție" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Dacă nu sunt specificate rolurile, cineva poate folosi acest emoji ca reacție." -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Aceste roluri trebuie să fie publice." -cancelReactionConfirm: "Ești sigur(ă) că vrei să ștergi reacția ta?" -changeReactionConfirm: "Sigur vrei sa îți ștergi reacția?" -later: "Mai târziu" -goToMisskey: "Spre Misskey" -additionalEmojiDictionary: "Dicționare emoji suplimentare" -installed: "Instalat" -branding: "Branding" -enableServerMachineStats: "Publicați statistici hardware ale serverului" -enableIdenticonGeneration: "Activați generarea identicon a utilizatorului" -turnOffToImprovePerformance: "Oprirea acestei opțiuni poate crește performanța." -createInviteCode: "Generează invitația" -createWithOptions: "Generează cu opțiuni" -createCount: "Numărul de invitații" -inviteCodeCreated: "Invitație generată" -inviteLimitExceeded: "Ați depășit limita invitațiilor pe care le puteți genera." -createLimitRemaining: "Limită invitații : {limit} rămase" -inviteLimitResetCycle: "Această limită se va reseta la {limit} la {time}." -expirationDate: "Data de expirare" -noExpirationDate: "Fără expirare" -inviteCodeUsedAt: "Codul de invitație în" -registeredUserUsingInviteCode: "Invitație folosita de" -waitingForMailAuth: "Verificarea e-mailului este în așteptare" -inviteCodeCreator: "Invitație creată de" -usedAt: "Folosit în" -unused: "Neutilizat" -used: "Utilizat" -expired: "Expirat" -doYouAgree: "De-acord?" -beSureToReadThisAsItIsImportant: "Te rugăm citește informația aceasta importantă" -iHaveReadXCarefullyAndAgree: "Am citit textul „{x}” și sunt de acord." -dialog: "Dialog" -icon: "Avatar" -forYou: "Pentru tine" -currentAnnouncements: "Anunțuri curente" -pastAnnouncements: "Anunțuri anterioare" -youHaveUnreadAnnouncements: "Sunt anunțuri necitite." -useSecurityKey: "Te rugăm să urmezi instrucțiunile browserului sau ale dispozitivului tău pentru a-ți folosi cheia de securitate sau de acces." -replies: "Răspunde" -renotes: "Re-Note" -loadReplies: "Afișează răspunsurile" -loadConversation: "Afișează conversația" -pinnedList: "Lista fixată" -keepScreenOn: "Menține ecranul aprins" -verifiedLink: "Deținerea linkului a fost verificată" -notifyNotes: "Notifică-mă despre notele noi" -unnotifyNotes: "Nu mai mă notifica despre notele noi" -authentication: "Autentificare" -authenticationRequiredToContinue: "Te rugăm să te autentifici pentru a continua" -dateAndTime: "Data și ora" -showRenotes: "Afiseaza Re-Notele" -edited: "Editat" -notificationRecieveConfig: "Setări de notificare" -mutualFollow: "Vă urmăriți" -followingOrFollower: "Urmărit sau urmăritor" -fileAttachedOnly: "Numai Note cu fișiere" -showRepliesToOthersInTimeline: "Afișează răspunsurile către ceilalți în cronologie" -hideRepliesToOthersInTimeline: "Ascunde răspunsurile către ceilalți în cronologie" -showRepliesToOthersInTimelineAll: "Afișează răspunsurile către ceilalți de către cei ce ii urmărești în cronologie" -repositoryUrlDescription: "Dacă utilizați Misskey așa cum este (fără modificări ale codului sursă), introduceți https://github.com/misskey-dev/misskey" -flip: "Invers" -copyReplayData: "Copiază datele de reluare" -lastNDays: "Ultimele {n} zile" -surrender: "Anulează" -copyPreferenceId: "Copiază ID-ul preferințelor" -information: "Despre" -_chat: - invitations: "Invită" - noHistory: "Nu există istoric" - members: "Membri" - home: "Acasă" - send: "Trimite" -_accountSettings: - requireSigninToViewContentsDescription2: "Conținutul nu va fi afișat în previzualizările URL (OGP), încorporate în paginile web sau pe serverele care nu acceptă citările de note." - makeNotesFollowersOnlyBefore: "Face ca notele anterioare pentru a fi afișate numai pentru urmăritori" -_delivery: - stop: "Suspendat" - _type: - none: "Publicare" -_initialTutorial: - _note: - reply: "Face clic pe acest buton pentru a răspunde la un mesaj. De asemenea, este posibil să răspunzi la răspunsuri, continuând conversația ca pe un șir de replici(thread)." - menu: "Poți vedea detaliile ce țin de Note, să copiezi linkuri și să efectuezi alte acțiuni." - _timeline: - social: "Vor fi afișate notele din cronologia „Acasă'' și „Locală''." - _postNote: - _visibility: - localOnly: "Postarea cu acest flag nu va federa nota pe alte servere. Utilizatorii de pe alte servere nu vor putea vizualiza aceste note direct, indiferent de setările de afișare de mai sus." - _cw: - description: "În locul corpului, va fi afișat conținutul scris în câmpul „comentarii”. Apăsând „citește mai mult” va dezvălui corpul." - useCases: "Acesta este folosit atunci când respectați instrucțiunile serverului, pentru notele necesare sau pentru auto-restrângerea spoilerului sau a textului sensibil." -_timelineDescription: - social: "Cronologia socială afișează note atât din cronologia de ,,Acasă'', cât și din cea ,,Locală\"." _role: - assignTarget: "Asignează" - priority: "Prioritate" _priority: - low: "Scăzuta" middle: "Mediu" - high: "Ridicată" - _options: - canManageCustomEmojis: "Gestionează emoji-uri personalizate" - canManageAvatarDecorations: "Gestionați decorațiunile avatarului" -_ffVisibility: - public: "Publică" -_ad: - back: "Înapoi" -_gallery: - my: "Galeria mea" - liked: "Postări apreciate" - like: "Îmi place!" - unlike: "Îmi displace" _email: _follow: - title: "Ai un nou urmăritor" -_instanceMute: - instanceMuteDescription: "Aceasta va dezactiva orice notă/renotă din instanțele enumerate, inclusiv cele ale utilizatorilor care răspund unui utilizator dintr-o instanță mută." + title: "te-a urmărit" _theme: description: "Descriere" keys: - fg: "Text" mention: "Mențiune" - renote: "Re-Notează" + renote: "Re-notează" divider: "Separator" - toastFg: "Textul din notificare" - fgHighlighted: "Textul evidențiat" _sfx: note: "Note" notification: "Notificări" + chat: "Chat" _ago: invalid: "Nu e nimic de văzut aici" -_2fa: - renewTOTPCancel: "Nu, mulțumesc." -_permissions: - "read:gallery": "Vizualizează-ți galeria" - "write:gallery": "Editează-ți galeria" - "read:gallery-likes": "Vizualizează-ți lista de postări apreciate din galerie" - "write:gallery-likes": "Editează-ți lista de postări apreciate din galerie" _widgets: profile: "Profil" instanceInfo: "Informații despre instanță" @@ -1302,22 +663,10 @@ _cw: _visibility: home: "Acasă" followers: "Urmăritori" -_postForm: - replyPlaceholder: "Răspunde la această notă..." - quotePlaceholder: "Citează aceasta nota..." - channelPlaceholder: "Postează pe un canal..." - _placeholders: - a: "Ce mai faci?" - b: "Ce se mai petrece in jurul tău?" - c: "La ce te gândești?" - d: "Ce vrei să scrii?" - e: "Începe să scrii..." - f: "Te aștept să scrii..." _profile: name: "Nume" username: "Nume de utilizator" _exportOrImport: - clips: "Clip" followingList: "Urmărești" muteList: "Amuțește" blockingList: "Blochează" @@ -1326,28 +675,23 @@ _charts: federation: "Federație" _timelines: home: "Acasă" - local: "Local" - social: "Social" - global: "Global" _play: script: "Script" summary: "Descriere" _pages: blocks: - text: "Text" image: "Imagini" _notification: youWereFollowed: "te-a urmărit" _types: follow: "Urmărești" mention: "Mențiune" - renote: "Re-Note" + renote: "Re-notează" quote: "Citează" reaction: "Reacție" - login: "Autentifică-te" _actions: reply: "Răspunde" - renote: "Re-Notează" + renote: "Re-notează" _deck: _columns: notifications: "Notificări" @@ -1356,38 +700,5 @@ _deck: list: "Liste" channel: "Canale" mentions: "Mențiuni" - roleTimeline: "Cronologia rolului" _webhookSettings: name: "Nume" - active: "Activat" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" -_moderationLogTypes: - suspend: "Suspendă" - resetPassword: "Resetează parola" - createInvitation: "Generează invitația" - deleteGalleryPost: "Postarea din galerie a fost ștearsă" -_dataSaver: - _code: - title: "Evidențierea codului" - description: "Dacă notațiile de evidențiere a codului sunt utilizate în MFM etc., acestea nu se vor încărca până când sunt atinse. Evidențierea de sintaxă necesită descărcarea fișierelor de definiție de evidențiere pentru fiecare limbaj de programare. Prin urmare, dezactivarea încărcării automate a acestor fișiere este de așteptat să reducă cantitatea de date de comunicare." -_reversi: - total: "Total" -_contextMenu: - app: "Aplicație" - appWithShift: "Aplicatie ce utilizeaza tasta ,,shift\"" - native: "Nativ" -_customEmojisManager: - _gridCommon: - copySelectionRows: "Copiază rândurile selectate" - copySelectionRanges: "Copiază selecția" -_remoteLookupErrors: - _noSuchObject: - title: "Nu a fost găsit" -_search: - searchScopeAll: "Tot" - searchScopeLocal: "Local" - searchScopeUser: "Utilizator specific" - serverHostPlaceholder: "Exemplu: misskey.example.com" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 647cd0a0df..9100753b49 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -2,27 +2,23 @@ _lang_: "Русский" headlineMisskey: "Сеть, сплетённая из заметок" introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀" -poweredByMisskeyDescription: "{name} – сервис на платформе с открытым исходным кодом Misskey, называемый экземпляром Misskey." +poweredByMisskeyDescription: "{name} – сервис на платформе с открытым исходным кодом Misskey, называемый инстансом Misskey." monthAndDay: "{day}.{month}" search: "Поиск" -reset: "Сброс" notifications: "Уведомления" username: "Имя пользователя" password: "Пароль" -initialPasswordForSetup: "Пароль для начала настройки" -initialPasswordIsIncorrect: "Пароль для запуска настройки неверен" -initialPasswordForSetupDescription: "Если вы установили Misskey самостоятельно, используйте пароль, который вы указали в файле конфигурации.\nЕсли вы используете что-то вроде хостинга Misskey, используйте предоставленный пароль.\nЕсли вы не установили пароль, оставьте его пустым и продолжайте." forgotPassword: "Забыли пароль?" fetchingAsApObject: "Приём с других сайтов" -ok: "Подтвердить" +ok: "Окей" gotIt: "Ясно!" cancel: "Отмена" noThankYou: "Нет, спасибо" enterUsername: "Введите имя пользователя" -renotedBy: "{user} делает репост" +renotedBy: "{user} делится" noNotes: "Нет ни одной заметки" -noNotifications: "Нет уведомлений" -instance: "Экземпляр" +noNotifications: "Нет ни одного уведомления" +instance: "Инстанс" settings: "Настройки" notificationSettings: "Настройки уведомлений" basicSettings: "Основные настройки" @@ -49,26 +45,19 @@ pin: "Закрепить в профиле" unpin: "Открепить от профиля" copyContent: "Скопировать содержимое" copyLink: "Скопировать ссылку" -copyRemoteLink: "Скопировать ссылку на репост" -copyLinkRenote: "Скопировать ссылку на репост" delete: "Удалить" deleteAndEdit: "Удалить и отредактировать" -deleteAndEditConfirm: "Удалить этот пост и отредактировать заново? Все реакции, репосты и ответы на него также будут удалены." +deleteAndEditConfirm: "Удалить эту заметку и создать отредактированную? Все реакции, ссылки и ответы на существующую будут будут потеряны." addToList: "Добавить в список" -addToAntenna: "Добавить к антенне" sendMessage: "Отправить сообщение" copyRSS: "Скопировать RSS" copyUsername: "Скопировать имя пользователя" copyUserId: "Скопировать ID пользователя" -copyNoteId: "Скопировать ID поста" -copyFileId: "Скопировать ID файла" -copyFolderId: "Скопировать ID папки" -copyProfileUrl: "Скопировать ссылку на профиль" +copyNoteId: "Скопировать ID заметки" searchUser: "Поиск людей" -searchThisUsersNotes: "Искать по заметкам пользователя" -reply: "Ответ" -loadMore: "Загрузить ещё" -showMore: "Показать ещё" +reply: "Ответить" +loadMore: "Показать еще" +showMore: "Показать еще" showLess: "Закрыть" youGotNewFollower: "Новый подписчик" receiveFollowRequest: "Получен запрос на подписку" @@ -114,14 +103,11 @@ enterEmoji: "Введите эмодзи" renote: "Репост" unrenote: "Отмена репоста" renoted: "Репост совершён." -renotedToX: "Репостнуть в {name}." cantRenote: "Это нельзя репостить." cantReRenote: "Невозможно репостить репост." quote: "Цитата" inChannelRenote: "В канале" inChannelQuote: "Заметки в канале" -renoteToChannel: "Репостнуть в канал" -renoteToOtherChannel: "Репостнуть в другой канал" pinnedNote: "Закреплённая заметка" pinned: "Закрепить в профиле" you: "Вы" @@ -130,23 +116,17 @@ sensitive: "Содержимое не для всех" add: "Добавить" reaction: "Реакции" reactions: "Реакции" -emojiPicker: "Палитра эмодзи" -pinnedEmojisForReactionSettingDescription: "Здесь можно закрепить эмодзи для реакций" -pinnedEmojisSettingDescription: "Здесь можно закрепить эмодзи в общей палитре" -emojiPickerDisplay: "Внешний вид палитры" -overwriteFromPinnedEmojisForReaction: "Заменить на эмодзи из списка реакций" -overwriteFromPinnedEmojis: "Заменить на эмодзи из общего списка закреплённых" +reactionSetting: "Реакции, отображаемые в палитре" reactionSettingDescription2: "Расставляйте перетаскиванием, удаляйте нажатием, добавляйте кнопкой «+»." rememberNoteVisibility: "Запоминать видимость заметок" attachCancel: "Удалить вложение" -deleteFile: "Удалить файл" markAsSensitive: "Отметить как «не для всех»" unmarkAsSensitive: "Снять отметку «не для всех»" enterFileName: "Введите имя файла" mute: "Скрыть" unmute: "Отменить скрытие" -renoteMute: "Скрыть репосты" -renoteUnmute: "Открыть репосты" +renoteMute: "Заглушить репосты" +renoteUnmute: "Включить репосты" block: "Заблокировать" unblock: "Разблокировать" suspend: "Заморозить" @@ -156,11 +136,8 @@ unblockConfirm: "Разблокировать этот аккаунт?" suspendConfirm: "Заморозить этот аккаунт?" unsuspendConfirm: "Разморозить этот аккаунт?" selectList: "Выберите список" -editList: "Редактировать список" selectChannel: "Выберите канал" selectAntenna: "Выберите антенну" -editAntenna: "Редактировать антенну" -createAntenna: "Создать антенну" selectWidget: "Выберите виджет" editWidgets: "Редактировать виджеты" editWidgetsExit: "Готово" @@ -168,14 +145,11 @@ customEmojis: "Собственные эмодзи" emoji: "Эмодзи" emojis: "Эмодзи" emojiName: "Название эмодзи" -emojiUrl: "Ссылка на эмодзи" +emojiUrl: "URL эмодзи" addEmoji: "Добавить эмодзи" settingGuide: "Рекомендуемые настройки" cacheRemoteFiles: "Кешировать внешние файлы" cacheRemoteFilesDescription: "Когда эта настройка отключена, файлы с других сайтов будут загружаться прямо оттуда. Это сэкономит место на сервере, но увеличит трафик, так как не будут создаваться эскизы." -youCanCleanRemoteFilesCache: "Вы можете очистить кэш, нажав на кнопку 🗑️ в меню управления файлами." -cacheRemoteSensitiveFiles: "Кэшировать внешние файлы «не для всех»" -cacheRemoteSensitiveFilesDescription: "Если отключено, файлы «не для всех» загружаются непосредственно с удалённых серверов, не кэшируясь." flagAsBot: "Аккаунт бота" flagAsBotDescription: "Включите, если этот аккаунт управляется программой. Это позволит системе Misskey учитывать это, а также поможет разработчикам других ботов предотвратить бесконечные циклы взаимодействия." flagAsCat: "Аккаунт кота" @@ -187,10 +161,6 @@ addAccount: "Добавить учётную запись" reloadAccountsList: "Обновить список учётных записей" loginFailed: "Неудачная попытка входа" showOnRemote: "Перейти к оригиналу на сайт" -continueOnRemote: "Продолжить на удалённом сервере" -chooseServerOnMisskeyHub: "Выбрать сервер с Misskey Hub" -specifyServerHost: "Укажите сервер напрямую" -inputHostName: "Введите домен" general: "Общее" wallpaper: "Обои" setWallpaper: "Установить обои" @@ -201,7 +171,6 @@ followConfirm: "Подписаться на {name}?" proxyAccount: "Учётная запись прокси" proxyAccountDescription: "Учетная запись прокси предназначена служить подписчиком на пользователей с других сайтов. Например, если пользователь добавит кого-то с другого сайта а список, деятельность того не отобразится, пока никто с этого же сайта не подписан на него. Чтобы это стало возможным, на него подписывается прокси." host: "Хост" -selectSelf: "Выбрать себя" selectUser: "Выберите пользователя" recipient: "Кому" annotation: "Описание" @@ -216,11 +185,8 @@ perHour: "По часам" perDay: "По дням" stopActivityDelivery: "Остановить отправку обновлений активности" blockThisInstance: "Блокировать этот инстанс" -silenceThisInstance: "Заглушить этот инстанс" -mediaSilenceThisInstance: "Заглушить сервер" operations: "Операции" software: "Программы" -softwareName: "Software Name" version: "Версия" metadata: "Метаданные" withNFiles: "Файлы, {n} шт." @@ -238,12 +204,6 @@ clearCachedFiles: "Очистить кэш" clearCachedFilesConfirm: "Удалить все закэшированные файлы с других сайтов?" blockedInstances: "Заблокированные инстансы" blockedInstancesDescription: "Введите список инстансов, которые хотите заблокировать. Они больше не смогут обмениваться с вашим инстансом." -silencedInstances: "Заглушённые инстансы" -silencedInstancesDescription: "Перечислите имена серверов, которые вы хотите отключить, разделив их новой строкой. Все учетные записи, принадлежащие к указанным в списке серверам, будут заблокированы и смогут отправлять запросы только на повторное использование и не смогут указывать локальные учетные записи, если они не будут отслеживаться. Это не повлияет на заблокированные серверы." -mediaSilencedInstances: "Заглушённые сервера" -mediaSilencedInstancesDescription: "Укажите названия серверов, для которых вы хотите отключить доступ к файлам, по одному серверу в строке. Все учетные записи, принадлежащие к перечисленным серверам, будут считаться конфиденциальными и не смогут использовать пользовательские эмодзи. Это никак не повлияет на заблокированные серверы." -federationAllowedHosts: "Серверы, поддерживающие федерацию" -federationAllowedHostsDescription: "Укажите имена серверов, для которых вы хотите разрешить объединение, разделив их разделителями строк." muteAndBlock: "Скрытие и блокировка" mutedUsers: "Скрытые пользователи" blockedUsers: "Заблокированные пользователи" @@ -251,6 +211,7 @@ noUsers: "Нет ни одного пользователя" editProfile: "Редактировать профиль" noteDeleteConfirm: "Вы хотите удалить эту заметку?" pinLimitExceeded: "Нельзя закрепить ещё больше заметок" +intro: "Установка Misskey завершена! А теперь создайте учетную запись администратора." done: "Готово" processing: "Обработка" preview: "Предпросмотр" @@ -261,7 +222,7 @@ noJobs: "Нет заданий" federating: "Федерируется" blocked: "Заблокировано" suspended: "Заморожено" -all: "Все" +all: "Всё" subscribing: "Подписка" publishing: "Публикация" notResponding: "Нет ответа" @@ -287,12 +248,12 @@ removed: "Удалено" removeAreYouSure: "Хотите удалить «{x}»?" deleteAreYouSure: "Хотите удалить «{x}»?" resetAreYouSure: "На самом деле сбросить?" -areYouSure: "Вы уверены?" saved: "Сохранено" +messaging: "Сообщения" upload: "Загрузить" keepOriginalUploading: "Сохранить исходное изображение" keepOriginalUploadingDescription: "Сохраняет исходную версию при загрузке изображений. Если выключить, то при загрузке браузер генерирует изображение для публикации." -fromDrive: "С Диска" +fromDrive: "С «диска»" fromUrl: "По ссылке" uploadFromUrl: "Загрузить по ссылке" uploadFromUrlDescription: "Ссылка на файл, который хотите загрузить" @@ -301,10 +262,10 @@ uploadFromUrlMayTakeTime: "Загрузка может занять некото explore: "Обзор" messageRead: "Прочитали" noMoreHistory: "История закончилась" -startChat: "Начать чат" +startMessaging: "Начать общение" nUsersRead: "Прочитали {n}" agreeTo: "Я соглашаюсь с {0}" -agree: "Согласен" +agree: "Согласиться" agreeBelow: "Согласен со следующими" basicNotesBeforeCreateAccount: "Записи, перед созданием аккаунта" termsOfService: "Условия использования" @@ -332,15 +293,12 @@ selectFile: "Выберите файл" selectFiles: "Выберите файлы" selectFolder: "Выберите папку" selectFolders: "Выберите папки" -fileNotSelected: "Файл не выбран" renameFile: "Переименовать файл" folderName: "Имя папки" createFolder: "Создать папку" renameFolder: "Переименовать папку" deleteFolder: "Удалить папку" -folder: "Папка" addFile: "Добавить файл" -showFile: "Посмотреть файл" emptyDrive: "Диск пуст" emptyFolder: "Папка пуста" unableToDelete: "Удаление невозможно" @@ -353,7 +311,6 @@ copyUrl: "Копировать ссылку" rename: "Переименовать" avatar: "Аватар" banner: "Шапка" -displayOfSensitiveMedia: "Отображение содержимого не для всех" whenServerDisconnected: "Когда соединение с сервером потеряно" disconnectedFromServer: "Разорвано соединение с сервером" reload: "Перезагрузить" @@ -383,10 +340,12 @@ enableLocalTimeline: "Включить локальную ленту" enableGlobalTimeline: "Включить глобальную ленту" disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам, даже если они отключены." registration: "Регистрация" +enableRegistration: "Разрешить регистрацию" invite: "Пригласить" -driveCapacityPerLocalAccount: "Объём Диска на одного локального пользователя" -driveCapacityPerRemoteAccount: "Объём Диска на одного пользователя с другого экземпляра" +driveCapacityPerLocalAccount: "Объём диска на одного локального пользователя" +driveCapacityPerRemoteAccount: "Объём диска на одного пользователя с другого сайта" inMb: "В мегабайтах" +iconUrl: "Ссылка на аватар" bannerUrl: "Ссылка на изображение в шапке" backgroundImageUrl: "Ссылка на фоновое изображение" basicInfo: "Общая информация" @@ -400,11 +359,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Включить hCaptcha" hcaptchaSiteKey: "Ключ сайта" hcaptchaSecretKey: "Секретный ключ" -mcaptcha: "mCaptcha" -enableMcaptcha: "Включить mCaptcha" -mcaptchaSiteKey: "Ключ сайта" -mcaptchaSecretKey: "Секретный ключ" -mcaptchaInstanceUrl: "Ссылка на сервер mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Включить reCAPTCHA" recaptchaSiteKey: "Ключ сайта" @@ -419,12 +373,10 @@ manageAntennas: "Настройки антенн" name: "Название" antennaSource: "Источник антенны" antennaKeywords: "Ключевые слова" -antennaExcludeKeywords: "Чёрный список слов" -antennaExcludeBots: "Исключать ботов" +antennaExcludeKeywords: "Исключения" antennaKeywordsDescription: "Пишите слова через пробел в одной строке, чтобы ловить их появление вместе; на отдельных строках располагайте слова, или группы слов, чтобы ловить любые из них." notifyAntenna: "Уведомлять о новых заметках" withFileAntenna: "Только заметки с вложениями" -excludeNotesInSensitiveChannel: "Исключить заметки из конфиденциальных каналов" enableServiceworker: "Включить ServiceWorker" antennaUsersDescription: "Пишите каждое название аккаута на отдельной строке" caseSensitive: "С учётом регистра" @@ -448,16 +400,11 @@ about: "Описание" aboutMisskey: "О Misskey" administrator: "Администратор" token: "Токен" -2fa: "Двухфакторная аутентификация" -setupOf2fa: "Настроить двухфакторную аутентификацию" +2fa: "2-х факторная аутентификация" totp: "Приложение-аутентификатор" totpDescription: "Описание приложения-аутентификатора" moderator: "Модератор" moderation: "Модерация" -moderationNote: "Примечания модератора" -moderationNoteDescription: "Вы можете заполнять заметки, которые будут доступны только модераторам." -addModerationNote: "" -moderationLogs: "Журнал модерации" nUsersMentioned: "Упомянуло пользователей: {n}" securityKeyAndPasskey: "Ключ безопасности и парольная фраза" securityKey: "Ключ безопасности" @@ -473,6 +420,7 @@ share: "Поделиться" notFound: "Не найдено" notFoundDescription: "Страница по указанной ссылке не найдена" uploadFolder: "Место загрузки по умолчанию" +cacheClear: "Очистка кэша" markAsReadAllNotifications: "Отметить все уведомления как прочитанные" markAsReadAllUnreadNotes: "Отметить все заметки как прочитанные" markAsReadAllTalkMessages: "Отметить все реплики как прочитанные" @@ -490,10 +438,10 @@ retype: "Введите ещё раз" noteOf: "Что пишет {user}" quoteAttached: "Цитата" quoteQuestion: "Хотите добавить цитату?" -attachAsFileQuestion: "Текста в буфере обмена слишком много. Прикрепить как текстовый файл?" +noMessagesYet: "Пока ни одного сообщения" +newMessageExists: "Новое сообщение" onlyOneFileCanBeAttached: "К сообщению можно прикрепить только один файл" signinRequired: "Пожалуйста, войдите" -signinOrContinueOnRemote: "Чтобы продолжить, вам необходимо войти в аккаунт на своём сервере или зарегистрироваться / войти в аккаунт на этом." invitations: "Приглашения" invitationCode: "Код приглашения" checking: "Проверка" @@ -503,7 +451,7 @@ usernameInvalidFormat: "Можно использовать только лат tooShort: "Слишком короткий" tooLong: "Слишком длинный" weakPassword: "Слабый пароль" -normalPassword: "Хороший пароль" +normalPassword: "Годный пароль" strongPassword: "Надёжный пароль" passwordMatched: "Совпали" passwordNotMatched: "Не совпадают" @@ -515,12 +463,8 @@ uiLanguage: "Язык интерфейса" aboutX: "Описание {x}" emojiStyle: "Стиль эмодзи" native: "Системные" -menuStyle: "Стиль меню" -style: "Стиль" -drawer: "Панель" -popup: "Всплывающие окна" -showNoteActionsOnlyHover: "Показывать кнопки у заметок только при наведении" -showReactionsCount: "Видеть количество реакций на заметках" +disableDrawer: "Не использовать выдвижные меню" +showNoteActionsOnlyHover: "Показывать кнопки управления заметкой только при наведении" noHistory: "История пока пуста" signinHistory: "Журнал посещений" enableAdvancedMfm: "Включить расширенный MFM" @@ -533,8 +477,8 @@ createAccount: "Новая учётная запись" existingAccount: "Существующая учётная запись" regenerate: "Создать повторно" fontSize: "Размер шрифта" -mediaListWithOneImageAppearance: "Вид изображения, если оно единственное в списке" -limitTo: "Ограничить до {x}" +mediaListWithOneImageAppearance: "Показывать список медиа только одним изображением" +limitTo: "Обрезать до {x}" noFollowRequests: "Нерассмотренные запросы на подписку отсутствуют" openImageInNewTab: "Открыть изображение в новой вкладке" dashboard: "Панель управления" @@ -568,12 +512,11 @@ objectStorageUseSSLDesc: "Отключите, если не собираетес objectStorageUseProxy: "Использовать прокси" objectStorageUseProxyDesc: "Отключите, если не будете испоьзовать прокси для соединений по протоколу ObjectStorage." objectStorageSetPublicRead: "Устанавливать public-read при загрузке на сервер" -s3ForcePathStyleDesc: "Включение s3ForcePathStyle приводит к тому, что имя корзины указывается как часть пути в URL, а не в имени хоста. Может потребоваться включить при использовании локального Minio или чего-то подобного." +s3ForcePathStyleDesc: "Включение s3ForcePathStyle принудительно указывает имя корзины как часть пути в URL-адресе вместо имени хоста. Может потребоваться активация при использовании таких вещей, как локальный Minio." serverLogs: "Журнал сервера" deleteAll: "Удалить всё" showFixedPostForm: "Показывать поле для ввода новой заметки наверху ленты" showFixedPostFormInChannel: "Показывать поле для ввода новой заметки наверху ленты (каналы)" -withRepliesByDefaultForNewlyFollowed: "По умолчанию включайте ответы новых пользователей, на которых вы подписались, во временную шкалу" newNoteRecived: "Появилась новая заметка" sounds: "Звуки" sound: "Звуки" @@ -583,10 +526,7 @@ showInPage: "Показать страницу" popout: "Развернуть" volume: "Громкость" masterVolume: "Основная регулировка громкости" -notUseSound: "Выключить звук" -useSoundOnlyWhenActive: "Воспроизводить звук только когда Misskey активен." details: "Подробнее" -renoteDetails: "Узнать больше" chooseEmoji: "Выберите эмодзи" unableToProcess: "Не удаётся завершить операцию" recentUsed: "Последние использованные" @@ -602,16 +542,10 @@ ascendingOrder: "по возрастанию" descendingOrder: "По убыванию" scratchpad: "Когтеточка" scratchpadDescription: "«Когтеточка» — это место для опытов с AiScript. Здесь можно писать программы, взаимодействующие с Misskey, запускать и смотреть что из этого получается." -uiInspector: "Средство проверки пользовательского интерфейса" -uiInspectorDescription: "Вы можете просмотреть список экземпляров компонентов пользовательского интерфейса, существующих в памяти. Элементы пользовательского интерфейса генерируются с помощью серии функций Ui:C:." output: "Выходы" script: "Скрипт" disablePagesScript: "Отключить скрипты на «Страницах»" updateRemoteUser: "Обновить данные пользователя с его сервера" -unsetUserAvatar: "Убрать аватар" -unsetUserAvatarConfirm: "Вы точно хотите убрать аватар?" -unsetUserBanner: "Убрать баннер" -unsetUserBannerConfirm: "Вы точно хотите убрать баннер?" deleteAllFiles: "Удалить все файлы" deleteAllFilesConfirm: "Вы хотите удалить все файлы?" removeAllFollowing: "Удалить всех подписчиков" @@ -622,7 +556,7 @@ yourAccountSuspendedTitle: "Эта учетная запись заблокир yourAccountSuspendedDescription: "Эта учетная запись была заблокирована из-за нарушения условий предоставления услуг сервера. Свяжитесь с администратором, если вы хотите узнать более подробную причину. Пожалуйста, не создавайте новую учетную запись." tokenRevoked: "Токен недействителен" tokenRevokedDescription: "Срок действия вашего токена входа истек. Пожалуйста, войдите снова." -accountDeleted: "Учетная запись удалена" +accountDeleted: "Эта учетная запись удалена" accountDeletedDescription: "Эта учетная запись удалена" menu: "Меню" divider: "Линия-разделитель" @@ -641,7 +575,7 @@ poll: "Опрос" useCw: "Скрывать содержимое под предупреждением" enablePlayer: "Включить проигрыватель" disablePlayer: "Выключить проигрыватель" -expandTweet: "Развернуть заметку" +expandTweet: "Развернуть твит" themeEditor: "Редактор темы оформления" description: "Описание" describeFile: "Добавить подпись" @@ -653,7 +587,7 @@ plugins: "Расширения" preferencesBackups: "Резервная копия" deck: "Пульт" undeck: "Покинуть пульт" -useBlurEffectForModal: "Размытие за формой ввода заметки" +useBlurEffectForModal: "Размывка под формой поверх всего" useFullReactionPicker: "Полнофункциональный выбор реакций" width: "Ширина" height: "Высота" @@ -662,7 +596,6 @@ medium: "Средне" small: "Мелко" generateAccessToken: "Создать токен доступа" permission: "Разрешения" -adminPermission: "Доступ администратора" enableAll: "Включить все" disableAll: "Выключить всё" tokenRequested: "Открыть доступ к учётной записи" @@ -684,19 +617,13 @@ smtpSecure: "Использовать SSL/TLS для SMTP-соединений" smtpSecureInfo: "Выключите при использовании STARTTLS." testEmail: "Проверка доставки электронной почты" wordMute: "Скрытие слов" -wordMuteDescription: "Сведите к минимуму записи, содержащие указанное утверждение. Нажмите на свернутую запись, чтобы отобразить ее." -hardWordMute: "Строгое скрытие слов" -showMutedWord: "Отображать слово без уведомления (звука)" -hardWordMuteDescription: "Скрыть заметки, содержащие указанное слово или фразу. В отличие от word mute, заметка будет полностью скрыта от просмотра." regexpError: "Ошибка в регулярном выражении" regexpErrorDescription: "В списке {tab} скрытых слов, в строке {line} обнаружена синтаксическая ошибка:" instanceMute: "Глушение инстансов" userSaysSomething: "{name} что-то сообщает" -userSaysSomethingAbout: "{name} что-то говорил о「{word}」" makeActive: "Активировать" display: "Отображение" copy: "Копировать" -copiedToClipboard: "Скопированы в буфер обмена" metrics: "Метрики" overview: "Обзор" logs: "Журналы" @@ -707,25 +634,26 @@ create: "Создать" notificationSetting: "Настройки уведомлений" notificationSettingDesc: "Выберите тип уведомлений для отображения" useGlobalSetting: "Использовать глобальные настройки" -useGlobalSettingDesc: "Если включено, будут использоваться настройки учётной записи. Если отключить, этот виджет можно будет настроить индивидуально." +useGlobalSettingDesc: "Если включено, будут использоваться настройки учётной записи. Если включить, этот виджет можно будет настроить индивидуально." other: "Другие" regenerateLoginToken: "Создать новый токен для входа" regenerateLoginTokenDescription: "Создаёт новый токен, используемый внутри программы во время входа. Обычно в этом нет необходимости. При создании все устройства будут отключены." -theKeywordWhenSearchingForCustomEmoji: "Это ключевое слово будет использовано при поиске эмодзи." setMultipleBySeparatingWithSpace: "Можно написать несколько через пробел" fileIdOrUrl: "Идентификатор файла или ссылка" behavior: "Поведение" sample: "Пример" abuseReports: "Жалобы" reportAbuse: "Жалоба" -reportAbuseRenote: "Пожаловаться на репост" reportAbuseOf: "Пожаловаться на пользователя {name}" fillAbuseReportDescription: "Опишите, пожалуйста, причину жалобы подробнее. Если речь о конкретной заметке, будьте добры приложить ссылку на неё." abuseReported: "Жалоба отправлена. Большое спасибо за информацию." reporter: "Сообщивший" reporteeOrigin: "О ком сообщено" reporterOrigin: "Кто сообщил" +forwardReport: "Отправить жалобу на инстанс автора." +forwardReportIsAnonymous: "Жалоба на удалённый инстанс будет отправлена анонимно. Вместо ваших данных у получателя будет отображена системная учётная запись." send: "Отправить" +abuseMarkAsResolved: "Отметить жалобу как решённую" openInNewTab: "Открыть в новой вкладке" openInSideView: "Открывать в боковой колонке" defaultNavigationBehaviour: "Поведение навигации по умолчанию" @@ -743,7 +671,6 @@ createNewClip: "Новая подборка" unclip: "Убрать из подборки" confirmToUnclipAlreadyClippedNote: "Эта заметка уже есть в подборке «{name}». Удалить из этой подборки?" public: "Общедоступно" -private: "Личное" i18nInfo: "Misskey переводят на разные языки добровольцы со всего света. Ваша помощь тоже пригодится здесь: {link}." manageAccessTokens: "Управление токенами доступа" accountInfo: "Сведения об учётной записи" @@ -768,7 +695,6 @@ lockedAccountInfo: "Даже если вы вручную подтверждае alwaysMarkSensitive: "Отмечать файлы как «содержимое не для всех» по умолчанию" loadRawImages: "Сразу показывать изображения в полном размере" disableShowingAnimatedImages: "Не проигрывать анимацию" -highlightSensitiveMedia: "Выделять содержимое не для всех" verificationEmailSent: "Вам отправлено письмо для подтверждения. Пройдите, пожалуйста, по ссылке из письма, чтобы завершить проверку." notSet: "Не настроено" emailVerified: "Адрес электронной почты подтверждён." @@ -780,12 +706,13 @@ useSystemFont: "Использовать шрифт, предлагаемый с clips: "Подборки" experimentalFeatures: "Экспериментальные функции" experimental: "Экспериментальные" -thisIsExperimentalFeature: "Это экспериментальная функция. Её поведение, вероятно, поменяется в следующей версии, а ещё она может работать не так, как задумано." +thisIsExperimentalFeature: "Это экспериментальная функция. Технические характеристики могут измениться или он может работать неправильно." developer: "Разработчик" makeExplorable: "Опубликовать профиль в «Обзоре»." makeExplorableDescription: "Если выключить, ваш профиль не будет показан в разделе «Обзор»." +showGapBetweenNotesInTimeline: "Показывать разделитель между заметками в ленте" duplicate: "Дубликат" -left: "Слева" +left: "Влево" center: "По центру" wide: "Толстый" narrow: "Тонкий" @@ -861,11 +788,10 @@ administration: "Управление" accounts: "Учётные записи" switch: "Переключение" noMaintainerInformationWarning: "Не заполнены сведения об администраторах" -noInquiryUrlWarning: "URL-адрес контактной формы еще не задан." noBotProtectionWarning: "Ботозащита не настроена" configure: "Настроить" postToGallery: "Опубликовать в галерею" -postToHashtag: "Написать заметку с этим хештегом" +postToHashtag: "Опубликовать пост с этим хештегом" gallery: "Галерея" recentPosts: "Недавние публикации" popularPosts: "Популярные публикации" @@ -882,35 +808,33 @@ emailNotConfiguredWarning: "Не указан адрес электронной ratio: "Соотношение" previewNoteText: "Предварительный просмотр" customCss: "Индивидуальный CSS" -customCssWarn: "Используйте эту настройку только если знаете, что делаете. Ошибки здесь чреваты тем, что у вас перестанет нормально работать сайт." +customCssWarn: "Используйте эту настройку только если знаете, что делаете. Ошибки здесь чреваты тем, что сайт перестанет нормально работать у вас." global: "Всеобщая" squareAvatars: "Квадратные аватарки" sent: "Отправить" received: "Получено" searchResult: "Результаты поиска" -hashtags: "Хештеги" +hashtags: "Хэштег" troubleshooting: "Разрешение проблем" useBlurEffect: "Размытие в интерфейсе" learnMore: "Подробнее" misskeyUpdated: "Misskey обновился!" whatIsNew: "Что новенького?" -translate: "Перевести" +translate: "Перевод" translatedFrom: "Перевод. Язык оригинала — {x}" accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи" usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже." aiChanMode: "Режим Ай" devMode: "Режим разработчика" -keepCw: "Сохраняйте предупреждения о содержимом" +keepCw: "Сохраняйте Предупреждения о содержимом" pubSub: "Учётные записи Pub/Sub" lastCommunication: "Последнее сообщение" resolved: "Решено" unresolved: "Без решения" breakFollow: "Отписка" -breakFollowConfirm: "Действительно удалить этого подписчика?" +breakFollowConfirm: "Удалить из подписок пользователя ?" itsOn: "Включено" itsOff: "Выключено" -on: "Вкл." -off: "Выкл." emailRequiredForSignup: "Для регистрации учётной записи нужен адрес электронной почты" unread: "Непрочитанное" filter: "Фильтры" @@ -921,12 +845,11 @@ makeReactionsPublicDescription: "Список сделанных вами реа classic: "Классика" muteThread: "Скрыть цепочку" unmuteThread: "Отменить сокрытие цепочки" -followingVisibility: "Видимость подписок" -followersVisibility: "Видимость подписчиков" +ffVisibility: "Видимость подписок и подписчиков" +ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков." continueThread: "Показать следующие ответы" deleteAccountConfirm: "Учётная запись будет безвозвратно удалена. Подтверждаете?" incorrectPassword: "Пароль неверен." -incorrectTotp: "Введен неверный одноразовый пароль или срок его действия истек." voteConfirm: "Отдать голос за «{choice}»?" hide: "Спрятать" useDrawerReactionPickerForMobile: "Выдвижная палитра на мобильном устройстве" @@ -942,7 +865,7 @@ numberOfColumn: "Количество столбцов" searchByGoogle: "Поиск" instanceDefaultLightTheme: "Светлая тема по умолчанию" instanceDefaultDarkTheme: "Темная тема по умолчанию" -instanceDefaultThemeDescription: "Введите код темы в формате объекта." +instanceDefaultThemeDescription: "Описание темы по умолчанию для инстанса" mutePeriod: "Продолжительность скрытия" period: "Опрос длится" indefinitely: "вечно" @@ -951,9 +874,6 @@ oneHour: "1 час" oneDay: "1 день" oneWeek: "1 неделя" oneMonth: "1 месяц" -threeMonths: "3 месяца" -oneYear: "1 год" -threeDays: "3 дня" reflectMayTakeTime: "Изменения могут занять время для отображения" failedToFetchAccountInformation: "Не удалось получить информацию об аккаунте" rateLimitExceeded: "Ограничение скорости превышено" @@ -969,7 +889,7 @@ thereIsUnresolvedAbuseReportWarning: "Остались нерешённые жа recommended: "Рекомендуем" check: "Проверить" driveCapOverrideLabel: "Изменение лимита дискового пространства для этого пользователя" -driveCapOverrideCaption: "Введите нуль или меньше, чтобы использовать значение по умолчанию." +driveCapOverrideCaption: "Укажите меньше или равное нулю для отмены" requireAdminForView: "Для просмотра необходимо иметь аккаунт администратора" isSystemAccount: "Данная учётная запись создана автоматически и управляется системой" typeToConfirm: "Введите {x} для продолжения" @@ -978,7 +898,6 @@ document: "Документ" numberOfPageCache: "Количество сохранённых страниц в кэше" numberOfPageCacheDescription: "Описание количества страниц в кэше" logoutConfirm: "Вы хотите выйти из аккаунта?" -logoutWillClearClientData: "Когда вы выйдете из системы, информация о конфигурации клиента будет удалена из браузера.Чтобы иметь возможность восстановить информацию о вашей конфигурации при повторном входе в систему, пожалуйста, включите опцию автоматического резервного копирования в настройках." lastActiveDate: "Последняя дата использования" statusbar: "Статусбар" pleaseSelect: "Пожалуйста, выберите" @@ -990,7 +909,7 @@ type: "Тип" speed: "Скорость" slow: "Медленная" fast: "Быстрая" -sensitiveMediaDetection: "Распознание содержимого не для всех" +sensitiveMediaDetection: "Определение содержимого деликатного характера" localOnly: "Локально" remoteOnly: "Только удалённо" failedToUpload: "Сбой выгрузки" @@ -1023,12 +942,11 @@ numberOfProfileView: "Количество профилей для просмо like: "Нравится!" unlike: "Отменить «нравится»" numberOfLikes: "Количество лайков" -show: "Показать" +show: "Отображение" neverShow: "Больше не показывать" remindMeLater: "Напомнить позже" didYouLikeMisskey: "Вам нравится Misskey?" pleaseDonate: "Сайт {host} работает на Misskey. Это бесплатное программное обеспечение, и ваши пожертвования очень бы помогли продолжать его разработку!" -correspondingSourceIsAvailable: "Соответствующий исходный код можно найти по адресу {anchor} " roles: "Роли" role: "Роль" noRole: "Нет роли" @@ -1038,7 +956,6 @@ assign: "Назначить" unassign: "Отменить назначение" color: "Цвет" manageCustomEmojis: "Управлять пользовательскими эмодзи" -manageAvatarDecorations: "Управление украшениями аватара" youCannotCreateAnymore: "Вы достигли лимита создания." cannotPerformTemporary: "Временно недоступен" cannotPerformTemporaryDescription: "Это действие временно невозможно выполнить из-за превышения лимита выполнения." @@ -1055,8 +972,7 @@ thisPostMayBeAnnoying: "Это сообщение может быть непри thisPostMayBeAnnoyingHome: "Этот пост может быть отправлен на главную" thisPostMayBeAnnoyingCancel: "Этот пост не может быть отменен." thisPostMayBeAnnoyingIgnore: "Этот пост может быть проигнорирован " -collapseRenotes: "Сворачивать увиденные репосты" -collapseRenotesDescription: "Сворачивать посты с которыми вы взаимодействовали." +collapseRenotes: "Свернуть репосты" internalServerError: "Внутренняя ошибка сервера" internalServerErrorDescription: "Внутри сервера произошла непредвиденная ошибка." copyErrorInfo: "Скопировать код ошибки" @@ -1070,198 +986,33 @@ invitationRequiredToRegister: "Этот сервер в настоящее вр emailNotSupported: "Доставка почты не поддерживается на этом сервере" postToTheChannel: "Отправить в канал" cannotBeChangedLater: "Это нельзя изменить позже" -reactionAcceptance: "Допустимые реакции" -likeOnly: "Только «нравится!»" -likeOnlyForRemote: "Всё (с других серверов только «нравится!»)" -nonSensitiveOnly: "Только безопасные" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Только безопасные (с других серверов только «нравится!»)" +reactionAcceptance: "Принятие реакций" +likeOnly: "Только лайки" +likeOnlyForRemote: "Только лайки с удалённых серверов" rolesAssignedToMe: "Мои роли" resetPasswordConfirm: "Сбросить пароль?" sensitiveWords: "Чувствительные слова" sensitiveWordsDescription: "Установите общедоступный диапазон заметки, содержащей заданное слово, на домашний. Можно сделать несколько настроек, разделив их переносами строк." sensitiveWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение." -prohibitedWords: "Запрещённые слова" -prohibitedWordsDescription: "Включает вывод ошибки при попытке опубликовать пост, содержащий указанное слово/набор слов.\nМножество слов может быть указано, разделяемые новой строкой." -prohibitedWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение." -hiddenTags: "Скрытые хештеги" -hiddenTagsDescription: "Установленные теги не будут отображаться в тренде, можно установить несколько тегов." notesSearchNotAvailable: "Поиск заметок недоступен" license: "Лицензия" unfavoriteConfirm: "Удалить избранное?" -myClips: "Мои подборки" +myClips: "Мои клипы" drivecleaner: "Очиститель дисков" retryAllQueuesNow: "Повторить все очереди сейчас" retryAllQueuesConfirmTitle: "Хотите попробовать ещё раз?" retryAllQueuesConfirmText: "Нагрузка на сервер может увеличиться" enableChartsForRemoteUser: "Создание диаграмм для удалённых пользователей" enableChartsForFederatedInstances: "Создание диаграмм для удалённых серверов" -enableStatsForFederatedInstances: "Получить информацию об удаленном сервере" -showClipButtonInNoteFooter: "Показать кнопку добавления в подборку в меню действий с заметкой" -reactionsDisplaySize: "Размер реакций" -limitWidthOfReaction: "Ограничить максимальную ширину реакций и отображать их в уменьшенном размере." +largeNoteReactions: "Показывать большие реакции на заметки" noteIdOrUrl: "ID или ссылка на заметку" video: "Видео" videos: "Видео" -audio: "Звук" -audioFiles: "Звуковые файлы" dataSaver: "Экономия трафика" -accountMigration: "Перенос учётной записи" -accountMoved: "Учётная запись перенесена" -accountMovedShort: "Эта учётная запись перемещена" -operationForbidden: "Это действие запрещено" -forceShowAds: "Всегда отображать рекламу" -addMemo: "Добавить памятку" -editMemo: "Изменить памятку" -reactionsList: "Список реакций" renotesList: "Репосты" -notificationDisplay: "Отображение уведомлений" -leftTop: "Слева вверху" -rightTop: "Справа сверху" -leftBottom: "Слева внизу" -rightBottom: "Справа внизу" -stackAxis: "Положение уведомлений" -vertical: "Вертикально" -horizontal: "Горизонтально" -position: "Позиция" -serverRules: "Правила сервера" -pleaseConfirmBelowBeforeSignup: "Для регистрации на данном сервере, необходимо согласится с нижеследующими положениями." -pleaseAgreeAllToContinue: "Чтобы продолжить, необходимо поставить отметки во всех полях \"согласен\"." -continue: "Продолжить" -preservedUsernames: "Зарезервированные имена пользователей" -preservedUsernamesDescription: "Перечислите зарезервированные имена пользователей, отделяя их строками. Они станут недоступны при создании учётной записи. Это ограничение не применяется при создании учётной записи администраторами. Также, уже существующие учётные записи останутся без изменений." -createNoteFromTheFile: "Создать заметку из этого файла" -archive: "Архив" -archived: "Архивировано" -unarchive: "Разархивировать" -channelArchiveConfirmTitle: "Переместить {name} в архив?" -channelArchiveConfirmDescription: "Архивированные каналы перестанут отображаться в списке каналов или результатах поиска. В них также нельзя будет добавлять новые записи." -thisChannelArchived: "Этот канал находится в архиве." -displayOfNote: "Отображение заметок" -initialAccountSetting: "Настройка профиля" -youFollowing: "Вы подписаны" -preventAiLearning: "Отказаться от использования в машинном обучении (Генеративный ИИ)" -preventAiLearningDescription: "Запросить краулеров не использовать опубликованный текст или изображения и т.д. для машинного обучения (Прогнозирующий / Генеративный ИИ) датасетов. Это достигается путём добавления \"noai\" HTTP-заголовка в ответ на соответствующий контент. Полного предотвращения через этот заголовок не избежать, так как он может быть просто проигнорирован." +horizontal: "Сбоку" +youFollowing: "Подписки" options: "Настройки ролей" -specifyUser: "Указанный пользователь" -lookupConfirm: "Хотите узнать?" -openTagPageConfirm: "Открыть страницу этого хештега?" -specifyHost: "Указать сайт" -failedToPreviewUrl: "Предварительный просмотр недоступен" -update: "Обновить" -rolesThatCanBeUsedThisEmojiAsReaction: "Роли тех, кому можно использовать эти эмодзи как реакцию" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Если здесь ничего не указать, в качестве реакции эту эмодзи сможет использовать каждый." -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Эти роли должны быть общедоступными." -cancelReactionConfirm: "Вы действительно хотите удалить свою реакцию?" -changeReactionConfirm: "Вы действительно хотите удалить свою реакцию?" -later: "Позже" -goToMisskey: "К Misskey" -additionalEmojiDictionary: "Дополнительные словари эмодзи" -installed: "Установлено" -branding: "Бренд" -enableServerMachineStats: "Опубликовать характеристики сервера" -enableIdenticonGeneration: "Включить генерацию иконки пользователя" -turnOffToImprovePerformance: "Отключение этого параметра может повысить производительность." -createInviteCode: "Создать код приглашения" -createWithOptions: "Используйте параметры для создания" -createCount: "Количество приглашений" -inviteCodeCreated: "Создан пригласительный код" -inviteLimitExceeded: "Достигнут предел количества пригласительных кодов, которые могут быть созданы." -createLimitRemaining: "Пригласительные коды, которые могут быть созданы: {limit} " -inviteLimitResetCycle: "За определенное {time} Вы можете создать неограниченное количество пригласительных кодов {limit} " -expirationDate: "Дата истечения" -noExpirationDate: "Бессрочно" -inviteCodeUsedAt: "Дата и время, когда был использован пригласительный код" -registeredUserUsingInviteCode: "Пользователи, которые использовали пригласительный код" -unused: "Неиспользованное" -used: "Использован" -expired: "Срок действия приглашения истёк" -doYouAgree: "Согласны?" -icon: "Аватар" -replies: "Ответы" -renotes: "Репост" -loadReplies: "Показать ответы" -pinnedList: "Закреплённый список" -keepScreenOn: "Держать экран включённым" -showRenotes: "Показывать репосты" -mutualFollow: "Взаимные подписки" -followingOrFollower: "Подписки или подписчики" -fileAttachedOnly: "Только заметки с файлами" -showRepliesToOthersInTimeline: "Показывать ответы в ленте" -showRepliesToOthersInTimelineAll: "Показывать в ленте ответы пользователей, на которых вы подписаны" -hideRepliesToOthersInTimelineAll: "Скрывать в ленте ответы пользователей, на которых вы подписаны" -sourceCode: "Исходный код" -sourceCodeIsNotYetProvided: "Исходный код пока не доступен. Свяжитесь с администратором, чтобы исправить эту проблему." -repositoryUrl: "Ссылка на репозиторий" -repositoryUrlDescription: "Если вы используете Misskey как есть (без изменений в исходном коде), введите https://github.com/misskey-dev/misskey" -privacyPolicy: "Политика Конфиденциальности" -privacyPolicyUrl: "Ссылка на Политику Конфиденциальности" -attach: "Прикрепить" -angle: "Угол" -flip: "Переворот" -useGroupedNotifications: "Отображать уведомления сгруппировано" -doReaction: "Добавить реакцию" -code: "Код" -remainingN: "Остаётся: {n}" -seasonalScreenEffect: "Эффект времени года на экране" -decorate: "Украсить" -addMfmFunction: "Добавить MFM" -lastNDays: "Последние {n} сут" -hemisphere: "Место проживания" -enableHorizontalSwipe: "Смахните в сторону, чтобы сменить вкладки" -surrender: "Этот пост не может быть отменен." -useNativeUIForVideoAudioPlayer: "Использовать интерфейс браузера при проигрывании видео и звука" -keepOriginalFilename: "Сохранять исходное имя файла" -keepOriginalFilenameDescription: "Если вы выключите данную настройку, имена файлов будут автоматически заменены случайной строкой при загрузке." -alwaysConfirmFollow: "Всегда подтверждать подписку" -inquiry: "Связаться" -messageToFollower: "Сообщение подписчикам" -postForm: "Форма отправки" -information: "Описание" -_chat: - invitations: "Пригласить" - noHistory: "История пока пуста" - members: "Участники" - home: "Главная" - send: "Отправить" -_settings: - webhook: "Вебхук" -_delivery: - stop: "Заморожено" - _type: - none: "Публикация" -_announcement: - tooManyActiveAnnouncementDescription: "Большое количество оповещений может ухудшить пользовательский опыт. Рассмотрите архивирование неактуальных оповещений. " -_initialAccountSetting: - accountCreated: "Аккаунт успешно создан!" - letsStartAccountSetup: "Давайте настроим вашу учётную запись." - profileSetting: "Настройки профиля" - privacySetting: "Настройки конфиденциальности" - initialAccountSettingCompleted: "Первоначальная настройка успешно завершена!" - startTutorial: "Пройти Обучение" - skipAreYouSure: "Пропустить настройку?" -_initialTutorial: - launchTutorial: "Пройти обучение" - _note: - description: "Посты в Misskey называются 'Заметками.' Заметки отсортированы в хронологическом порядке в ленте и обновляются в режиме реального времени." - _reaction: - reactToContinue: "Добавьте реакцию, чтобы продолжить." - _postNote: - _visibility: - public: "Твоя заметка будет видна всем." - doNotSendConfidencialOnDirect2: "Администратор целевого сервера может видеть что вы отправляете. Будьте осторожны с конфиденциальной информацией, когда отправляете личные заметки пользователям с ненадёжных серверов." -_timelineDescription: - home: "В персональной ленте располагаются заметки тех, на которых вы подписаны." - local: "Местная лента показывает заметки всех пользователей этого экземпляра." - social: "В социальной ленте собирается всё, что есть в персональной и местной лентах." - global: "В глобальную ленту попадает вообще всё со связанных экземпляров." -_serverSettings: - iconUrl: "Адрес на иконку роли" -_accountMigration: - moveFrom: "Перенести другую учётную запись сюда" - moveTo: "Перенести учётную запись на другой сервер" - moveAccountDescription: "Это действие перенесёт ваш аккаунт на другой сервер.\n ・Подписчики с этого аккаунта автоматически подпишутся на новый\n ・Этот аккаунт отпишется от всех пользователей, на которых подписан сейчас\n ・Вы не сможете создавать новые заметки и т.д. на этом аккаунте\n\nТогда как перенос подписчиков происходит автоматически, вы должны будете подготовиться, сделав некоторые шаги, чтобы перенести список пользователей, на которых вы подписаны. Чтобы сделать это, экспортируйте список подписчиков в файл, который затем импортируете на новом аккаунте в меню настроек. То же самое необходимо будет сделать со списками, также как и со скрытыми и заблокированными пользователями.\n\n(Это объяснение применяется к Misskey v13.12.0 и выше. Другое ActivityPub программное обеспечение, такое, как Mastodon, может работать по-другому." - startMigration: "Перенести" - movedAndCannotBeUndone: "Аккаунт был перемещён. Это действие необратимо." _achievements: earnedAt: "Разблокировано в" _types: @@ -1537,7 +1288,6 @@ _role: canPublicNote: "Может публиковать общедоступные заметки" canInvite: "Может создавать пригласительные коды" canManageCustomEmojis: "Управлять пользовательскими эмодзи" - canManageAvatarDecorations: "Управление украшениями аватара" driveCapacity: "Доступное пространство на «диске»" alwaysMarkNsfw: "Всегда отмечать файлы как «не для всех»" pinMax: "Доступное количество закреплённых заметок" @@ -1551,7 +1301,6 @@ _role: rateLimitFactor: "Ограничение активности" descriptionOfRateLimitFactor: "Меньшее значение — слабые ограничения, большее — сильные" canHideAds: "Может скрыть рекламу" - canImportFollowing: "Можно импортировать подписчиков" _condition: isLocal: "Местный" isRemote: "Неместный" @@ -1615,7 +1364,6 @@ _plugin: install: "Установка расширений" installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете." manage: "Управление расширениями" - viewSource: "Просмотр исходника" _preferencesBackups: list: "Существующие резервные копии" saveNew: "Создать резервную копию" @@ -1649,11 +1397,6 @@ _aboutMisskey: donate: "Пожертвование на Misskey" morePatrons: "Большое спасибо и многим другим, кто принял участие в этом проекте! 🥰" patrons: "Материальная поддержка" - projectMembers: "Участники проекта" -_displayOfSensitiveMedia: - respect: "Скрывать содержимое не для всех" - ignore: "Показывать содержимое не для всех" - force: "Скрывать всё содержимое" _instanceTicker: none: "Не показывать" remote: "Только для других сайтов" @@ -1681,8 +1424,13 @@ _wordMute: muteWords: "Скрыть слово" muteWordsDescription: "Пишите слова через пробел в одной строке, чтобы фильтровать их появление вместе; а если хотите фильтровать любое из них, пишите в отдельных строках." muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)." + softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты." + hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся." + soft: "Мягко" + hard: "Жёстко" + mutedNotes: "Скрытые заметки" _instanceMute: - instanceMuteDescription: "Любые активности, затрагивающие инстансы из данного списка, будут скрыты." + instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться." instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке" title: "Скрывает заметки с заданных инстансов." heading: "Список скрытых инстансов" @@ -1727,10 +1475,11 @@ _theme: header: "Заголовок" navBg: "Фон боковой панели" navFg: "Текст на боковой панели" + navHoverFg: "Текст на боковой панели (под указателем)" navActive: "Текст на боковой панели (активирован)" navIndicator: "Индикатор на боковой панели" link: "Ссылка" - hashtag: "Хештег" + hashtag: "Хэштег" mention: "Упоминание" mentionMe: "Упоминания вас" renote: "Репост" @@ -1743,22 +1492,30 @@ _theme: infoFg: "Текст сообщения" infoWarnBg: "Фон предупреждения" infoWarnFg: "Текст предупреждения" + cwBg: "Фон предупреждения о содержимом" + cwFg: "Текст предупреждения о содержимом" + cwHoverBg: "Фон предупреждения о содержимом (под указателем)" toastBg: "Фон оповещения" toastFg: "Текст оповещения" buttonBg: "Фон кнопки" buttonHoverBg: "Текст кнопки" inputBorder: "Рамка поля ввода" + listItemHoverBg: "Фон пункта списка (под указателем)" + driveFolderBg: "Фон папки «Диска»" + wallpaperOverlay: "Слой обоев" badge: "Значок" messageBg: "Фон беседы" + accentDarken: "Фон (затемнённый)" + accentLighten: "Фон (осветлённый)" fgHighlighted: "Подсвеченный текст" _sfx: note: "Заметки" noteMy: "Собственные заметки" notification: "Уведомления" - reaction: "При выборе реакции" -_soundSettings: - driveFile: "Использовать аудиофайл с Диска." - driveFileWarn: "Выбрать аудиофайл с Диска." + chat: "Сообщения" + chatBg: "Сообщения (фон)" + antenna: "Антенна" + channel: "Канал" _ago: future: "Из будущего" justNow: "Только что" @@ -1770,30 +1527,36 @@ _ago: monthsAgo: "{n} мес. назад" yearsAgo: "{n} г. назад" invalid: "Ничего нет" -_timeIn: - seconds: "Через {n} с" - minutes: "Через {n} мин" - hours: "Через {n} ч" - days: "Через {n} сут" - weeks: "Через {n} нед." - months: "Через {n} мес." - years: "Через {n} г." _time: second: "с" minute: "мин" hour: "ч" day: "сут" +_timelineTutorial: + title: "Как пользоваться Misskey" + step1_1: "Это лицо Misskey, так называемая лента. Ваш инстанс, {name}, покажет тут все опубликованные на нём заметки в хронологическом порядке." + step1_2: "Здесь есть несколько лент. К примеру «персональная» лента отображает заметки тех, на кого вы подписаны. А «местная» — заметки тех, кого приютил {name}." + step2_1: "Что ж, теперь самое время опубликовать заметку. Если нажать вверху страницы на изображение карандаша, появится форма для текста." + step2_2: "Почему бы не написать немного о себе? Ну, или хотя бы «Привет, {name}»?" + step3_1: "Справились с первой заметкой?" + step3_2: "Отлично, теперь она должна появиться в вашей ленте." + step4_1: "А ещё здесь можно делиться своими реакциями на заметки." + step4_2: "Отмечайте реакции, нажимая на символ «+» под заметкой и выбирая значок по душе." _2fa: alreadyRegistered: "Двухфакторная аутентификация уже настроена." registerTOTP: "Начните настраивать приложение-аутентификатор" + passwordToTOTP: "Пожалуйста, введите свой пароль" step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}." step2: "Далее отсканируйте отображаемый QR-код при помощи приложения." + step2Click: "Нажав на QR-код, вы можете зарегистрироваться с помощью приложения для аутентификации или брелка для ключей, установленного на вашем устройстве." + step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):" step3Title: "Введите проверочный код" step3: "И наконец, введите код, который покажет приложение." step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом." securityKeyNotSupported: "Ваш браузер не поддерживает ключи безопасности." registerTOTPBeforeKey: "Чтобы зарегистрировать ключ безопасности и пароль, сначала настройте приложение аутентификации." securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве." + chromePasskeyNotSupported: "В настоящее время Chrome не поддерживает пароль-ключи." registerSecurityKey: "Зарегистрируйте ключ безопасности ・Passkey" securityKeyName: "Введите имя для ключа" tapSecurityKey: "Пожалуйста, следуйте инструкциям в вашем браузере, чтобы зарегистрировать свой ключ безопасности или пароль" @@ -1837,8 +1600,6 @@ _permissions: "write:gallery": "Редактирование галереи" "read:gallery-likes": "Просмотр списка понравившегося в галерее" "write:gallery-likes": "Изменение списка понравившегося в галерее" - "write:admin:reset-password": "Сбросить пароль пользователю" - "write:chat": "Писать и удалять сообщения" _auth: shareAccessTitle: "Разрешения для приложений" shareAccess: "Дать доступ для «{name}» к вашей учётной записи?" @@ -1865,7 +1626,7 @@ _weekday: _widgets: profile: "Профиль" instanceInfo: "Информация об инстансе" - memo: "Памятки" + memo: "Напоминания" notifications: "Уведомления" timeline: "Лента" calendar: "Календарь" @@ -1892,10 +1653,9 @@ _widgets: _userList: chooseList: "Выберите список" clicker: "Счётчик щелчков" - birthdayFollowings: "Пользователи, у которых сегодня день рождения" _cw: hide: "Спрятать" - show: "Показать" + show: "Показать еще" chars: "знаков: {count}" files: "файлов: {count}" _poll: @@ -1946,7 +1706,7 @@ _profile: name: "Имя" username: "Имя пользователя" description: "О себе" - youCanIncludeHashtags: "Можете использовать здесь хештеги." + youCanIncludeHashtags: "Можете использовать здесь хэштеги" metadata: "Дополнительные сведения" metadataEdit: "Редактировать дополнительные сведения" metadataDescription: "Можно добавить до четырёх дополнительных граф в профиль." @@ -1954,12 +1714,9 @@ _profile: metadataContent: "Содержимое" changeAvatar: "Поменять аватар" changeBanner: "Поменять изображение в шапке" - verifiedLinkDescription: "Указывая здесь URL, содержащий ссылку на профиль, иконка владения ресурсом может быть отображена рядом с полем" - avatarDecorationMax: "Вы можете добавить до {max} украшений." _exportOrImport: allNotes: "Все заметки\n" favoritedNotes: "Избранное" - clips: "Подборка" followingList: "Подписки" muteList: "Скрытые" blockingList: "Заблокированные" @@ -2016,6 +1773,9 @@ _pages: newPage: "Создать страницу" editPage: "Править страницу" readPage: "Читать страницу" + created: "Страница успешно создана." + updated: "Страница успешно обновлена." + deleted: "Страница успешно удалена." pageSetting: "Настройки страницы" nameAlreadyExists: "Указанный адрес страницы уже существует." invalidNameTitle: "Указанный адрес страницы недопустим." @@ -2075,9 +1835,6 @@ _notification: unreadAntennaNote: "Антенна {name}" emptyPushNotificationMessage: "Обновлены push-уведомления" achievementEarned: "Получено достижение" - checkNotificationBehavior: "Проверить внешний вид уведомления" - sendTestNotification: "Отправить тестовое уведомление" - flushNotification: "Очистить уведомления" _types: all: "Все" follow: "Подписки" @@ -2090,11 +1847,10 @@ _notification: receiveFollowRequest: "Получен запрос на подписку" followRequestAccepted: "Запрос на подписку одобрен" achievementEarned: "Получение достижений" - login: "Войти" app: "Уведомления из приложений" _actions: followBack: "отвечает взаимной подпиской" - reply: "Ответ" + reply: "Ответить" renote: "Репост" _deck: alwaysShowMainColumn: "Всегда показывать главную колонку" @@ -2123,71 +1879,12 @@ _deck: channel: "Каналы" mentions: "Упоминания" direct: "Личное" - roleTimeline: "История Ролей" _dialog: charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}" charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}" _disabledTimeline: title: "Лента отключена" description: "Ваша текущая роль не позволяет пользоваться этой лентой." -_drivecleaner: - orderBySizeDesc: "Размеры файлов по убыванию" - orderByCreatedAtAsc: "По увеличению даты" _webhookSettings: - createWebhook: "Создать вебхук" - modifyWebhook: "Изменить Вебхук" name: "Название" - secret: "Секрет" - trigger: "Условие срабатывания" active: "Вкл." - _events: - follow: "Когда подписались на пользователя" - followed: "Когда на вас подписались" - note: "Когда создали заметку" - reply: "Когда получили ответ на заметку" - renote: "Когда вас репостнули" - reaction: "Когда получили реакцию" - mention: "Когда вас упоминают" - _systemEvents: - abuseReport: "Когда приходит жалоба" - abuseReportResolved: "Когда разрешается жалоба" - userCreated: "Когда создан пользователь" - deleteConfirm: "Вы уверены, что хотите удалить этот Вебхук?" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Электронная почта" - webhook: "Вебхук" - _captions: - webhook: "Отправить уведомление Системному Вебхуку при получении или разрешении жалоб." - notifiedWebhook: "Используемый Вебхук" -_moderationLogTypes: - suspend: "Заморозить" - addCustomEmoji: "Добавлено эмодзи" - updateCustomEmoji: "Изменено эмодзи" - deleteCustomEmoji: "Удалено эмодзи" - deleteDriveFile: "Файл удалён" - resetPassword: "Сброс пароля:" - createInvitation: "Создать код приглашения" - createSystemWebhook: "Создать Системный Вебхук" - updateSystemWebhook: "Обновить Системый Вебхук" - deleteSystemWebhook: "Удалить Системный Вебхук" -_fileViewer: - url: "Ссылка" - attachedNotes: "Закреплённые заметки" -_dataSaver: - _code: - title: "Подсветка кода" -_hemisphere: - N: "Северное полушарие" - S: "Южное полушарие" - caption: "Используется для некоторых настроек клиента для определения сезона." -_reversi: - total: "Всего" -_remoteLookupErrors: - _noSuchObject: - title: "Не найдено" -_search: - searchScopeAll: "Все" - searchScopeLocal: "Местная" - searchScopeUser: "Указанный пользователь" diff --git a/locales/si-LK.yml b/locales/si-LK.yml index 841fb10585..ed97d539c0 100644 --- a/locales/si-LK.yml +++ b/locales/si-LK.yml @@ -1,39 +1 @@ --- -_lang_: "සිංහල" -monthAndDay: "{month}-{day}" -search: "සොයන්න" -reset: "යළි සකසන්න" -notifications: "දැනුම්දීම්" -username: "පරිශීලක නාමය" -password: "මුරපදය" -ok: "හරි" -gotIt: "තේරුණා" -cancel: "අවලංගු කරන්න" -noThankYou: "එපා, ස්තුතියි" -noNotifications: "දැනුම්දීම් නැත" -instance: "සර්වර්" -settings: "සැකසුම්" -login: "පිවිසෙන්න" -users: "පරිශීලක" -note: "නෝට්" -notes: "නෝට්" -instances: "සර්වර්" -smtpUser: "පරිශීලක නාමය" -smtpPass: "මුරපදය" -user: "පරිශීලක" -searchByGoogle: "සොයන්න" -_sfx: - note: "නෝට්" - notification: "දැනුම්දීම්" -_2fa: - renewTOTPCancel: "එපා, ස්තුතියි" -_widgets: - notifications: "දැනුම්දීම්" -_profile: - username: "පරිශීලක නාමය" -_notification: - _types: - login: "පිවිසෙන්න" -_deck: - _columns: - notifications: "දැනුම්දීම්" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 577689698f..f4c598ac83 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -113,6 +113,7 @@ sensitive: "NSFW" add: "Pridať" reaction: "Reakcie" reactions: "Reakcie" +reactionSetting: "Reakcie zobrazené vo výbere reakcií" reactionSettingDescription2: "Ťahaním preusporiadate, kliknutím odstránite, Stlačením \"+\" pridáte" rememberNoteVisibility: "Zapamätať nastavenia viditeľnosti poznámky" attachCancel: "Odstrániť prílohu" @@ -204,6 +205,7 @@ noUsers: "Žiadni používatelia" editProfile: "Upraviť profil" noteDeleteConfirm: "Naozaj chcete odstrániť túto poznámku?" pinLimitExceeded: "Ďalšie poznámky už nemôžete pripnúť." +intro: "Inštalácia Misskey je dokončená! Prosím vytvorte administrátora." done: "Hotovo" processing: "Pracujem..." preview: "Náhľad" @@ -241,6 +243,7 @@ removeAreYouSure: "Naozaj chcete odstrániť \"{x}\"?" deleteAreYouSure: "Naozaj chcete odstrániť \"{x}\"?" resetAreYouSure: "Naozaj resetovať?" saved: "Uložené" +messaging: "Chat" upload: "Nahrať súbor" keepOriginalUploading: "Zachovať pôvodný obrázok" keepOriginalUploadingDescription: "Uloží pôvodný obrázok ako je. Ak je vypnuté, verzia pre web sa vygeneruje pri nahratí." @@ -253,6 +256,7 @@ uploadFromUrlMayTakeTime: "Nahrávanie môže nejaký čas trvať." explore: "Objavovať" messageRead: "Prečítané" noMoreHistory: "To je všetko" +startMessaging: "Začať chat" nUsersRead: "prečítané {n} používateľmi" agreeTo: "Súhlasím s {0}" agreeBelow: "Súhlasím s nasledovným" @@ -328,10 +332,12 @@ enableLocalTimeline: "Povoliť lokálnu časovú os" enableGlobalTimeline: "Povoliť globálnu časovú os" disablingTimelinesInfo: "Administrátori a moderátori majú vždy prístup ku všetkým časovým osiam, aj keď sú vypnuté." registration: "Registrácia" +enableRegistration: "Povoliť registráciu nových používateľov" invite: "Pozvať" driveCapacityPerLocalAccount: "Kapacita disku pre používateľa" driveCapacityPerRemoteAccount: "Kapacita disku pre vzdialeného používateľa" inMb: "V megabajtoch" +iconUrl: "Favicon URL" bannerUrl: "URL obrázku bannera" backgroundImageUrl: "URL obrázku pozadia" basicInfo: "Základné informácie" @@ -345,8 +351,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Zapnúť hCaptchu" hcaptchaSiteKey: "Site key" hcaptchaSecretKey: "Secret key" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret key" recaptcha: "reCAPTCHA" enableRecaptcha: "Zapnúť ReCAPTCHA" recaptchaSiteKey: "Site key" @@ -408,6 +412,7 @@ share: "Zdieľať" notFound: "Nenájdené" notFoundDescription: "Nenašla sa žiadna stránka na zadanej URL." uploadFolder: "Predvolený priečinok pre nahrávanie" +cacheClear: "Vyčistiť cache" markAsReadAllNotifications: "Označiť všetky oznámenia ako prečítané" markAsReadAllUnreadNotes: "Označiť všetky poznámky ako prečítané" markAsReadAllTalkMessages: "Označiť všetky správy ako prečítané" @@ -425,6 +430,8 @@ retype: "Zadajte znovu" noteOf: "Poznámky používateľa {user}" quoteAttached: "Citované" quoteQuestion: "Pripojiť ako citát?" +noMessagesYet: "Zatiaľ žiadne správy" +newMessageExists: "Máte novú správu" onlyOneFileCanBeAttached: "Ku správe môžete priložiť len jeden súbor" signinRequired: "Prihláste sa, prosím!" invitations: "Pozvať" @@ -448,6 +455,7 @@ uiLanguage: "Jazyk používateľského prostredia" aboutX: "O {x}" emojiStyle: "Štýl emoji" native: "Natívne" +disableDrawer: "Nepoužívať šuflíkové menu" showNoteActionsOnlyHover: "Ovládacie prvky poznámky sa zobrazujú len po nabehnutí myši" noHistory: "Žiadna história" signinHistory: "História prihlásení" @@ -625,7 +633,10 @@ abuseReported: "Vaše nahlásenie je odoslané. Veľmi pekne ďakujeme." reporter: "Nahlásil" reporteeOrigin: "Pôvod nahláseného" reporterOrigin: "Pôvod nahlasovača" +forwardReport: "Preposlať nahlásenie na server" +forwardReportIsAnonymous: "Namiesto vášho účtu bude zobrazený anonymný systémový účet na vzdialenom serveri ako autor nahlásenia." send: "Poslať" +abuseMarkAsResolved: "Označiť nahlásenia ako vyriešené" openInNewTab: "Otvoriť v novom tabe" openInSideView: "Otvoriť v bočnom paneli" defaultNavigationBehaviour: "Predvolené správanie navigácie" @@ -643,7 +654,6 @@ createNewClip: "Vytvoriť nový klip" unclip: "Odopnúť" confirmToUnclipAlreadyClippedNote: "Táto poznámka je už pripnutá ako \"{name}\". Naozaj ju chcete odopnúť?" public: "Verejné" -private: "Súkromné" i18nInfo: "Misskey je prekladaný do rôznych jazykov dobrovoľníkmi. Pomôcť môžete na {link}." manageAccessTokens: "Spravovať prístupové tokeny" accountInfo: "Informácie o účte" @@ -681,6 +691,7 @@ experimentalFeatures: "Experimentálne funkcie" developer: "Vývojár" makeExplorable: "Spraviť účet viditeľný v \"Objavovať\"" makeExplorableDescription: "Ak toto vypnete, váš účet sa nezobrazí v sekcii \"Objavovat\"." +showGapBetweenNotesInTimeline: "Zobraziť medzeru medzi príspevkami časovej osi." duplicate: "Duplikovať" left: "Naľavo" center: "Stred" @@ -812,6 +823,8 @@ makeReactionsPublicDescription: "Toto spraví všetky vaše minulé reakcie vidi classic: "Klasika" muteThread: "Ztíšiť vlákno" unmuteThread: "Zrušiť stíšenie vlákna" +ffVisibility: "Viditeľnosť sledujúcich/sledovaných" +ffVisibilityDescription: "Umožňuje nastaviť kto vidí koho sledujete a kto vás sleduje." continueThread: "Zobraziť pokračovanie vlákna" deleteAccountConfirm: "Toto nezvrátiteľne vymaže váš účet. Pokračovať?" incorrectPassword: "Nesprávne heslo." @@ -905,24 +918,6 @@ pleaseDonate: "Misskey je bezplatný softvér, ktorý používa {host}. Prosím, color: "Farba" horizontal: "Strana" youFollowing: "Sledované" -icon: "Avatar" -replies: "Odpovedať" -renotes: "Preposlať" -sourceCode: "Zdrojový kód" -flip: "Preklopiť" -lastNDays: "Posledných {n} dní" -postForm: "Napísať poznámku" -information: "Informácie" -_chat: - invitations: "Pozvať" - noHistory: "Žiadna história" - members: "Členovia" - home: "Domov" - send: "Poslať" -_delivery: - stop: "Zmrazené" - _type: - none: "Zverejňovanie" _role: priority: "Priorita" _priority: @@ -980,7 +975,6 @@ _plugin: install: "Inštalova pluginy" installWarn: "Prosím neinštalujte nedôveryhodné pluginy." manage: "Spravovanie pluginov" - viewSource: "Ukázať zdroj" _preferencesBackups: list: "Vytvorené zálohy" saveNew: "Uložiť novú" @@ -1041,6 +1035,11 @@ _wordMute: muteWords: "Umlčané slová" muteWordsDescription: "Medzerami oddeľte pre podmienku AND a novými riadkami pre podmienku OR." muteWordsDescription2: "Regulárne výrazy sa použijú keď použijete okolo lomítka." + softDescription: "Skryje poznámky z časovej osi, ktoré spĺňajú podmienky." + hardDescription: "Zabráni poznámky spĺňajúce množinu podmienok, aby boli pridané do časovej osi. Navyše tieto poznámky nepribudnú v časovej osi ani keď sa podmienky zmenia." + soft: "Mäkké" + hard: "Tvrdé" + mutedNotes: "Umlčané poznámky" _instanceMute: instanceMuteDescription: "Toto umlčí všetky poznámky/preposlania zo zoznamu serverov, vrátane tých, na ktoré používatelia odpovedajú z umlčaného servera." instanceMuteDescription2: "Oddeľte novými riadkami" @@ -1087,6 +1086,7 @@ _theme: header: "Hlavička" navBg: "Pozadie bočného panela" navFg: "Text bočného panela" + navHoverFg: "Text bočného panela (pod kurzorom)" navActive: "Text bočného panela (aktívny)" navIndicator: "Indikátor bočného panela" link: "Odkaz" @@ -1103,18 +1103,30 @@ _theme: infoFg: "Informačný text" infoWarnBg: "Pozadie varovania" infoWarnFg: "Text varovania" + cwBg: "CW pozadie tlačidla" + cwFg: "CW text tlačidla" + cwHoverBg: "CW pozadie tlačidla (pod kurzorom)" toastBg: "Pozadie upozornenia" toastFg: "Text upozornenia" buttonBg: "Pozadie tlačidla" buttonHoverBg: "Pozadie tlačidla (pod kurzorom)" inputBorder: "Okraj vstupného poľa" + listItemHoverBg: "Pozadie položky zoznamu (pod kurzorom)" + driveFolderBg: "Pozadie priečinu disku" + wallpaperOverlay: "Vrstvenie pozadia" badge: "Odznak" messageBg: "Pozadie chatu" + accentDarken: "Akcent (stmavené)" + accentLighten: "Akcent (zosvetlené)" fgHighlighted: "Zvýraznený text" _sfx: note: "Poznámky" noteMy: "Vlastná poznámka" notification: "Oznámenia" + chat: "Chat" + chatBg: "Chat (pozadie)" + antenna: "Antény" + channel: "Upozornenia kanála" _ago: future: "Budúcnosť" justNow: "Teraz" @@ -1135,6 +1147,7 @@ _2fa: alreadyRegistered: "Už ste zaregistrovali 2-faktorové autentifikačné zariadenie." step1: "Najprv si nainštalujte autentifikačnú aplikáciu (napríklad {a} alebo {b}) na svoje zariadenie." step2: "Potom, naskenujte QR kód zobrazený na obrazovke." + step2Url: "Do aplikácie zadajte nasledujúcu URL adresu:" step3: "Nastavenie dokončíte zadaním tokenu z vašej aplikácie." step4: "Od teraz, všetky ďalšie prihlásenia budú vyžadovať prihlasovací token." securityKeyInfo: "Okrem odtlačku prsta alebo PIN autentifikácie si môžete nastaviť autentifikáciu cez hardvérový bezpečnostný kľúč podporujúci FIDO2 a tak ešte viac zabezpečiť svoj účet." @@ -1173,7 +1186,6 @@ _permissions: "write:gallery": "Upravovať vašu galériu" "read:gallery-likes": "Vidieť zoznam obľúbených príspevkov z galérie" "write:gallery-likes": "Upraviť zoznam obľúbených príspevov z galérie" - "write:chat": "Písať alebo odstraňovať správy v chate" _auth: shareAccess: "Prajete si povoliť \"{name}\", aby mal prístup k tomuto účtu?" shareAccessAsk: "Naozaj chcete povoliť tejto aplikácii prístup k tomuto účtu?" @@ -1282,7 +1294,6 @@ _profile: changeBanner: "Zmeniť banner" _exportOrImport: allNotes: "Všetky poznámky" - clips: "Klip" followingList: "Sledujete" muteList: "Vypnúť zvuk" blockingList: "Zablokovať" @@ -1330,6 +1341,9 @@ _pages: newPage: "Vytvoriť novú stránku" editPage: "Upraviť túto stránku" readPage: "Zobrazenie zdroja aktívne" + created: "Stránka úspešne vytvorená" + updated: "Stránka úspešne upravená" + deleted: "Stránka úspešne odstránená" pageSetting: "Nastavenia stránky" nameAlreadyExists: "Zadaná URL stránku už existuje" invalidNameTitle: "Zadaná URL stránku je nesprávna" @@ -1399,7 +1413,6 @@ _notification: pollEnded: "Hlasovanie skončilo" receiveFollowRequest: "Doručené žiadosti o sledovanie" followRequestAccepted: "Schválené žiadosti o sledovanie" - login: "Prihlásiť sa" app: "Oznámenia z prepojených aplikácií" _actions: followBack: "Sledovať späť\n" @@ -1435,18 +1448,3 @@ _deck: _webhookSettings: name: "Názov" active: "Zapnuté" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" -_moderationLogTypes: - suspend: "Zmraziť" - resetPassword: "Resetovať heslo" -_reversi: - total: "Celkom" -_remoteLookupErrors: - _noSuchObject: - title: "Nenájdené" -_search: - searchScopeAll: "Všetko" - searchScopeLocal: "Lokálne" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index ba6d8a93d2..5526708f04 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -20,7 +20,6 @@ noNotes: "Inga noteringar" noNotifications: "Inga notifikationer" instance: "Instanser" settings: "Inställningar" -notificationSettings: "Notifieringsinställningar" basicSettings: "Basinställningar" otherSettings: "Andra inställningar" openInWindow: "Öppna i ett fönster" @@ -54,8 +53,6 @@ copyRSS: "Kopiera RSS" copyUsername: "Kopiera användarnamn" copyUserId: "Kopiera användar-ID" copyNoteId: "Kopiera noter-ID" -copyFileId: "Kopiera Fil-ID" -copyFolderId: "Kopiera mapp-ID" searchUser: "Sök användare" reply: "Svara" loadMore: "Ladda mer" @@ -109,7 +106,6 @@ cantRenote: "Inlägget kunde inte bli omnoterat." cantReRenote: "En omnotering kan inte bli omnoterad." quote: "Citat" inChannelRenote: "Omnotera inom kanalen" -inChannelQuote: "I kanal citat" pinnedNote: "Fästad not" pinned: "Fäst till profil" you: "Du" @@ -118,6 +114,7 @@ sensitive: "Känsligt innehåll" add: "Lägg till" reaction: "Reaktioner" reactions: "Reaktioner" +reactionSetting: "Reaktioner som ska visas i reaktionsväljaren" reactionSettingDescription2: "Dra för att omordna, klicka för att radera, tryck \"+\" för att lägga till." rememberNoteVisibility: "Komihåg notvisningsinställningar" attachCancel: "Ta bort bilaga" @@ -211,6 +208,7 @@ noUsers: "Det finns inga användare" editProfile: "Redigera profil" noteDeleteConfirm: "Är du säker på att du vill ta bort denna not?" pinLimitExceeded: "Du kan inte fästa fler noter" +intro: "Misskey har installerats! Vänligen skapa en adminanvändare." done: "Klar" processing: "Bearbetar..." preview: "Förhandsvisning" @@ -248,6 +246,7 @@ removeAreYouSure: "Är du säker att du vill radera \"{x}\"?" deleteAreYouSure: "Är du säker att du vill radera \"{x}\"?" resetAreYouSure: "Vill du återställa?" saved: "Sparad" +messaging: "Chatt" upload: "Ladda upp" keepOriginalUploading: "Behåll originalbild" keepOriginalUploadingDescription: "Sparar den originellt uppladdade bilden i sitt i befintliga skick. Om avstängd, kommer en webbversion bli genererad vid uppladdning." @@ -260,6 +259,7 @@ uploadFromUrlMayTakeTime: "Det kan ta tid tills att uppladdningen blir klar." explore: "Utforska" messageRead: "Läs" noMoreHistory: "Det finns ingen mer historik" +startMessaging: "Starta en chatt" nUsersRead: "läst av {n}" agreeTo: "Jag accepterar {0}" agree: "Överens" @@ -309,7 +309,6 @@ banner: "Banner" reload: "Ladda om" doNothing: "Ignorera" reloadConfirm: "Vill du ladda om tidslinjen?" -watch: "Titta" accept: "Tillåt" reject: "Neka" normal: "Normal" @@ -330,27 +329,18 @@ disconnectService: "Koppla från" enableLocalTimeline: "Aktivera lokal tidslinje" enableGlobalTimeline: "Aktivera global tidslinje" registration: "Registrera" +enableRegistration: "Aktivera registrering av nya användare" invite: "Inbjudan" inMb: "I megabyte" +iconUrl: "URL till profilbilden" bannerUrl: "URL till banner-bilden" -basicInfo: "Grundläggande info" -pinnedUsers: "Fästa användare" -pinnedPages: "Fästa sidor" pinnedNotes: "Fästad not" hcaptcha: "hCaptcha" enableHcaptcha: "Aktivera hCaptcha" hcaptchaSiteKey: "Webbplatsnyckel" -hcaptchaSecretKey: "Hemlig nyckel" -mcaptchaSiteKey: "Webbplatsnyckel" -mcaptchaSecretKey: "Hemlig nyckel" recaptcha: "reCAPTCHA" enableRecaptcha: "Aktivera reCAPTCHA" -recaptchaSiteKey: "Webbplatsnyckel" -recaptchaSecretKey: "Hemlig nyckel" -turnstile: "Turnstile" enableTurnstile: "Aktivera Turnstile" -turnstileSiteKey: "Webbplatsnyckel" -turnstileSecretKey: "Hemlig nyckel" antennas: "Antenner" manageAntennas: "Hantera Antenner" name: "Namn" @@ -362,7 +352,6 @@ notifyAntenna: "Notifiera om nya noter" withFileAntenna: "Endast noter med filer" enableServiceworker: "Aktivera pushnotiser i denna webbläsaren" antennaUsersDescription: "Ange ett användarnamn per linje" -withReplies: "Med svar" notesAndReplies: "Inlägg och svar" silence: "Tystnad" recentlyUpdatedUsers: "Nyligen aktiva användare" @@ -373,31 +362,22 @@ userList: "Listor" about: "Om" aboutMisskey: "Om Misskey" administrator: "Administratör" -2fa: "Tvåfaktorsautentisering" -totp: "Autentiseringsapp" -moderator: "Moderator" passwordLessLogin: "Lösenordsfri inloggning" passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey." resetPassword: "Återställ Lösenord" newPasswordIs: "Det nya lösenordet är \"{password}\"" share: "Dela" -markAsReadAllTalkMessages: "Markera alla meddelanden som lästa" help: "Hjälp" close: "Stäng" invites: "Inbjudan" members: "Medlemmar" -transfer: "Överför" text: "Text" enable: "Aktivera" next: "Nästa" -retype: "Ange igen" invitations: "Inbjudan" -invitationCode: "Inbjudningskod" -available: "Tillgängligt" weakPassword: "Svagt Lösenord" normalPassword: "Medel Lösenord" strongPassword: "Starkt Lösenord" -signinWith: "Logga in med {x}" signinFailed: "Kan inte logga in. Det angivna användarnamnet eller lösenordet är felaktigt." or: "eller" language: "Språk" @@ -409,124 +389,70 @@ existingAccount: "Existerande konto" regenerate: "Regenerera" fontSize: "Textstorlek" openImageInNewTab: "Öppna bild i ny flik" -appearance: "Utseende" clientSettings: "Klientinställningar" accountSettings: "Kontoinställningar" numberOfDays: "Antal dagar" -objectStorageUseSSL: "Använd SSL" -serverLogs: "Serverloggar" deleteAll: "Radera alla" sounds: "Ljud" sound: "Ljud" listen: "Lyssna" none: "Ingen" volume: "Volym" -notUseSound: "Inaktivera ljud" chooseEmoji: "Välj en emoji" recentUsed: "Senast använd" install: "Installera" uninstall: "Avinstallera" -deleteAllFiles: "Radera alla filer" -deleteAllFilesConfirm: "Är du säker på att du vill radera alla filer?" menu: "Meny" -addItem: "Lägg till objekt" serviceworkerInfo: "Måste vara aktiverad för pushnotiser." enableInfiniteScroll: "Ladda mer automatiskt" enablePlayer: "Öppna videospelare" -description: "Beskrivning" permission: "Behörigheter" enableAll: "Aktivera alla" -disableAll: "Inaktivera alla" edit: "Ändra" enableEmail: "Aktivera epost-utskick" email: "E-post" -emailAddress: "E-postadress" smtpHost: "Värd" smtpUser: "Användarnamn" smtpPass: "Lösenord" emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering" -makeActive: "Aktivera" -copy: "Kopiera" -overview: "Översikt" logs: "Logg" -database: "Databas" channel: "kanal" create: "Skapa" other: "Mer" -abuseReports: "Rapporter" -reportAbuse: "Rapporter" -reportAbuseOf: "Rapportera {name}" -abuseReported: "Din rapport har skickats. Tack så mycket." send: "Skicka" openInNewTab: "Öppna i ny flik" createNew: "Skapa ny" -private: "Privat" i18nInfo: "Misskey översätts till många olika språk av volontärer. Du kan hjälpa till med översättningen på {link}." accountInfo: "Kontoinformation" -followersCount: "Antal följare" -yes: "Ja" -no: "Nej" clips: "Klipp" duplicate: "Duplicera" reloadToApplySetting: "Inställningen tillämpas efter sidan laddas om. Vill du göra det nu?" clearCache: "Rensa cache" onlineUsersCount: "{n} användare är online" -nUsers: "{n} användare" nNotes: "{n} Noter" backgroundColor: "Bakgrundsbild" textColor: "Text" -saveAs: "Spara som..." -saveConfirm: "Spara ändringar?" youAreRunningUpToDateClient: "Klienten du använder är uppdaterat." newVersionOfClientAvailable: "Ny version av klienten är tillgänglig." -editCode: "Redigera kod" publish: "Publicera" typingUsers: "{users} skriver" -goBack: "Tillbaka" -addDescription: "Lägg till beskrivning" info: "Om" -online: "Online" -active: "Aktiv" -offline: "Offline" enabled: "Aktiverad" -quickAction: "Snabbåtgärder" user: "Användare" -gallery: "Galleri" -popularPosts: "Populära inlägg" customCssWarn: "Den här inställningen borde bara ändrats av en som har rätta kunskaper. Om du ställer in det här fel så kan klienten sluta fungera rätt." global: "Global" squareAvatars: "Visa fyrkantiga profilbilder" sent: "Skicka" -searchResult: "Sökresultat" -learnMore: "Läs mer" misskeyUpdated: "Misskey har uppdaterats!" -translate: "Översätt" -controlPanel: "Kontrollpanel" -manageAccounts: "Hantera konton" incorrectPassword: "Fel lösenord." -hide: "Dölj" welcomeBackWithName: "Välkommen tillbaka, {name}" clickToFinishEmailVerification: "Tryck på [{ok}] för att slutföra bekräftelsen på e-postadressen." -size: "Storlek" searchByGoogle: "Sök" -indefinitely: "Aldrig" -tenMinutes: "10 minuter" -oneHour: "En timme" -oneDay: "En dag" -oneWeek: "En vecka" -oneMonth: "En månad" -threeMonths: "3 månader" -oneYear: "1 år" -threeDays: "3 dagar" file: "Filer" -deleteAccount: "Radera konto" -label: "Etikett" cannotUploadBecauseNoFreeSpace: "Kan inte ladda upp filen för att det finns inget lagringsutrymme kvar." cannotUploadBecauseExceedsFileSizeLimit: "Kan inte ladda upp filen för att den är större än filstorleksgränsen." -beta: "Beta" enableAutoSensitive: "Automatisk NSFW markering" enableAutoSensitiveDescription: "Tillåter automatiskt detektering och marketing av NSFW media genom Maskininlärning när möjligt. Även om denna inställningen är avaktiverad, kan det vara aktiverat på hela instansen." -move: "Flytta" pushNotification: "Pushnotiser" subscribePushNotification: "Aktivera pushnotiser" unsubscribePushNotification: "Avaktivera pushnotiser" @@ -535,92 +461,33 @@ pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för windowMaximize: "Maximera" windowMinimize: "Minimera" windowRestore: "Återställ" -tools: "Verktyg" -like: "Gilla" pleaseDonate: "Misskey är en gratis programvara som används på {host}. Donera gärna för att göra utvecklingen ständigt, tack!" -roles: "Roll" -role: "Roll" -color: "Färg" resetPasswordConfirm: "Återställ verkligen ditt lösenord?" -dataSaver: "Databesparing" -icon: "Profilbild" -forYou: "För dig" -replies: "Svara" -renotes: "Omnotera" -loadReplies: "Visa svar" -loadConversation: "Visa konversation" -authentication: "Autentisering" -sourceCode: "Källkod" -doReaction: "Lägg till reaktion" -code: "Kod" -gameRetry: "Försök igen" -inquiry: "Kontakt" -tryAgain: "Försök igen senare" -signinWithPasskey: "Logga in med nyckel" -unknownWebAuthnKey: "Okänd nyckel" -information: "Om" -_chat: - invitations: "Inbjudan" - members: "Medlemmar" - home: "Hem" - send: "Skicka" -_delivery: - stop: "Suspenderad" - _type: - none: "Publiceras" -_initialAccountSetting: - profileSetting: "Profilinställningar" -_initialTutorial: - _reaction: - title: "Vad är reaktioner?" _achievements: _types: _open3windows: title: "Flera Fönster" description: "Ha minst 3 fönster öppna samtidigt" -_role: - edit: "Redigera roll" _ffVisibility: public: "Publicera" - private: "Privat" -_accountDelete: - accountDelete: "Radera konto" -_ad: - back: "Tillbaka" -_gallery: - like: "Gilla" _email: _follow: title: "följde dig" -_aboutMisskey: - source: "Källkod" - projectMembers: "Projektmedlemmar" _channel: setBanner: "Välj banner" removeBanner: "Ta bort banner" - nameAndDescription: "Namn och beskrivning" -_menuDisplay: - hide: "Dölj" _theme: - description: "Beskrivning" - color: "Färg" keys: mention: "Nämn" renote: "Omnotera" _sfx: note: "Noter" notification: "Notifikationer" -_ago: - justNow: "Just nu" + chat: "Chatt" + antenna: "Antenner" _2fa: - step3Title: "Ange en autentiseringskod" + passwordToTOTP: "Skriv in ditt lösenord" renewTOTPCancel: "Nej tack" -_permissions: - "read:reactions": "Visa dina reaktioner" - "write:reactions": "Redigera dina reaktioner" - "write:admin:delete-account": "Radera användarkonto" - "write:admin:roles": "Hantera roller" - "read:admin:roles": "Visa roller" _antennaSources: all: "Alla noter" homeTimeline: "Noter från följda användare" @@ -637,19 +504,13 @@ _widgets: _userList: chooseList: "Välj lista" _cw: - hide: "Dölj" show: "Ladda mer" - chars: "{count} tecken" - files: "{count} fil(er)" -_poll: - infinite: "Aldrig" _visibility: home: "Hem" followers: "Följare" _profile: name: "Namn" username: "Användarnamn" - metadataLabel: "Etikett" changeAvatar: "Ändra profilbild" changeBanner: "Ändra banner" _exportOrImport: @@ -660,12 +521,9 @@ _exportOrImport: userLists: "Listor" _charts: federation: "Federation" - activeUsers: "Aktiva användare" _timelines: home: "Hem" global: "Global" -_play: - summary: "Beskrivning" _pages: blocks: image: "Bilder" @@ -678,13 +536,10 @@ _notification: renote: "Omnotera" quote: "Citat" reaction: "Reaktioner" - login: "Logga in" _actions: reply: "Svara" renote: "Omnotera" _deck: - addColumn: "Lägg till kolumn" - deleteProfile: "Radera profil" _columns: notifications: "Notifikationer" tl: "Tidslinje" @@ -695,19 +550,3 @@ _deck: _webhookSettings: name: "Namn" active: "Aktiverad" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "E-post" -_moderationLogTypes: - suspend: "Suspendera" - resetPassword: "Återställ Lösenord" -_reversi: - blackOrWhite: "Svart/Vit" - rules: "Regler" - black: "Svart" - white: "Vit" -_selfXssPrevention: - warning: "VARNING" -_search: - searchScopeAll: "Allt" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index b4a28aed5b..387f4b9d6a 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1,28 +1,24 @@ --- _lang_: "ภาษาไทย" headlineMisskey: "เชื่อมต่อเครือข่ายโดยโน้ต" -introMisskey: "ยินดีต้อนรับทุกคนจ้า! Misskey คือ ซอฟต์แวร์โอเพนซอร์สสำหรับบริการไมโครบล็อกกิ้ง (MicroBlogging) แบบกระจายศูนย์อำนาจ (Decentralized) \n\nเขียน “โน้ต (Note)” เพื่อส่งต่อเรื่องราวของคุณให้ทั้งโลกได้รับรู้📡\nและอย่าลืมที่จะ “รีแอคชั่น” กับเรื่องราวของคนอื่น ๆ ด้วยนะ! 👍\n\nท่องสำรวจโลกใบใหม่กันเถอะ🚀" -poweredByMisskeyDescription: "{name} เป็นหนึ่งในเซิร์ฟเวอร์ของแพลตฟอร์มโอเพ่นซอร์ส Misskey" -monthAndDay: "{month}/{day}" +introMisskey: "ยินดีต้อนรับจ้าาา! Misskey เป็นบริการไมโครบล็อกโอเพ่นซอร์ส แบบการกระจายอำนาจ\nสร้าง \"โน้ต\" เพื่อแบ่งปันความคิดของคุณกับทุกคนรอบตัวคุณกันเถอะ 📡\nด้วยการ \"รีแอคชั่นผู้คน\" คุณยังสามารถแสดงความรู้สึกของคุณเกี่ยวกับบันทึกของทุกคนได้อย่างรวดเร็ว 👍\n\nแล้วมาท่องสำรวจโลกใบใหม่กันเถอะ! 🚀" +poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริการที่ถูกขับเคลื่อนโดยแพลตฟอร์มโอเพ่นซอร์ส Misskey (เรียกว่า \"อินสแตนซ์ Misskey\")" +monthAndDay: "{เดือน}/{วัน}" search: "ค้นหา" -reset: "รีเซ็ต" -notifications: "เเจ้งเตือน" +notifications: "การเเจ้งเตือน" username: "ชื่อผู้ใช้" password: "รหัสผ่าน" -initialPasswordForSetup: "รหัสผ่านเริ่มต้นสำหรับการตั้งค่า" -initialPasswordIsIncorrect: "รหัสผ่านเริ่มต้นสำหรับตั้งค่านั้นไม่ถูกต้องค่ะ" -initialPasswordForSetupDescription: "ถ้าหากคุณติดตั้ง Misskey เอง ให้ใช้รหัสผ่านที่คุณป้อนในไฟล์กำหนดค่า \nถ้าหากคุณกำลังใช้บริการโฮสต์ Misskey ให้ใช้รหัสผ่านที่ได้รับมา\nถ้ายังไม่มีรหัสผ่าน ให้ข้ามช่องรหัสผ่านไป แล้วกดต่อไป" -forgotPassword: "ลืมรหัสผ่าน" -fetchingAsApObject: "กำลังดึงข้อมูลจากสหพันธ์..." -ok: "ตกลง" +forgotPassword: "ลืมรหัสผ่านใช่ไหม" +fetchingAsApObject: "กำลังดึงข้อมูล จาก เฟดิเวิร์ส..." +ok: "โอเค" gotIt: "เข้าใจแล้ว !" cancel: "ยกเลิก" -noThankYou: "ไม่เอาดีกว่า" -enterUsername: "กรอกชื่อผู้ใช้" -renotedBy: "รีโน้ตโดย {user}" +noThankYou: "ไม่เป็นไร" +enterUsername: "ใส่ชื่อผู้ใช้" +renotedBy: "รีโน้ตโดย {ผู้ใช้}" noNotes: "ไม่มีโน้ต" noNotifications: "ไม่มีการแจ้งเตือน" -instance: "เซิร์ฟเวอร์" +instance: "อินสแตนซ์" settings: "การตั้งค่า" notificationSettings: "ตั้งค่าการแจ้งเตือน" basicSettings: "การตั้งค่าพื้นฐาน" @@ -30,432 +26,388 @@ otherSettings: "การตั้งค่าอื่นๆ" openInWindow: "เปิดในหน้าต่าง" profile: "โปรไฟล์" timeline: "ไทม์ไลน์" -noAccountDescription: "ผู้ใช้รายนี้ยังไม่ได้เขียนคำแนะนำตัว" +noAccountDescription: "ผู้ใช้รายนี้ยังไม่ได้เขียนลงประวัติของพวกเขา" login: "เข้าสู่ระบบ" loggingIn: "กำลังเข้าสู่ระบบ" logout: "ออกจากระบบ" signup: "สร้างบัญชีผู้ใช้" -uploading: "กำลังอัปโหลด" +uploading: "กำลังอัพโหลด..." save: "บันทึก" -users: "ผู้ใช้" +users: "ผู้ใช้งาน" addUser: "เพิ่มผู้ใช้" favorite: "รายการโปรด" favorites: "รายการโปรด" unfavorite: "ลบออกจากรายการโปรด" -favorited: "เพิ่มลงรายการโปรดแล้ว" -alreadyFavorited: "เพิ่มลงรายการโปรดอยู่แล้ว" -cantFavorite: "ไม่สามารถเพิ่มลงรายการโปรดได้" -pin: "ปักหมุด" -unpin: "เลิกปักหมุด" +favorited: "เพิ่มแล้วในรายการโปรด" +alreadyFavorited: "เพิ่มในรายการโปรดอยู่แล้ว" +cantFavorite: "ไม่สามารถเพิ่มในรายการโปรดได้" +pin: "ปักหมุดไปยังโปรไฟล์" +unpin: "เลิกปักหมุดจากโปรไฟล์" copyContent: "คัดลอกเนื้อหา" copyLink: "คัดลอกลิงก์" -copyRemoteLink: "คัดลอกลิงค์ระยะไกล" -copyLinkRenote: "คัดลอกลิงก์รีโน้ต" delete: "ลบ" deleteAndEdit: "ลบและแก้ไข" -deleteAndEditConfirm: "ต้องการลบโน้ตนี้และแก้ไขใหม่ใช่ไหม? รีแอคชั่น รีโน้ต และการตอบกลับต่อโน้ตนี้ทั้งหมดจะถูกลบออกด้วย" -addToList: "เพิ่มลงรายชื่อ" -addToAntenna: "เพิ่มไปยังเสาอากาศ" +deleteAndEditConfirm: "นายแน่ใจแล้วเหรอ? ว่าต้องการลบโน้ตนี้และแก้ไข คุณอาจจะสูญเสียการโต้ตอบ, โน้ต, และการตอบกลับทั้งหมดได้นะ" +addToList: "เพิ่มในลิสต์" sendMessage: "ส่งข้อความ" copyRSS: "คัดลอก RSS" copyUsername: "คัดลอกชื่อผู้ใช้" copyUserId: "คัดลอก ID ผู้ใช้" copyNoteId: "คัดลอก ID โน้ต " -copyFileId: "คัดลอกไฟล์ ID" -copyFolderId: "คัดลอกโฟลเดอร์ ID" -copyProfileUrl: "คัดลอกโปรไฟล์ URL" -searchUser: "ค้นหาผู้ใช้" -searchThisUsersNotes: "ค้นหาโน้ตของผู้ใช้" +searchUser: "ค้นหาผู้ใช้งาน" reply: "ตอบกลับ" -loadMore: "แสดงเพิ่มเติม" +loadMore: "โหลดเพิ่มเติม" showMore: "แสดงเพิ่มเติม" showLess: "ปิด" youGotNewFollower: "ได้ติดตามคุณ" -receiveFollowRequest: "มีคำขอติดตามส่งมาหา" -followRequestAccepted: "การติดตามได้รับการอนุมัติแล้ว" +receiveFollowRequest: "คำขอผู้ติดตามที่ได้รับ" +followRequestAccepted: "ผู้ติดตามได้ตอบรับคำขอร้องของคุณแล้ว" mention: "กล่าวถึง" -mentions: "กล่าวถึงคุณ" -directNotes: "โพสต์แบบไดเร็กต์" +mentions: "พูดถึง" +directNotes: "ไดเร็คโน้ต" importAndExport: "นำเข้า / ส่งออก" import: "นำเข้า" -export: "ส่งออก" +export: "นำออก" files: "ไฟล์" download: "ดาวน์โหลด" -driveFileDeleteConfirm: "ต้องการลบไฟล์ “{name}” ใช่ไหม? โน้ตที่แนบมากับไฟล์นี้ก็จะถูกลบไปด้วย" -unfollowConfirm: "ต้องการเลิกติดตาม {name} ใช่ไหม?" -exportRequested: "คุณได้ร้องขอการส่งออก อาจใช้เวลาสักครู่ และจะถูกเพิ่มในไดรฟ์ของคุณเมื่อเสร็จสิ้นแล้ว" -importRequested: "คุณได้ร้องขอการนำเข้า การดำเนินการนี้อาจใช้เวลาสักครู่" -lists: "รายชื่อ" -noLists: "คุณไม่มีรายชื่อใดๆ" +driveFileDeleteConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการลบไฟล์ \"{name}\" โน้ตย่อที่แนบมากับไฟล์นี้ก็จะถูกลบด้วยนะ" +unfollowConfirm: "นายแน่ใจแล้วหรอว่าต้องการเลิกติดตาม {name}?" +exportRequested: "เมื่อคุณได้ร้องขอการส่งออก อาจจะต้องใช้เวลาสักครู่ และจะถูกเพิ่มในไดรฟ์ของคุณเมื่อเสร็จสิ้นแล้ว" +importRequested: "เมื่อคุณได้ร้องขอการนำเข้า อาจจะต้องใช้เวลาสักครู่นะ" +lists: "รายการ" +noLists: "คุณไม่มีลิสต์ใด ๆ" note: " โน้ต" -notes: " โน้ต" +notes: "ตัวโน้ต" following: "กำลังติดตาม" followers: "ผู้ติดตาม" followsYou: "ติดตามคุณ" -createList: "สร้างรายชื่อ" -manageLists: "จัดการรายชื่อ" +createList: "สร้างลิสต์" +manageLists: "จัดการลิสต์" error: "ผิดพลาด!" somethingHappened: "อุ๊ย ! มีอะไรบางอย่างผิดพลาด" retry: "ลองใหม่อีกครั้ง" pageLoadError: "เกิดข้อผิดพลาดในการโหลดหน้านี้" -pageLoadErrorDescription: "ปัญหานี้มักเกิดจากแคชของเครือข่ายหรือเบราว์เซอร์ ควรล้างแคช, รอสักครู่ แล้วลองใหม่อีกครั้ง" -serverIsDead: "เซิร์ฟเวอร์นี้ไม่มีการตอบสนอง โปรดกรุณารอสักครู่แล้วลองใหม่อีกครั้ง" -youShouldUpgradeClient: "หากต้องการดูหน้านี้ กรุณาโหลดหน้าใหม่เพื่ออัปเดตไคลเอ็นต์ของคุณ" -enterListName: "ป้อนนามเรียกของรายชื่อชุดนี้" +pageLoadErrorDescription: "โดยปกติแล้วมักจะเกิดจากข้อผิดพลาดของเครือข่ายหรือแคชของเบราว์เซอร์ ลองล้างแคชแล้วลองใหม่อีกครั้งหลังจากรอสักครู่ " +serverIsDead: "เซิร์ฟเวอร์นี้ไม่มีการตอบสนอง ได้โปรดกรุณารอสักครู่แล้วลองใหม่อีกครั้งนะ" +youShouldUpgradeClient: "หากต้องการดูหน้านี้ได้โปรดกรุณา รีเซ็ตเพื่ออัปเดตไคลเอ็นต์ของคุณนะ" +enterListName: "ใส่ชื่อสำหรับรายการลิสต์" privacy: "ความเป็นส่วนตัว" -makeFollowManuallyApprove: "อนุมัติคำขอติดตามด้วยตนเอง" +makeFollowManuallyApprove: "ติดตามคำขอที่ต้องได้รับการอนุมัติ" defaultNoteVisibility: "การมองเห็นที่เป็นค่าเริ่มต้น" -follow: "ติดตาม" +follow: "กำลังติดตาม" followRequest: "ส่งคำขอติดตาม" followRequests: "ส่งคำขอติดตาม" unfollow: "เลิกติดตาม" -followRequestPending: "รออนุมัติคำขอติดตาม" -enterEmoji: "พิมพ์เอโมจิ" +followRequestPending: "กำลังรอดำเนินการร้องขอติดตาม" +enterEmoji: "ใส่อีโมจิ" renote: "รีโน้ต" unrenote: "เลิกรีโน้ต" renoted: "รีโน้ตแล้ว" -renotedToX: "รีโน้ตให้ {name} แล้ว" -cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตใหม่ได้" -cantReRenote: "รีโน้ตไม่สามารถรีโน้ตซ้ำได้" -quote: "อ้างอิง" -inChannelRenote: "รีโน้ตในช่องเท่านั้น" -inChannelQuote: "อ้างอิงในช่องเท่านั้น" -renoteToChannel: "รีโน้ตไปที่ช่อง" -renoteToOtherChannel: "รีโน้ตไปยังช่องอื่น" -pinnedNote: "โน้ตที่ปักหมุดไว้" -pinned: "ปักหมุด" +cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตไว้ใหม่ได้นะ" +cantReRenote: "ไม่สามารถรีโน้ตเอาไว้ใหม่ได้นะ" +quote: "อ้างคำพูด" +inChannelRenote: "รีโน้ตช่องแชลแนลเท่านั้น" +inChannelQuote: "อ้างช่องเท่านั้น" +pinnedNote: "โน้ตที่ปักหมุดเอาไว้" +pinned: "ปักหมุดไปยังโปรไฟล์" you: "คุณ" clickToShow: "คลิกเพื่อแสดง" -sensitive: "เนื้อหาที่ละเอียดอ่อน" +sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW" add: "เพิ่ม" reaction: "รีแอคชั่น" reactions: "รีแอคชั่น" -emojiPicker: "ตัวจิ้มเอโมจิ" -pinnedEmojisForReactionSettingDescription: "ตรึงเอโมจิไว้ด้านบนสำหรับรีแอคชั่นอย่างเร่งด่วน" -pinnedEmojisSettingDescription: "ตรึงเอโมจิไว้ด้านบนสำหรับพิมพ์เอโมจิอย่างเร่งด่วน" -emojiPickerDisplay: "แสดงตัวจิ้มเอโมจิ" -overwriteFromPinnedEmojisForReaction: "เขียนทับการตั้งค่ารีแอคชั่น" -overwriteFromPinnedEmojis: "เขียนทับการตั้งค่าทั่วไป" -reactionSettingDescription2: "ลากเพื่อจัดลำดับใหม่ คลิกที่เอโมจินั้นเพื่อลบ กด “+” เพื่อเพิ่ม" -rememberNoteVisibility: "จำการตั้งค่าการมองเห็นโน้ต" -attachCancel: "ยกเลิกแนบไฟล์" -deleteFile: "ลบไฟล์ออก" -markAsSensitive: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน" -unmarkAsSensitive: "ยกเลิกทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน" +reactionSetting: "รีแอคชั่นไปยังแสดงผลในตัวเลือกการรีแอคชั่น" +reactionSettingDescription2: "กดลากเพื่อจัดลำดับใหม่ กดคลิกเพื่อลบ กด \"+\" เพื่อเพิ่ม" +rememberNoteVisibility: "จดจำการตั้งค่าการมองเห็นตัวโน้ต" +attachCancel: "ลบไฟล์ออกที่แนบมา" +markAsSensitive: "ทำเครื่องหมายว่าละเอียดอ่อน" +unmarkAsSensitive: "ยกเลิกทำเครื่องหมายเป็น NSFW" enterFileName: "พิมพ์ชื่อไฟล์" mute: "ปิดเสียง" unmute: "ยกเลิกการปิดเสียง" renoteMute: "ปิดเสียงรีโน้ต" renoteUnmute: "เปิดเสียง รีโน้ต" -block: "บล็อก" -unblock: "เลิกบล็อก" -suspend: "ระงับ" -unsuspend: "เลิกระงับ" -blockConfirm: "ต้องการบล็อกบัญชีนี้ใช่ไหม?" -unblockConfirm: "ต้องการเลิกบล็อกบัญชีนี้ใช่ไหม?" -suspendConfirm: "ต้องการระงับบัญชีนี้ใช่ไหม?" -unsuspendConfirm: "ต้องการยกเลิกการระงับบัญชีนี้ใช่ไหม?" -selectList: "เลือกรายชื่อ" -editList: "แก้ไขรายชื่อ" -selectChannel: "เลือกช่อง" +block: "บล็อค" +unblock: "เลิกปิดกั้น" +suspend: "ถูกระงับ" +unsuspend: "ยกเลิกระงับ" +blockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต้องการบล็อกบัญชีนี้" +unblockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต้องการปลดบล็อคบัญชีนี้" +suspendConfirm: "นายแน่ใจแล้วเหรอว่าต้องการระงับบัญชีนี้อ่ะ?" +unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้" +selectList: "เลือกรายการ" +editList: "แก้ไขรายการ" +selectChannel: "เลือกแชนแนล" selectAntenna: "เลือกเสาอากาศ" editAntenna: "แก้ไขเสาอากาศ" -createAntenna: "สร้างเสาอากาศ" selectWidget: "เลือกวิดเจ็ต" editWidgets: "แก้ไขวิดเจ็ต" editWidgetsExit: "เรียบร้อย" -customEmojis: "เอโมจิที่กำหนดเอง" -emoji: "เอโมจิ" +customEmojis: "กำหนดอีโมจิเอง" +emoji: "อีโมจิ" emojis: "อีโมจิ" -emojiName: "ชื่อเอโมจิ" -emojiUrl: "URL ของเอโมจิ" -addEmoji: "แทรกเอโมจิ" +emojiName: "ชื่ออิโมจิ" +emojiUrl: "อิโมจิ URL" +addEmoji: "แทรกอีโมจิ" settingGuide: "การตั้งค่าที่แนะนำ" cacheRemoteFiles: "แคชไฟล์ระยะไกล" -cacheRemoteFilesDescription: "หากเปิดใช้งาน ไฟล์ระยะไกลจะถูกแคชไว้ ทำให้แสดงภาพเร็วขึ้น แต่ก็ใช้พื้นที่เก็บข้อมูลของเซิร์ฟเวอร์มากขึ้นเช่นกัน สำหรับขีดจำกัดที่ผู้ใช้ระยะไกลถูกแคชไว้จะขึ้นอยู่กับความจุไดรฟ์ตามบทบาทของเขา เมื่อเกินแล้วไฟล์เก่าจะถูกลบออกและเก็บเป็นลิงก์แทน หากปิดใช้งาน ไฟล์ระยะไกลจะถูกเก็บเป็นลิงก์ตั้งแต่ต้น เราแนะนำให้ตั้งค่า proxyRemoteFiles ใน default.yml เป็น true เพื่อสร้างธัมบ์เนลและปกป้องความเป็นส่วนตัวของผู้ใช้" -youCanCleanRemoteFilesCache: "สามารถลบแคชทั้งหมดได้โดยใช้ปุ่ม 🗑️ ในหน้าการจัดการไฟล์" -cacheRemoteSensitiveFiles: "แคชไฟล์ระยะไกลที่มีเนื้อหาละเอียดอ่อน" -cacheRemoteSensitiveFilesDescription: "เมื่อปิดการใช้งานการตั้งค่านี้ ไฟล์ระยะไกลที่มีเนื้อหาละเอียดอ่อนจะถูกโหลดโดยตรงจากเซิร์ฟเวอร์ระยะไกลโดยไม่มีการแคช" -flagAsBot: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นบอต" -flagAsBotDescription: "เปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยโปรแกรม เมื่อเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นในการป้องกันการสร้างห่วงโซ่การโต้ตอบแบบอนันต์กับบอตตัวอื่น และปรับระบบภายในของ Misskey เพื่อจัดการบัญชีนี้ในฐานะบอต" -flagAsCat: "เมี้ยววววววววววววววว!!!!!!!!!!!" -flagAsCatDescription: "เหมียวเหมียวเมี้ยว??" -flagShowTimelineReplies: "แสดงตอบกลับโน้ตลงไทม์ไลน์" -flagShowTimelineRepliesDescription: "เมื่อเปิดใช้งาน จะแสดงการตอบกลับของผู้ใช้คนนั้นต่อโน้ตอื่นๆ ในไทม์ไลน์ด้วย" -autoAcceptFollowed: "อนุมัติคำขอติดตามจากผู้ใช้ที่คุณติดตามอยู่โดยอัตโนมัติ" +cacheRemoteFilesDescription: "เมื่อปิดใช้งานการตั้งค่านี้ ไฟล์ระยะไกลนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกล แต่กรณีการปิดใช้งานนี้จะช่วยลดปริมาณการใช้พื้นที่จัดเก็บข้อมูล แต่เพิ่มปริมาณการใช้งาน เพราะเนื่องจากจะไม่มีการสร้างภาพขนาดย่อ" +flagAsBot: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นบอท" +flagAsBotDescription: "การเปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยนักเขียนโปรแกรม หรือ ถ้าหากเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นๆ และเพื่อป้องกันการโต้ตอบแบบไม่มีที่สิ้นสุดกับบอทตัวอื่นๆ และยังสามารถปรับเปลี่ยนระบบภายในของ Misskey เพื่อปฏิบัติต่อบัญชีนี้เป็นบอท" +flagAsCat: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นแมว" +flagAsCatDescription: "การเปิดใช้งานตัวเลือกนี้เพื่อทำเครื่องหมายบอกว่าบัญชีนี้เป็นแมว" +flagShowTimelineReplies: "แสดงตอบกลับ ในไทม์ไลน์" +flagShowTimelineRepliesDescription: "แสดงการตอบกลับของผู้ใช้งานไปยังโน้ตของผู้ใช้งานรายอื่นๆในไทม์ไลน์หากได้เปิดเอาไว้" +autoAcceptFollowed: "อนุมัติคำขอติดตามโดยอัตโนมัติทันที จากผู้ใช้งานที่คุณกำลังติดตาม" addAccount: "เพิ่มบัญชี" reloadAccountsList: "รีโหลดรายการบัญชีใหม่" loginFailed: "การเข้าสู่ระบบไม่สำเร็จ" -showOnRemote: "ดูบนเซิร์ฟเวอร์ฝั่งระยะไกล" -continueOnRemote: "ดำเนินการต่อบนเซิร์ฟเวอร์ฝั่งระยะไกล" -chooseServerOnMisskeyHub: "เลือกเซิร์ฟเวอร์จาก Misskey Hub" -specifyServerHost: "ระบุโดเมนของเซิร์ฟเวอร์โดยตรง" -inputHostName: "โปรดป้อนโดเมน" +showOnRemote: "ดูบนอินสแตนซ์ระยะไกล" general: "ทั่วไป" -wallpaper: "ภาพพื้นหลัง" -setWallpaper: "ตั้งค่าภาพพื้นหลัง" -removeWallpaper: "นำภาพพื้นหลังออก" +wallpaper: "วอลล์เปเปอร์" +setWallpaper: "ตั้งวอลเปเปอร์" +removeWallpaper: "นำวอลเปเปอร์ออก" searchWith: "ค้นหา: {q}" -youHaveNoLists: "คุณไม่มีรายชื่อใดๆ " -followConfirm: "ต้องการติดตาม {name} ใช่ไหม?" -proxyAccount: "บัญชีพร็อกซี่" -proxyAccountDescription: "บัญชีพร็อกซี คือ บัญชีที่ทำหน้าที่ติดตาม(ผู้ใช้)ระยะไกลภายใต้เงื่อนไขบางประการ ตัวอย่างเช่น เมื่อผู้ใช้ท้องถิ่นเพิ่มผู้ใช้ระยะไกลลงรายชื่อ หากไม่มีใครติดตามผู้ใช้ระยะไกลในรายชื่อนั้น กิจกรรมก็จะไม่ถูกส่งมายังเซิร์ฟเวอร์ ดังนั้นจึงมีบัญชีพร็อกซีไว้ติดตามผู้ใช้ระยะไกลเหล่านั้น" +youHaveNoLists: "คุณไม่มีลิสต์ใด ๆ " +followConfirm: "คุณแน่ใจแล้วหรอว่าต้องการที่จะติดตาม {name}?" +proxyAccount: "บัญชี พร็อกซี่" +proxyAccountDescription: "บัญชีพร็อกซี่ คือ บัญชีที่จะทำหน้าที่เป็นผู้ติดตามระยะไกลสำหรับผู้ใช้งานที่อยู่ภายใต้ด้วยเงื่อนไขบางอย่าง ยกตัวอย่าง เช่น เมื่อมีผู้ใช้งานนั้นได้เพิ่มผู้ใช้งานจากระยะไกลลงในรายการ แต่กิจกรรมของผู้ใช้ในระยะไกลนั้นจะไม่ถูกส่งไปยังอินสแตนซ์หากไม่มีผู้ใช้งานในพื้นที่ติดตามผู้ใช้รายนั้น ดังนั้นบัญชีพร็อกซีนี้จะติดตามแทน" host: "โฮสต์" -selectSelf: "เลือกตัวเอง" selectUser: "เลือกผู้ใช้งาน" recipient: "ผู้รับ" -annotation: "หมายเหตุประกอบ" -federation: "สหพันธ์" -instances: "เซิร์ฟเวอร์" -registeredAt: "วันที่ลงทะเบียน" -latestRequestReceivedAt: "คำขอล่าสุดที่ได้รับ" +annotation: "ความคิดเห็น" +federation: "เฟดิเวิร์ส" +instances: "ตัวอย่าง" +registeredAt: "จดทะเบียนที่" +latestRequestReceivedAt: "ได้รับคำขอล่าสุดไปแล้ว" latestStatus: "สถานะล่าสุด" storageUsage: "พื้นที่จัดเก็บข้อมูลที่ใช้ไป" -charts: "แผนภูมิ" -perHour: "ต่อชั่วโมง" +charts: "โดดเด่น" +perHour: "ทุกชั่วโมง" perDay: "ต่อวัน" stopActivityDelivery: "หยุดส่งกิจกรรม" -blockThisInstance: "บล็อกเซิร์ฟเวอร์นี้" -silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี้" -mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้" +blockThisInstance: "บล็อกอินสแตนซ์นี้" operations: "ดำเนินการ" software: "ซอฟต์แวร์" version: "เวอร์ชั่น" metadata: "Metadata" -withNFiles: "{n} ไฟล์" +withNFiles: "{n} ไฟล์(s)" monitor: "มอนิเตอร์" jobQueue: "คิวงาน" cpuAndMemory: "ซีพียู และ หน่วยความจำ" -network: "เครือข่าย" +network: "เน็ตเวิร์ก" disk: "ดิสก์" -instanceInfo: "ข้อมูลเซิร์ฟเวอร์" +instanceInfo: "ข้อมูล อินสแตนซ์" statistics: "สถิติการใช้งาน" clearQueue: "ล้างคิว" -clearQueueConfirmTitle: "ต้องการล้างคิวใช่ไหม?" -clearQueueConfirmText: "โพสต์ที่ยังค้างในคิวจะไม่ถูกจัดส่งอีกต่อไป โดยปกติแล้วการดำเนินการนี้ไม่จำเป็น" +clearQueueConfirmTitle: "คุณแน่ใจแล้วหรอว่าต้องการที่จะล้างคิว?" +clearQueueConfirmText: "บันทึกย่อที่ยังไม่ได้ส่งที่เหลืออยู่ในคิวนั้นมักจะ ไม่ถูกรวมเข้าด้วยกัน โดยปกติแล้วไม่จำเป็นต้องดำเนินการนี้" clearCachedFiles: "ล้างแคช" -clearCachedFilesConfirm: "ต้องการลบไฟล์ระยะไกลที่แคชไว้ทั้งหมดใช่ไหม?" -blockedInstances: "เซิร์ฟเวอร์ที่ถูกบล็อก" -blockedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการบล็อก คั่นด้วยการขึ้นบรรทัดใหม่ เซิร์ฟเวอร์ที่ถูกบล็อกจะไม่สามารถติดต่อกับอินสแตนซ์นี้ได้" -silencedInstances: "ปิดปากเซิร์ฟเวอร์นี้แล้ว" -silencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปาก คั่นด้วยการขึ้นบรรทัดใหม่, บัญชีทั้งหมดของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในเซิร์ฟเวอร์นี้ได้หากไม่ได้ถูกติดตามกลับ | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก" -mediaSilencedInstances: "เซิร์ฟเวอร์ที่ถูกปิดปากสื่อ" -mediaSilencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปากสื่อ คั่นด้วยการขึ้นบรรทัดใหม่, ไฟล์ที่ถูกส่งจากบัญชีของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปาก แล้วจะถูกติดเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน และเอโมจิแบบกำหนดเองก็จะใช้ไม่ได้ด้วย | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก" -federationAllowedHosts: "เซิร์ฟเวอร์ที่เปิดให้บริการแบบเฟเดอเรชั่น" -federationAllowedHostsDescription: "ระบุชื่อโฮสต์ของเซิร์ฟเวอร์ที่คุณต้องการอนุญาตให้เชื่อมต่อแบบเฟเดอเรชั่น โดยต้องเว้นวรรคแต่ละบรรทัด" +clearCachedFilesConfirm: "นายแน่ใจแล้วหรอว่าต้องการที่จะลบไฟล์ระยะไกลที่แคชไว้ทั้งหมด?" +blockedInstances: "อินสแตนซ์ที่ ถูกบล็อก" +blockedInstancesDescription: "ระบุชื่อโฮสต์ของอินสแตนซ์ที่คุณต้องการบล็อก อินสแตนซ์ที่อยู่ในรายการนั้นจะไม่สามารถพูดคุยกับอินสแตนซ์นี้ได้อีกต่อไป" muteAndBlock: "ปิดเสียงและบล็อก" mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง" blockedUsers: "ผู้ใช้ที่ถูกบล็อก" noUsers: "ไม่พบผู้ใช้งาน" editProfile: "แก้ไขโปรไฟล์" -noteDeleteConfirm: "ต้องการลบโน้ตนี้ใช่ไหม?" +noteDeleteConfirm: "นายแน่ใจแล้วหรอว่าต้องการลบโน้ตนี้นะ?" pinLimitExceeded: "คุณไม่สามารถปักหมุดโน้ตเพิ่มเติมใดๆได้อีก" +intro: "การติดตั้ง Misskey เสร็จสิ้นแล้วนะ! โปรดสร้างผู้ใช้งานที่เป็นผู้ดูแลระบบ" done: "เสร็จสิ้น" processing: "กำลังประมวลผล..." preview: "แสดงตัวอย่าง" default: "ค่าเริ่มต้น" defaultValueIs: "ค่าเริ่มต้น: {value}" -noCustomEmojis: "ไม่มีเอโมจิ" -noJobs: "ไม่มีงาน" +noCustomEmojis: "ไม่มีอีโมจิ" +noJobs: "ไม่มีชิ้นงาน" federating: "สหพันธ์" blocked: "ถูกบล็อก" -suspended: "ระงับการส่ง" +suspended: "ถูกระงับ" all: "ทั้งหมด" -subscribing: "กำลังสมัครสมาชิก" +subscribing: "สมัครแล้ว" publishing: "กำลังเผยแพร่" notResponding: "ไม่มีการตอบสนอง" -instanceFollowing: "กำลังติดตามบนเซิร์ฟเวอร์" -instanceFollowers: "ผู้ติดตามของเซิร์ฟเวอร์" -instanceUsers: "ผู้ใช้ของเซิร์ฟเวอร์นี้" +instanceFollowing: "กำลังติดตาม บน อินสแตนซ์" +instanceFollowers: "ผู้ติดตามของอินสแตนซ์" +instanceUsers: "ผู้ใช้งานของอินสแตนซ์นี้" changePassword: "เปลี่ยนรหัสผ่าน" security: "ความปลอดภัย" -retypedNotMatch: "ทั้งสองป้อนข้อมูลไม่สอดคล้องกัน" +retypedNotMatch: "อินพุตไม่ตรงกันนะ" currentPassword: "รหัสผ่านปัจจุบัน" newPassword: "รหัสผ่านใหม่" newPasswordRetype: "ใส่รหัสผ่านใหม่อีกครั้ง" attachFile: "แนบไฟล์" -more: "เพิ่มเติม!" +more: "เพิ่มเติม" featured: "ไฮไลท์" usernameOrUserId: "ชื่อผู้ใช้หรือรหัสผู้ใช้งาน" noSuchUser: "ไม่พบผู้ใช้" lookup: "การค้นหา" announcements: "ประกาศ" -imageUrl: "URL รูปภาพ" +imageUrl: "url รูปภาพ" remove: "ลบ" removed: "ถูกลบไปแล้ว" -removeAreYouSure: "ต้องการลบ “{x}” ใช่ไหม?" -deleteAreYouSure: "ต้องการลบ “{x}” ใช่ไหม?" -resetAreYouSure: "รีเซ็ตเลยไหม?" -areYouSure: "แน่ใจแล้วใช่ไหมคะ?" +removeAreYouSure: "นายแน่ใจจริงหรอว่าต้องการที่จะลบออก \"{x}\"" +deleteAreYouSure: "นายแน่ใจจริงหรอว่าต้องการที่จะลบออก \"{x}\"" +resetAreYouSure: "รีเซ็ตเลยไหม" saved: "บันทึกแล้ว" -upload: "อัปโหลด" +messaging: "แชท" +upload: "อัพโหลด" keepOriginalUploading: "เก็บภาพต้นฉบับ" -keepOriginalUploadingDescription: "เก็บภาพต้นฉบับไว้เมื่ออัปโหลดภาพ หากปิด รูปภาพสำหรับการเผยแพร่ทางเว็บจะถูกสร้างขึ้นในเบราว์เซอร์เมื่อทำการอัปโหลด" +keepOriginalUploadingDescription: "บันทึกรูปภาพที่อัพโหลดต้นฉบับตามที่เป็นอยู่ ถ้าหากปิดอยู่ ระบบจะสร้างเวอร์ชั่นที่จะแสดงบนเว็บเมื่ออัพโหลดนะ" fromDrive: "จากไดรฟ์" fromUrl: "จาก URL" -uploadFromUrl: "อัปโหลดจาก URL" +uploadFromUrl: "อัพโหลดจาก URL" uploadFromUrlDescription: "URL ของไฟล์ที่คุณต้องการอัปโหลด" -uploadFromUrlRequested: "ร้องขอการอัปโหลดแล้ว" -uploadFromUrlMayTakeTime: "การอัปโหลดอาจใช้เวลาสักครู่จึงจะเสร็จสมบูรณ์" +uploadFromUrlRequested: "อัพโหลดที่ร้องขอ" +uploadFromUrlMayTakeTime: "มันอาจจะต้องใช้เวลาสักครู่จนกว่าการอัพโหลดจะเสร็จสมบูรณ์นะ" explore: "สำรวจ" messageRead: "อ่านแล้ว" -noMoreHistory: "ไม่มีประวัติเพิ่มเติม" +noMoreHistory: "ในนั้นไม่มีประวัติอีกต่อไปแล้วนะ" +startMessaging: "เริ่มการสนทนา" nUsersRead: "อ่านโดย {n}" -agreeTo: "ฉันยอมรับ {0}" +agreeTo: "ฉันยอมรับที่จะ {0}" agree: "ยอมรับ" -agreeBelow: "ยอมรับตามที่ระบุด้านล่าง" +agreeBelow: "ฉันยอมรับถึงด้านล่าง" basicNotesBeforeCreateAccount: "หมายเหตุสำคัญ" termsOfService: "เงื่อนไขการให้บริการ" -start: "เริ่ม" -home: "หน้าหลัก" -remoteUserCaution: "ข้อมูลอาจไม่สมบูรณ์เนื่องจากผู้ใช้รายนี้มาจากเซิร์ฟเวอร์ระยะไกล" +start: "เริ่มต้น​ใช้งาน​" +home: "หน้าแรก" +remoteUserCaution: "เนื่องจากผู้ใช้งานรายนี้นั้น มาจากอินสแตนซ์ระยะไกล ข้อมูลที่แสดงดังกล่าวนั้นอาจจะไม่สมบูรณ์ก็ได้นะ" activity: "กิจกรรม" images: "รูปภาพ" image: "รูปภาพ" birthday: "วันเกิด" yearsOld: "{age} ปี" -registeredDate: "วันที่ลงทะเบียน" +registeredDate: "วันที่สมัครสมาชิก" location: "ตำแหน่งที่ตั้ง" theme: "ธีม" -themeForLightMode: "ธีมที่จะใช้ในโหมดสว่าง" +themeForLightMode: "ธีมที่จะใช้ในโหมดแสง" themeForDarkMode: "ธีมที่จะใช้ในโหมดมืด" light: "สว่าง" dark: "มืด" lightThemes: "ธีมสว่าง" darkThemes: "ธีมมืด" -syncDeviceDarkMode: "ซิงค์โหมดมืดกับการตั้งค่าอุปกรณ์ของคุณ" +syncDeviceDarkMode: "ซิงค์โหมดมืดด้วยการตั้งค่ากับอุปกรณ์" drive: "ไดรฟ์" fileName: "ชื่อไฟล์" selectFile: "เลือกไฟล์" selectFiles: "เลือกไฟล์" selectFolder: "เลือกโฟลเดอร์" selectFolders: "เลือกโฟลเดอร์" -fileNotSelected: "ยังไม่ได้เลือกไฟล์" renameFile: "เปลี่ยนชื่อไฟล์" -folderName: "ชื่อโฟลเดอร์" +folderName: "ชื่อแฟ้ม" createFolder: "สร้างโฟลเดอร์" renameFolder: "เปลี่ยนชื่อโฟลเดอร์" deleteFolder: "ลบโฟลเดอร์" -folder: "โฟลเดอร์" addFile: "เพิ่มไฟล์" -showFile: "แสดงไฟล์" emptyDrive: "ไดรฟ์ของคุณว่างเปล่านะ" -emptyFolder: "โฟลเดอร์นี้ว่างเปล่า" -unableToDelete: "ไม่สามารถลบออกได้" -inputNewFileName: "ป้อนชื่อไฟล์ใหม่" +emptyFolder: "โฟลเดอร์นี้น่าจะว่างเปล่านะ" +unableToDelete: "ไม่สามารถลบออกได้นะ" +inputNewFileName: "ป้อนชื่อไฟล์ใหม่นะ" inputNewDescription: "กรุณาใส่แคปชั่นใหม่" -inputNewFolderName: "กรุณาใส่ชื่อโฟลเดอร์ใหม่" -circularReferenceFolder: "โฟลเดอร์ปลายทางคือโฟลเดอร์ย่อยของโฟลเดอร์ที่คุณกำลังย้าย" -hasChildFilesOrFolders: "เนื่องจากโฟลเดอร์นี้ไม่ว่างเปล่า จึงไม่สามารถลบ" +inputNewFolderName: "กรุณาใส่ชื่อโฟลเดอร์ใหม่นะ\n" +circularReferenceFolder: "โฟลเดอร์ปลายทาง คือ โฟลเดอร์ย่อยของโฟลเดอร์ที่คุณต้องการที่จะย้ายล่ะนะ" +hasChildFilesOrFolders: "เนื่องจากโฟลเดอร์นี้ไม่ว่างเปล่า จึงไม่สามารถลบได้นะ" copyUrl: "คัดลอก URL" rename: "เปลี่ยนชื่อ" avatar: "ไอคอน" banner: "แบนเนอร์" -displayOfSensitiveMedia: "แสดงสื่อที่มีเนื้อหาละเอียดอ่อน" -whenServerDisconnected: "เมื่อสูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์" -disconnectedFromServer: "การเชื่อมต่อเซิร์ฟเวอร์ถูกตัด" +whenServerDisconnected: "สูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์" +disconnectedFromServer: "ถูกตัดการเชื่อมต่อออกจากเซิร์ฟเวอร์" reload: "รีโหลด" -doNothing: "ช่างมัน" -reloadConfirm: "รีโหลดเลยไหม?" -watch: "เพ่งเล็ง" -unwatch: "เลิกเพ่งเล็ง" +doNothing: "เมิน" +reloadConfirm: "นายต้องการรีเฟรชไทม์ไลน์หรือป่าว?" +watch: "ดู" +unwatch: "หยุดดู" accept: "ยอมรับ" reject: "ปฏิเสธ" normal: "ปกติ" -instanceName: "ชื่อเซิร์ฟเวอร์" -instanceDescription: "คำอธิบายแนะนำเซิร์ฟเวอร์" +instanceName: "ชื่อ อินสแตนซ์" +instanceDescription: "คำอธิบายอินสแตนซ์" maintainerName: "ผู้ดูแล" -maintainerEmail: "อีเมลผู้ดูแลระบบ" -tosUrl: "URL เงื่อนไขการให้บริการ" +maintainerEmail: "อีเมล์แอดมิน" +tosUrl: "เงื่อนไขการให้บริการ URL" thisYear: "ปีนี้" thisMonth: "เดือนนี้" today: "วันนี้" -dayX: "{day}" -monthX: "เดือน {month}" -yearX: "{year}" -pages: "หน้าเพจ" -integration: "เชื่อมโยง" +dayX: "{วัน}" +monthX: "{เดือน}" +yearX: "{ปี}" +pages: "หน้า" +integration: "รวบรวม" connectService: "เชื่อมต่อ" disconnectService: "ตัดการเชื่อมต่อ" -enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ท้องถิ่น" +enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ในพื้นที่" enableGlobalTimeline: "เปิดใช้งานไทม์ไลน์ทั่วโลก" disablingTimelinesInfo: "ผู้ดูแลระบบและผู้ควบคุมจะสามารถเข้าถึงไทม์ไลน์ทั้งหมด ถึงแม้ว่าจะไม่ได้เปิดใช้งานก็ตาม" registration: "ลงทะเบียน" -invite: "คำเชิญ" -driveCapacityPerLocalAccount: "ความจุของไดรฟ์ต่อผู้ใช้ท้องถิ่น" +enableRegistration: "เปิดใช้งานการลงทะเบียนผู้ใช้ใหม่" +invite: "เชิญชวน" +driveCapacityPerLocalAccount: "ความจุของไดรฟ์ต่อผู้ใช้ภายในเครื่อง" driveCapacityPerRemoteAccount: "ความจุของไดรฟ์ต่อผู้ใช้ระยะไกล" inMb: "เป็นเมกะไบต์" +iconUrl: "ไอคอน URL" bannerUrl: "URL รูปภาพแบนเนอร์" backgroundImageUrl: "URL ภาพพื้นหลัง" basicInfo: "ข้อมูลเบื้องต้น" -pinnedUsers: "ผู้ใช้ที่ถูกปักหมุด" -pinnedUsersDescription: "ป้อนชื่อผู้ใช้ที่คุณต้องการปักหมุดในหน้า “ค้นพบ” ฯลฯ คั่นด้วยการขึ้นบรรทัดใหม่" -pinnedPages: "หน้าเพจที่ปักหมุด" -pinnedPagesDescription: "ป้อนเส้นทางของหน้าเพจที่คุณต้องการปักหมุดไว้ที่หน้าแรกของเซิร์ฟเวอร์นี้ คั่นด้วยการขึ้นบรรทัดใหม่" +pinnedUsers: "ผู้ใช้งานที่ได้รับการปักหมุด" +pinnedUsersDescription: "ลิสต์ชื่อผู้ใช้โดยคั่นด้วยการขึ้นบรรทัดใหม่เพื่อปักหมุดในแท็บ \"สำรวจ\"" +pinnedPages: "หน้าที่ปักหมุด" +pinnedPagesDescription: "ป้อนเส้นทางของหน้าที่คุณต้องการตรึงไว้ที่หน้าแรกของอินสแตนซ์นี้ โดยคั่นด้วยตัวแบ่งบรรทัด" pinnedClipId: "ID ของคลิปที่จะปักหมุด" -pinnedNotes: "โน้ตที่ปักหมุดไว้" +pinnedNotes: "โน้ตที่ปักหมุดเอาไว้" hcaptcha: "hCaptcha" enableHcaptcha: "เปิดใช้ hCaptcha" hcaptchaSiteKey: "คีย์ไซต์" hcaptchaSecretKey: "คีย์ลับ" -mcaptcha: "mCaptcha" -enableMcaptcha: "เปิดใช้ mCaptcha" -mcaptchaSiteKey: "คีย์ไซต์" -mcaptchaSecretKey: "คีย์ลับ" -mcaptchaInstanceUrl: "URL ของอินสแตนซ์ของ mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "เปิดใช้ reCAPTCHA" recaptchaSiteKey: "คีย์ไซต์" recaptchaSecretKey: "คีย์ลับ" -turnstile: "Turnstile" -enableTurnstile: "เปิดใช้งาน Turnstile" +turnstile: "เทิร์น'สไทล" +enableTurnstile: "เปิดใช้งาน เทิร์น'สไทล" turnstileSiteKey: "คีย์ไซต์" turnstileSecretKey: "คีย์ลับ" -avoidMultiCaptchaConfirm: "การใช้ Captcha หลายตัวอาจทำให้เกิดการรบกวนหรือข้อผิดพลาดได้ ต้องการที่จะปิดการใช้งาน Captcha ตัวอื่นเลยไหม? หากต้องการให้เปิดใช้งานต่อไป ให้กดยกเลิก" +avoidMultiCaptchaConfirm: "การใช้ระบบ Captcha หลายระบบอาจทำให้เกิดการรบกวนหรืออาจจะเกิดข้อผิดพลาดได้ หากต้องการที่จะปิดการใช้งานระบบ Captcha อื่น ๆ แนะนำให้ปิดตัวอื่นๆก่อน ถ้าหากคุณต้องการให้เปิดใช้งานต่อไป ให้ กด ยกเลิก" antennas: "เสาอากาศ" manageAntennas: "จัดการเสาอากาศ" name: "ชื่อ" antennaSource: "แหล่งเสาอากาศ" antennaKeywords: "คีย์เวิร์ดที่ควรฟัง" antennaExcludeKeywords: "คีย์เวิร์ดที่จะยกเว้น" -antennaExcludeBots: "ยกเว้นบัญชีบอต" -antennaKeywordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่อนไข AND, หรือขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR" +antennaKeywordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ" notifyAntenna: "แจ้งเตือนเกี่ยวกับโน้ตใหม่" withFileAntenna: "เฉพาะโน้ตที่มีไฟล์" -enableServiceworker: "เปิดใช้งานการแจ้งเตือนแบบพุชไปยังเบราว์เซอร์ของคุณ" +enableServiceworker: "เปิดใช้งาน การแจ้งเตือนแบบพุชสำหรับเบราว์เซอร์ของคุณ" antennaUsersDescription: "ระบุหนึ่งชื่อผู้ใช้ต่อบรรทัด" -caseSensitive: "อักษรพิมพ์ใหญ่-พิมพ์เล็กความหมายต่างกัน" +caseSensitive: "กรณีที่สำคัญ" withReplies: "รวมตอบกลับ" connectedTo: "บัญชีดังต่อไปนี้มีการเชื่อมต่อกัน" notesAndReplies: "โพสต์และการตอบกลับ" -withFiles: "มีไฟล์" +withFiles: "รวบรวมไฟล์" silence: "ถูกปิดปาก" -silenceConfirm: "ต้องการปิดปากผู้ใช้รายนี้ใช่ไหม?" +silenceConfirm: "นายแน่ใจแล้วหรอว่าต้องการที่จะ ปิดปาก ผู้ใช้งานรายนี้?" unsilence: "ยกเลิกการปิดปาก" -unsilenceConfirm: "ต้องการเลิกปิดปากผู้ใช้รายนี้ใช่ไหม?" +unsilenceConfirm: "นายแน่ใจแล้วหรอว่าต้องการที่จะยกเลิกปิดปากผู้ใช้งานรายนี้?" popularUsers: "ผู้ใช้ที่เป็นที่นิยม" recentlyUpdatedUsers: "ผู้ใช้ที่เพิ่งใช้งานล่าสุด" recentlyRegisteredUsers: "ผู้ใช้ที่เข้าร่วมใหม่" -recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพบล่าสุด" -exploreUsersCount: "มีผู้ใช้ {count} ราย" -exploreFediverse: "สำรวจสหพันธ์" +recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพบใหม่" +exploreUsersCount: "มีผู้ใช้ {จำนวน} ราย" +exploreFediverse: "สำรวจเฟดดิเวิร์ส" popularTags: "แท็กยอดนิยม" -userList: "ลิสต์" +userList: "รายการ" about: "เกี่ยวกับ" aboutMisskey: "เกี่ยวกับ Misskey" administrator: "ผู้ดูแลระบบ" token: "โทเค็น" 2fa: "การยืนยันตัวตนแบบสองชั้น" -setupOf2fa: "ตั้งค่าการยืนยันตัวตนแบบสองชั้น" totp: "แอป Authenticator" totpDescription: "ใช้แอปยืนยันตัวตนเพื่อป้อนรหัสผ่านแบบใช้ครั้งเดียว" moderator: "ผู้ควบคุม" moderation: "การกลั่นกรอง" -moderationNote: "โน้ตการกลั่นกรอง" -moderationNoteDescription: "คุณสามารถใส่โน้ตส่วนตัวที่เฉพาะผู้ดูแลระบบเท่านั้นที่สามารถเข้าถึงได้" -addModerationNote: "เพิ่มโน้ตการกลั่นกรอง" -moderationLogs: "ปูมการควบคุมดูแล" -nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} ราย" +nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้" securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน" securityKey: "กุญแจความปลอดภัย" lastUsed: "ใช้ล่าสุด" @@ -464,19 +416,20 @@ unregister: "เลิกติดตาม" passwordLessLogin: "เข้าสู่ระบบแบบไม่ใช้รหัสผ่าน" passwordLessLoginDescription: "อนุญาตให้เข้าสู่ระบบโดยไม่ต้องใช้รหัสผ่านโดยใช้รหัสรักษาความปลอดภัยหรือรหัสผ่านเท่านั้น" resetPassword: "รีเซ็ตรหัสผ่าน" -newPasswordIs: "รหัสผ่านใหม่คือ “{password}”" +newPasswordIs: "รหัสผ่านใหม่คือ \"{password}\"" reduceUiAnimation: "ลดภาพเคลื่อนไหว UI" -share: "แบ่งปัน" +share: "แชร์" notFound: "ไม่พบหน้าที่ต้องการ" -notFoundDescription: "ไม่พบหน้าตาม URL ที่ระบุ" -uploadFolder: "โฟลเดอร์เริ่มต้นสำหรับอัปโหลด" +notFoundDescription: "ไม่พบหน้าที่สอดคล้องตรงกันกับ URL นี้นะ" +uploadFolder: "โฟลเดอร์เริ่มต้นสำหรับอัพโหลด" +cacheClear: "ล้างแคช" markAsReadAllNotifications: "ทำเครื่องหมายการแจ้งเตือนทั้งหมดว่าอ่านแล้ว" markAsReadAllUnreadNotes: "ทำเครื่องหมายโน้ตทั้งหมดว่าอ่านแล้ว" markAsReadAllTalkMessages: "ทำเครื่องหมายข้อความทั้งหมดว่าอ่านแล้ว" help: "ช่วยเหลือ" inputMessageHere: "พิมพ์ข้อความที่นี่" close: "ปิด" -invites: "คำเชิญ" +invites: "เชิญชวน" members: "สมาชิก" transfer: "ถ่ายโอน" title: "หัวข้อ" @@ -484,62 +437,58 @@ text: "ข้อความ" enable: "เปิดใช้งาน" next: "ถัด​ไป" retype: "พิมพ์รหัสอีกครั้ง" -noteOf: "โน้ตของ {user}" +noteOf: "โน้ต โดย {ผู้ใช้งาน}" quoteAttached: "อ้างอิง" -quoteQuestion: "ต้องการที่จะแนบมันเพื่ออ้างอิงใช่ไหม?" -attachAsFileQuestion: "ข้อความในคลิปบอร์ดยาวเกินไป คุณต้องการแนบเป็นไฟล์ข้อความหรือไม่?" -onlyOneFileCanBeAttached: "สามารถแนบไฟล์ได้เพียงไฟล์เดียวต่อ 1 ข้อความ" -signinRequired: "ก่อนดำเนินการต่อ กรุณาลงทะเบียนหรือเข้าสู่ระบบ" -signinOrContinueOnRemote: "เพื่อดำเนินการต่อได้ คุณต้องไปที่เซิร์ฟเวอร์ที่คุณใช้งานอยู่ หรือลงทะเบียน/เข้าสู่ระบบเซิร์ฟเวอร์นี้" -invitations: "คำเชิญ" -invitationCode: "รหัสเชิญ" +quoteQuestion: "นายต้องการที่จะอ้างอิงหรอ?" +noMessagesYet: "ยังไม่มีข้อความนะ" +newMessageExists: "คุณมีข้อความใหม่" +onlyOneFileCanBeAttached: "คุณสามารถแนบไฟล์กับข้อความได้เพียงไฟล์เดียวเท่านั้นนะ" +signinRequired: "กรุณาลงทะเบียนหรือลงชื่อเข้าใช้ก่อนดำเนินการต่อนะ" +invitations: "เชิญชวน" +invitationCode: "รหัสคำเชิญ" checking: "Checking" available: "พร้อมใช้งาน" unavailable: "ไม่พร้อมใช้" -usernameInvalidFormat: "สามารถใช้ a~z A~Z 0~9 และ _ ได้" +usernameInvalidFormat: "คุณสามารถใช้อักษรตัวพิมพ์ใหญ่และตัวพิมพ์เล็ก ตัวเลข และขีดล่างได้นะ ( a-z , A-Z , 0-9 , รวมไปถึงอักษรพิเศษเช่น + * / , . - อื่นๆเป็นต้น )" tooShort: "สั้นเกินไปนะ" tooLong: "ยาวเกินไปนะ" -weakPassword: "รหัสผ่านแย่มาก" +weakPassword: "รหัสผ่าน แย่มาก" normalPassword: "รหัสผ่านปกติ" strongPassword: "รหัสผ่านรัดกุมมาก" passwordMatched: "ถูกต้อง!" passwordNotMatched: "ไม่ถูกต้อง" -signinWith: "เข้าสู่ระบบด้วย {x}" -signinFailed: "ไม่สามารถเข้าสู่ระบบได้ กรุณาตรวจสอบชื่อผู้ใช้และรหัสผ่าน" +signinWith: "ลงชื่อเข้าใช้ด้วย {x}" +signinFailed: "ไม่สามารถลงชื่อผู้เข้าใช้ได้ เนื่องจาก ชื่อผู้ใช้หรือรหัสผ่านที่คุณป้อนนั้นไม่ถูกต้องนะ" or: "หรือ" language: "ภาษา" uiLanguage: "ภาษาอินเทอร์เฟซผู้ใช้งาน" aboutX: "เกี่ยวกับ {x}" -emojiStyle: "สไตล์ของเอโมจิ" +emojiStyle: "สไตล์อิโมจิ" native: "ภาษาแม่" -menuStyle: "สไตล์เมนู" -style: "สไตล์" -drawer: "ตัววาด" -popup: "ป๊อปอัพ" -showNoteActionsOnlyHover: "แสดงการดำเนินการโน้ตเมื่อโฮเวอร์(วางเมาส์เหนือ)เท่านั้น" -showReactionsCount: "แสดงจำนวนรีแอกชั่นในโน้ต" -noHistory: "ไม่มีประวัติ" +disableDrawer: "อย่าใช้ลิ้นชักสไตล์เมนู" +showNoteActionsOnlyHover: "แสดงการดำเนินการเฉพาะโน้ตเมื่อโฮเวอร์" +noHistory: "ไม่มีรายการ" signinHistory: "ประวัติการเข้าสู่ระบบ" enableAdvancedMfm: "เปิดใช้งาน MFM ขั้นสูง" -enableAnimatedMfm: "เปิดการใช้งาน MFM แบบเคลื่อนไหว" +enableAnimatedMfm: "เปิดการใช้งาน MFM ด้วยแอนิเมชั่น" doing: "กำลังประมวลผล......" category: "หมวดหมู่" tags: "นามแฝง" docSource: "ที่มาของเอกสารนี้" createAccount: "สร้างบัญชี" -existingAccount: "บัญชีที่มีอยู่แล้ว" +existingAccount: "บัญชีที่มีอยู่" regenerate: "สร้างอีกครั้ง" fontSize: "ขนาดตัวอักษร" -mediaListWithOneImageAppearance: "ความสูงของรายการสื่อที่มีเพียงรูปเดียว" +mediaListWithOneImageAppearance: "ความสูงของลิสต์สื่อจะต้องมีรูปภาพเดียวเท่านั้น" limitTo: "จำกัดไว้ที่ {x}" noFollowRequests: "คุณไม่มีคำขอติดตามที่รอดำเนินการ" openImageInNewTab: "เปิดรูปภาพในแท็บใหม่" dashboard: "หน้ากระดานหลัก" -local: "ท้องถิ่น" +local: "ในพื้นที่" remote: "ระยะไกล" total: "รวมทั้งหมด" -weekOverWeekChanges: "เทียบกับสัปดาห์ก่อน" -dayOverDayChanges: "เทียบกับเมื่อวาน" +weekOverWeekChanges: "เปลี่ยนแปลงไปเมื่อสัปดาห์ที่แล้ว" +dayOverDayChanges: "เปลี่ยนแปลงไปเมื่อวานนี้" appearance: "ภาพลักษณ์" clientSettings: "การตั้งค่าไคลเอนต์" accountSettings: "ตั้งค่าบัญชี" @@ -548,29 +497,28 @@ promote: "โปรโมท" numberOfDays: "จำนวนวัน" hideThisNote: "ซ่อนโน้ตนี้" showFeaturedNotesInTimeline: "แสดงโน้ตเด่นในไทม์ไลน์" -objectStorage: "การจัดเก็บในรูปแบบอ็อบเจกต์" -useObjectStorage: "ใช้การจัดเก็บในรูปแบบอ็อบเจกต์" -objectStorageBaseUrl: "Base URL" +objectStorage: "อ็อบเจ็กต์ ที่จัดเก็บ" +useObjectStorage: "ใช้ อ็อบเจ็กต์ ที่จัดเก็บ" +objectStorageBaseUrl: "URL ฐาน" objectStorageBaseUrlDesc: "URL ที่ใช้เป็นข้อมูลอ้างอิง ระบุ URL ของ CDN หรือ Proxy ถ้าหากคุณใช้อย่างใดอย่างหนึ่ง\n สำหรับการใช้งาน S3 'https://.s3.amazonaws.com' และสำหรับ GCS หรือบริการที่เทียบเท่าใช้ 'https://storage.googleapis.com/', เป็นต้น" objectStorageBucket: "Bucket" -objectStorageBucketDesc: "โปรดระบุชื่อบัคเก็ตของบริการที่ใช้อยู่" +objectStorageBucketDesc: "โปรดระบุชื่อที่เก็บข้อมูลที่ใช้กับผู้ให้บริการของคุณ" objectStoragePrefix: "คำนำหน้า" -objectStoragePrefixDesc: "ไฟล์ทั้งหมดจะถูกเก็บไว้ภายใต้ไดเร็กทอรีที่มีคำนำหน้านี้" +objectStoragePrefixDesc: "ไฟล์ทั้งหมดจะถูกเก็บไว้ภายใต้ไดเร็กทอรีที่มีคำนำหน้านี้นะ" objectStorageEndpoint: "ปลายทาง" objectStorageEndpointDesc: "เว้นว่างไว้หากคุณใช้ AWS S3 หรือระบุปลายทางเป็น '' หรือ ':' ทั้งนี้ขึ้นอยู่กับผู้ให้บริการที่คุณใช้อยู่ด้วย" objectStorageRegion: "ภูมิภาค" -objectStorageRegionDesc: "ระบุภูมิภาค เช่น ‘xx-east-1’ หากบริการของคุณไม่แยกภูมิภาค ให้ระบุเป็น ‘us-east-1’ หรือเว้นวางไว้หากใช้ AWS configuration files / environment variables" +objectStorageRegionDesc: "ระบุภูมิภาค เช่น 'xx-east-1' ถ้าหากบริการของคุณไม่ได้แยกความแตกต่างระหว่างภูมิภาคก็ให้ เว้นว่างไว้หรือป้อน 'us-east-1'" objectStorageUseSSL: "ใช้ SSL" objectStorageUseSSLDesc: "ปิดการทำงานนี้ไว้ ถ้าหากคุณจะไม่ใช้ HTTPS สำหรับการเชื่อมต่อ API" objectStorageUseProxy: "เชื่อมต่อผ่านพร็อกซี" objectStorageUseProxyDesc: "ปิดสิ่งนี้ไว้ถ้าหากคุณจะไม่ใช้ Proxy สำหรับการเชื่อมต่อ API" -objectStorageSetPublicRead: "ตั้งค่าเป็น “public-read” เมื่ออัปโหลด" -s3ForcePathStyleDesc: "เมื่อเปิดใช้งาน s3ForcePathStyle จะบังคับให้ ระบุชื่อบัคเก็ตเป็นส่วนหนึ่งของพาธ แทนที่จะเป็นชื่อโฮสต์ใน URL, อาจจำเป็นต้องเปิดใช้งานตัวเลือกนี้เมื่อใช้กับ Minio ที่โฮสต์เองหรือบริการที่คล้ายกัน" -serverLogs: "ปูมของเซิร์ฟเวอร์" +objectStorageSetPublicRead: "ตั้งค่า \"public-read\" ในการอัปโหลด" +s3ForcePathStyleDesc: "ถ้าหากเปิดใช้งาน s3ForcePathStyle ชื่อบัคเก็ตนั้นอาจจะต้องรวมอยู่ในเส้นทางของ URL ซึ่งตรงข้ามกับชื่อโฮสต์ของ URL คุณอาจจะต้องเปิดใช้งานการตั้งค่านี้เมื่อใช้บริการต่างๆ เช่น อินสแตนซ์ Minio ที่โฮสต์เองนะ" +serverLogs: "บันทึกของเซิร์ฟเวอร์" deleteAll: "ลบทั้งหมด" showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนสุดของไทม์ไลน์" -showFixedPostFormInChannel: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนของไทม์ไลน์ (ช่อง)" -withRepliesByDefaultForNewlyFollowed: "แสดงการตอบกลับจากผู้ใช้ที่คุณเพิ่งติดตามลงไทม์ไลน์ตามค่าเริ่มต้น" +showFixedPostFormInChannel: "แสดงแบบฟอร์มกำลังโพสต์ที่ด้านบนของไทม์ไลน์ (แชนแนล)" newNoteRecived: "มีโน้ตใหม่" sounds: "เสียง" sound: "เสียง" @@ -578,12 +526,10 @@ listen: "ฟัง" none: "ไม่มี" showInPage: "แสดงในเพจ" popout: "ป๊อปเอาต์" -volume: "ระดับเสียง" -masterVolume: "ระดับเสียงหลัก" -notUseSound: "ไม่ใช้เสียง" -useSoundOnlyWhenActive: "มีเสียงออกเฉพาะตอนกำลังใช้ Misskey อยู่เท่านั้น" +volume: "ความดัง" +masterVolume: "มาสเตอร์วอลุ่ม" details: "รายละเอียด" -chooseEmoji: "เลือกเอโมจิ" +chooseEmoji: "เลือกโมจิของเธอ" unableToProcess: "ไม่สามารถดำเนินการให้เสร็จสิ้นได้" recentUsed: "ใช้ล่าสุด" install: "ติดตั้ง" @@ -594,39 +540,33 @@ installedDate: "วันที่ติดตั้ง" lastUsedDate: "ใช้งานครั้งล่าสุด" state: "สถานะ" sort: "เรียงลำดับ" -ascendingOrder: "เรียงลำดับขึ้น" -descendingOrder: "เรียงลำดับลง" -scratchpad: "Scratchpad" -scratchpadDescription: "Scratchpad ให้สภาพแวดล้อมสำหรับการทดลอง AiScript คุณสามารถเขียนโค้ด/สั่งดำเนินการ/ตรวจสอบผลลัพธ์ ของการโต้ตอบกับ Misskey ได้" -uiInspector: "ตัวตรวจสอบ UI" -uiInspectorDescription: "คุณสามารถตรวจสอบรายชื่อเซิร์ฟเวอร์ที่เกี่ยวข้องกับส่วนประกอบอินเตอร์เฟซผู้ใช้ (UI) บนหน่วยความจำของระบบ ส่วนประกอบ UI เหล่านี้จะถูกสร้างขึ้นโดยฟังก์ชัน Ui:C:" +ascendingOrder: "เรียงจากน้อยไปมาก" +descendingOrder: "เรียงจากมากไปน้อย" +scratchpad: "กระดานทดลอง" +scratchpadDescription: "Scratchpad เป็นการจัดเตรียมสภาพแวดล้อมสำหรับการทดลอง AiScript แต่คุณสามารถเขียน ดำเนินการ และตรวจสอบผลลัพธ์ของการโต้ตอบกับ Misskey มันได้ด้วยนะ" output: "เอาท์พุต" script: "สคริปต์" disablePagesScript: "ปิดการใช้งาน AiScript บนเพจ" updateRemoteUser: "อัปเดตข้อมูลผู้ใช้งานระยะไกล" -unsetUserAvatar: "เลิกตั้งอวตาร" -unsetUserAvatarConfirm: "ต้องการเลิกตั้งอวตารใข่ไหม?" -unsetUserBanner: "เลิกตั้งแบนเนอร์" -unsetUserBannerConfirm: "ต้องการเลิกตั้งแบนเนอร์?" deleteAllFiles: "ลบไฟล์ทั้งหมด" -deleteAllFilesConfirm: "ต้องการลบไฟล์ทั้งหมดใช่ไหม?" +deleteAllFilesConfirm: "นายแน่ใจแล้วหรอว่าต้องการที่จะลบไฟล์ทั้งหมด?" removeAllFollowing: "เลิกติดตามผู้ใช้ที่ติดตามทั้งหมด" -removeAllFollowingDescription: "จะเลิกติดตามทั้งหมดจาก {host} โปรดดำเนินการสิ่งนี้เมื่อเซิร์ฟเวอร์ดังกล่าวได้สูญหายตายจากไปแล้ว" +removeAllFollowingDescription: "การที่คุณดำเนินการนี้จะเลิกติดตามบัญชีทั้งหมดจาก {host} โปรดเรียกใช้คำสั่งสิ่งนี้หากต้องการยกเลิกอินสแตนซ์ เช่น ไม่มีอยู่แล้ว" userSuspended: "ผู้ใช้รายนี้ถูกระงับการใช้งาน" -userSilenced: "ผู้ใช้รายนี้ถูกปิดปากอยู่" +userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น" yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ" yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่" tokenRevoked: "โทเค็นไม่ถูกต้อง" -tokenRevokedDescription: "โทเค็นการเข้าสู่ระบบหมดอายุ กรุณาเข้าสู่ระบบใหม่อีกครั้ง" +tokenRevokedDescription: "โทเค็นนี้หมดอายุแล้วนะค่ะกรุณาเข้าสู่ระบบอีกครั้งนะ" accountDeleted: "ลบบัญชีแล้ว" -accountDeletedDescription: "บัญชีนี้ถูกลบแล้ว" +accountDeletedDescription: "บัญชีนี้ถูกลบไปแล้วนะ" menu: "เมนู" divider: "ตัวแบ่ง" addItem: "เพิ่มรายการ" rearrange: "จัดใหม่" relays: "รีเลย์" addRelay: "เพิ่มรีเลย์" -inboxUrl: "URL ของอินบ็อกซ์" +inboxUrl: "อินบ็อกซ์ URL" addedRelays: "เพิ่มรีเลย์แล้ว" serviceworkerInfo: "ต้องเปิดใช้งานสำหรับการแจ้งเตือนแบบพุช" deletedNote: "โน้ตที่ถูกลบ" @@ -639,38 +579,37 @@ enablePlayer: "เปิดเครื่องเล่นวิดีโอ" disablePlayer: "ปิดเครื่องเล่นวิดีโอ" expandTweet: "ขยายทวีต" themeEditor: "ตัวแก้ไขธีม" -description: "คำอธิบาย" +description: "รายละเอียด" describeFile: "เพิ่มแคปชั่น" enterFileDescription: "ใส่แคปชั่น" author: "ผู้เขียน" -leaveConfirm: "มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก ต้องการละทิ้งมันใช่ไหม?" +leaveConfirm: "คุณมีการเปลี่ยนแปลงที่ไม่ได้บันทึกนะ นายต้องการทิ้งการเปลี่ยนแปลงเหล่านั้นหรอ?" manage: "การจัดการ" plugins: "ปลั๊กอิน" -preferencesBackups: "สำรองการตั้งค่า" +preferencesBackups: "ตั้งค่าการสำรองข้อมูล" deck: "เด็ค" undeck: "ออกจากเด็ค" useBlurEffectForModal: "ใช้เอฟเฟกต์เบลอสำหรับโมดอล" -useFullReactionPicker: "ใช้ตัวจิ้มรีแอคชั่นอย่างเต็มรูปแบบ" +useFullReactionPicker: "ใช้เครื่องมือเลือกปฏิกิริยาขนาดเต็ม" width: "ความกว้าง" height: "ความสูง" large: "ใหญ่" medium: "ปานกลาง" small: "เล็ก" -generateAccessToken: "สร้างโทเค็นการเข้าถึง" -permission: "สิทธิ์" -adminPermission: "สิทธิ์ของผู้ดูแลระบบ" +generateAccessToken: "สร้างการเข้าถึงโทเค็น" +permission: "การอนุญาต" enableAll: "เปิดใช้งานทั้งหมด" disableAll: "ปิดการใช้งานทั้งหมด" tokenRequested: "ให้สิทธิ์การเข้าถึงบัญชี" -pluginTokenRequestedDescription: "ปลั๊กอินนี้จะใช้สิทธิ์ตามที่ตั้งค่าไว้ที่นี่" +pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ" notificationType: "ประเภทการแจ้งเตือน" edit: "แก้ไข" -emailServer: "เซิร์ฟเวอร์ของอีเมล" +emailServer: "อีเมล์เซิร์ฟเวอร์" enableEmail: "เปิดใช้งานการกระจายอีเมล" -emailConfigInfo: "ใช้สำหรับการยืนยันอีเมลหรือการรีเซ็ตรหัสผ่าน" -email: "อีเมล" -emailAddress: "ที่อยู่อีเมล" -smtpConfig: "ตั้งค่าเซิร์ฟเวอร์ SMTP" +emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน" +email: "อีเมล์" +emailAddress: "ที่อยู่อีเมล์" +smtpConfig: "กำหนดค่าเซิร์ฟเวอร์ SMTP" smtpHost: "โฮสต์" smtpPort: "พอร์ต" smtpUser: "ชื่อผู้ใช้" @@ -680,51 +619,49 @@ smtpSecure: "ใช้โดยนัย SSL/TLS สำหรับการเ smtpSecureInfo: "ปิดสิ่งนี้เมื่อใช้ STARTTLS" testEmail: "ทดสอบการส่งอีเมล" wordMute: "ปิดเสียงคำ" -hardWordMute: "ปิดเสียงคำแบบแข็งโป๊ก" -hardWordMuteDescription: "ซ่อนหมายเหตุที่มีวลีที่ระบุ ต่างจากการปิดเสียงคำ โน้ตต่างๆ จะถูกซ่อนไว้อย่างสมบูรณ์" -regexpError: "เกิดข้อผิดพลาดใน regular expression" -regexpErrorDescription: "เกิดข้อผิดพลาดใน regular expression บรรทัดที่ {line} ของการปิดเสียงคำ {tab} :" -instanceMute: "ปิดเสียงเซิร์ฟเวอร์" +regexpError: "ข้อผิดพลาดของนิพจน์ทั่วไป" +regexpErrorDescription: "เกิดข้อผิดพลาดในนิพจน์ทั่วไปในบรรทัดที่ {line} ของการปิดเสียงคำ {tab} ของคุณ:" +instanceMute: "ปิดเสียง อินสแตนซ์" userSaysSomething: "{name} พูดอะไรบางอย่าง" -userSaysSomethingAbout: "{name} พูดอะไรบางอย่างเกี่ยวกับ \"{word}\"" makeActive: "เปิดใช้งาน" display: "แสดงผล" copy: "คัดลอก" metrics: "เมตริก" overview: "ภาพรวม" -logs: "ปูม" +logs: "บันทึกข้อมูลระบบ" delayed: "ดีเลย์" database: "ฐานข้อมูล" -channel: "ช่อง" +channel: "แชนแนล" create: "สร้าง" notificationSetting: "ตั้งค่าการแจ้งเตือน" notificationSettingDesc: "เลือกประเภทการแจ้งเตือนที่ต้องการจะแสดง" useGlobalSetting: "ใช้การตั้งค่าส่วนกลาง" -useGlobalSettingDesc: "เมื่อเปิดใช้งาน ใช้การตั้งค่าการแจ้งเตือนจากบัญชีคุณ เมื่อปิดใช้งาน สามารถตั้งค่าได้อย่างอิสระ" +useGlobalSettingDesc: "หากเปิดไว้ ระบบจะใช้การตั้งค่าการแจ้งเตือนของบัญชีของคุณ หากปิดอยู่ สามารถทำการกำหนดค่าแต่ละรายการได้นะ" other: "อื่น ๆ" regenerateLoginToken: "สร้างโทเค็นการเข้าสู่ระบบอีกครั้ง" regenerateLoginTokenDescription: "สร้างโทเค็นใหม่ที่ใช้ภายในระหว่างการเข้าสู่ระบบ โดยตามหลักปกติแล้วการดำเนินการนี้ไม่จำเป็น หากสร้างใหม่ อุปกรณ์ทั้งหมดจะถูกออกจากระบบนะ" -theKeywordWhenSearchingForCustomEmoji: "คีย์เวิร์ดสำหรับใช้ค้นหาเอโมจิที่กำหนดเอง" setMultipleBySeparatingWithSpace: "คั่นหลายรายการด้วยช่องว่าง" -fileIdOrUrl: "ID ของไฟล์ หรือ URL" +fileIdOrUrl: "ไฟล์ ID หรือ URL" behavior: "พฤติกรรม" sample: "ตัวอย่าง" abuseReports: "รายงาน" reportAbuse: "รายงาน" -reportAbuseRenote: "รายงานรีโน้ต" -reportAbuseOf: "รายงาน {name}" +reportAbuseOf: "รายงาน {ชื่อ}" fillAbuseReportDescription: "กรุณากรอกรายละเอียดเกี่ยวกับรายงานนี้ หากเป็นเรื่องเกี่ยวกับโน้ตโดยเฉพาะ ได้โปรดระบุ URL" abuseReported: "เราได้ส่งรายงานของคุณไปแล้ว ขอบคุณมากๆนะ" -reporter: "ผู้รายงาน" -reporteeOrigin: "ปลายทางรายงาน" -reporterOrigin: "แหล่งผู้รายงาน" +reporter: "นักข่าว" +reporteeOrigin: "รายงานต้นทาง" +reporterOrigin: "นักข่าวต้นทาง" +forwardReport: "ส่งต่อรายงานไปยังอินสแตนซ์ระยะไกล" +forwardReportIsAnonymous: "แทนที่จะเป็นบัญชีของคุณ บัญชีระบบที่ไม่ระบุตัวตนจะแสดงเป็นนักข่าวที่อินสแตนซ์ระยะไกล" send: "ส่ง" +abuseMarkAsResolved: "ทำเครื่องหมายรายงานว่าแก้ไขแล้ว" openInNewTab: "เปิดในแท็บใหม่" openInSideView: "เปิดในมุมมองด้านข้าง" defaultNavigationBehaviour: "พฤติกรรมการนำทางที่เป็นค่าเริ่มต้น" editTheseSettingsMayBreakAccount: "การแก้ไขการตั้งค่าเหล่านี้อาจทำให้บัญชีของคุณเสียหายนะ" -instanceTicker: "ข้อมูลเซิร์ฟเวอร์ของโน้ต" -waitingFor: "กำลังรอ {x}" +instanceTicker: "ข้อมูลอินสแตนซ์ของบันทึกย่อ" +waitingFor: "กำลังรอคอย {x}" random: "สุ่มค่า" system: "ระบบ" switchUi: "สลับ UI" @@ -734,9 +671,8 @@ createNew: "สร้างใหม่" optional: "ไม่บังคับ" createNewClip: "สร้างคลิปใหม่" unclip: "ลบคลิป" -confirmToUnclipAlreadyClippedNote: "โน้ตนี้เป็นส่วนหนึ่งของคลิป “{name}” อยู่แล้ว ต้องการนำมันออกจากคลิปใช่ไหม?" +confirmToUnclipAlreadyClippedNote: "โน้ตนี้เป็นส่วนหนึ่งของคลิป \"{name}\" แล้ว คุณต้องการลบออกจากคลิปนี้แทนอย่างงั้นหรอ?" public: "สาธารณะ" -private: "ส่วนตัว" i18nInfo: "Misskey กำลังได้รับการแปลเป็นภาษาต่างๆ โดยอาสาสมัคร คุณสามารถช่วยเหลือได้ที่ {link}" manageAccessTokens: "การจัดการโทเค็นการเข้าถึง" accountInfo: "ข้อมูลบัญชี" @@ -747,8 +683,8 @@ repliedCount: "จำนวนของการตอบกลับที่ renotedCount: "จำนวนรีโน้ตที่ได้รับแล้ว" followingCount: "จำนวนบัญชีที่ติดตาม" followersCount: "จำนวนผู้ติดตาม" -sentReactionsCount: "จำนวนรีแอคชั่นที่ส่ง" -receivedReactionsCount: "จำนวนรีแอคชั่นที่ได้รับ" +sentReactionsCount: "จำนวนปฏิกิริยาที่ส่ง" +receivedReactionsCount: "จำนวนปฏิกิริยาที่ได้รับ" pollVotesCount: "จำนวนโหวตที่ส่งไป" pollVotedCount: "จำนวนโหวตที่ได้รับ" yes: "ใช่" @@ -756,107 +692,106 @@ no: "ไม่" driveFilesCount: "จำนวนไฟล์ไดรฟ์" driveUsage: "การใช้พื้นที่ไดรฟ์" noCrawle: "ปฏิเสธการจัดทำดัชนีของโปรแกรมรวบรวมข้อมูล" -noCrawleDescription: "ขอให้เครื่องมือค้นหาไม่จัดทำดัชนีหน้าโปรไฟล์ โน้ต หน้าเพจ ฯลฯ" -lockedAccountInfo: "แม้ว่าการอนุมัติการติดตามถูกเปิดใช้งานอยู่ทุกคนก็ยังคงสามารถเห็นโน้ตของคุณได้ เว้นแต่ว่าคุณจะเปลี่ยนการเปิดเผยโน้ตของคุณเป็น “เฉพาะผู้ติดตาม”" -alwaysMarkSensitive: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อนเป็นค่าเริ่มต้น" +noCrawleDescription: "ขอให้เครื่องมือค้นหาไม่จัดทำดัชนีหน้าโปรไฟล์ บันทึกย่อ หน้า ฯลฯ" +lockedAccountInfo: "เว้นแต่ว่าคุณจะต้องตั้งค่าการเปิดเผยโน้ตเป็น \"ผู้ติดตามเท่านั้น\" โน้ตย่อของคุณจะปรากฏแก่ทุกคน ถึงแม้ว่าคุณจะเป็นกำหนดให้ผู้ติดตามต้องได้รับการอนุมัติด้วยตนเองก็ตาม" +alwaysMarkSensitive: "ทำเครื่องหมายเป็น NSFW เป็นค่าเริ่มต้น" loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ" disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว" -highlightSensitiveMedia: "ไฮไลท์สื่อที่มีเนื้อหาละเอียดอ่อน" -verificationEmailSent: "ได้ส่งอีเมลยืนยันแล้ว กรุณาเข้าลิงก์ที่ระบุในอีเมลเพื่อทำการตั้งค่าให้เสร็จสิ้น" +verificationEmailSent: "ส่งอีเมลยืนยันแล้วนะ ได้โปรดกรุณาไปที่ลิงก์ที่รวมไว้เพื่อทำการตรวจสอบให้เสร็จสิ้น" notSet: "ไม่ได้ตั้งค่า" emailVerified: "อีเมลได้รับการยืนยันแล้ว" noteFavoritesCount: "จำนวนโน้ตที่ชื่นชอบ" -pageLikesCount: "จำนวนเพจที่ถูกใจ" +pageLikesCount: "จำนวนเพจที่ชอบ" pageLikedCount: "จำนวนการกดถูกใจเพจที่ได้รับแล้ว" contact: "ติดต่อ" useSystemFont: "ใช้ฟอนต์เริ่มต้นของระบบ" clips: "คลิป" experimentalFeatures: "ฟังก์ชั่นทดสอบ" experimental: "ทดลอง" -thisIsExperimentalFeature: "นี่เป็นฟีเจอร์ทดลอง ซึ่งอาจมีการเปลี่ยนแปลงการทำงาน และอาจไม่ทำงานตามที่ตั้งใจไว้" +thisIsExperimentalFeature: "นี่คือฟีเจอร์ทดลองนะค่ะ ฟังก์ชันการทำงานบางอย่างอาจเปลี่ยนแปลงได้ และอาจไม่ทำงานหรือไม่เสถียรตามที่ตั้งใจไว้นะ" developer: "สำหรับนักพัฒนา" -makeExplorable: "ทำให้บัญชีมองเห็นใน “สำรวจ”" -makeExplorableDescription: "ถ้าหากคุณปิดการทำงานนี้ บัญชีของคุณนั้นจะไม่แสดงในส่วน “สำรวจ”" +makeExplorable: "ทำให้บัญชีมองเห็นใน \"สำรวจ\"" +makeExplorableDescription: "ถ้าหากคุณปิดการทำงานนี้ บัญชีของคุณนั้นจะไม่แสดงในส่วน \"สำรวจ\" นะ" +showGapBetweenNotesInTimeline: "แสดงช่องว่างระหว่างโพสต์บนไทม์ไลน์" duplicate: "ทำซ้ำ" left: "ซ้าย" -center: "กึ่งกลาง" +center: "ศูนย์กลาง" wide: "กว้าง" narrow: "ชิด" -reloadToApplySetting: "การตั้งค่านี้จะมีผลหลังจากโหลดหน้าซ้ำเท่านั้น ต้องการที่จะโหลดใหม่เลยไหม?" -needReloadToApply: "ต้องรีโหลดเพื่อให้การเปลี่ยนแปลงมีผล" +reloadToApplySetting: "การตั้งค่านี้จะมีผลหลังจากโหลดหน้าซ้ำเท่านั้น ต้องการที่จะโหลดใหม่เลยมั้ย" +needReloadToApply: "จำเป็นต้องโหลดซ้ำถึงจะมีผลนะ" showTitlebar: "แสดงแถบชื่อ" clearCache: "ล้างแคช" -onlineUsersCount: "{n} รายกำลังออนไลน์" +onlineUsersCount: "{n} ผู้ใช้คนนี้กำลังออนไลน์" nUsers: "{n} ผู้ใช้งาน" nNotes: "{n} โน้ต" -sendErrorReports: "ส่งรายงานข้อผิดพลาด" -sendErrorReportsDescription: "เมื่อเปิดใช้งาน การแจ้งข้อผิดพลาดจะถูกแชร์กับ Misskey เมื่อเกิดปัญหา ซึ่งช่วยในการปรับปรุงคุณภาพของซอฟต์แวร์ ข้อมูลข้อผิดพลาดอาจรวมถึงเวอร์ชันของระบบปฏิบัติการ ประเภทของเบราว์เซอร์ และประวัติการใช้งาน ฯลฯ" +sendErrorReports: "ส่งรายงานว่าข้อผิดพลาด" +sendErrorReportsDescription: "เมื่อเปิดใช้งาน ข้อมูลข้อผิดพลาดโดยรายละเอียดนั้นจะถูกแชร์ให้กับ Misskey เมื่อเกิดปัญหา ซึ่งช่วยปรับปรุงคุณภาพของ Misskey\nซึ่งจะรวมถึงข้อมูล เช่น เวอร์ชั่นของระบบปฏิบัติการ เบราว์เซอร์ที่คุณใช้ กิจกรรมของคุณใน Misskey เป็นต้น" myTheme: "ธีมของฉัน" -backgroundColor: "สีพื้นหลัง" -accentColor: "สีหลัก" +backgroundColor: "ภาพพื้นหลัง" +accentColor: "รูปแบบสี" textColor: "สีข้อความ" saveAs: "บันทึกเป็น..." advanced: "ขั้นสูง" advancedSettings: "การตั้งค่าขั้นสูง" value: "ค่า" createdAt: "สร้างเมื่อ" -updatedAt: "อัปเดตล่าสุด" +updatedAt: "อัพเดทล่าสุด" saveConfirm: "บันทึกเปลี่ยนแปลงมั้ย?" -deleteConfirm: "ต้องการลบใช่ไหม?" +deleteConfirm: "ลบจริงๆเหรอ?" invalidValue: "ค่านี้ไม่ถูกต้อง" registry: "ทะเบียน" closeAccount: "ปิด บัญชี" currentVersion: "เวอร์ชั่นปัจจุบัน" -latestVersion: "เวอร์ชั่นล่าสุด" +latestVersion: "รุ่นปัจจุบัน" youAreRunningUpToDateClient: "คุณกำลังใช้ไคลเอ็นต์เวอร์ชันใหม่ล่าสุดนะ" newVersionOfClientAvailable: "มีไคลเอ็นต์เวอร์ชันใหม่กว่าของคุณพร้อมใช้งานนะ" usageAmount: "การใช้งาน" capacity: "ความจุ" inUse: "ใช้แล้ว" editCode: "แก้ไขโค้ด" -apply: "นำไปใช้" -receiveAnnouncementFromInstance: "รับการแจ้งเตือนจากเซิร์ฟเวอร์นี้" -emailNotification: "การแจ้งเตือนทางอีเมล" +apply: "ตกลง" +receiveAnnouncementFromInstance: "รับการแจ้งเตือนจากอินสแตนซ์นี้" +emailNotification: "การแจ้งเตือนทางอีเมล์" publish: "เผยแพร่" inChannelSearch: "ค้นหาในช่อง" -useReactionPickerForContextMenu: "คลิกขวาเพื่อเปิดตัวจิ้มรีแอคชั่น" -typingUsers: "{users} กำลังพิมพ์..." +useReactionPickerForContextMenu: "เปิดตัวเลือกปฏิกิริยาเมื่อคลิกขวา" +typingUsers: "{users} กำลัง/กำลังพิมพ์..." jumpToSpecifiedDate: "ข้ามไปยังวันที่เฉพาะเจาะจง" showingPastTimeline: "กำลังแสดงผลไทม์ไลน์เก่า" clear: "ล้าง" markAllAsRead: "ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว" goBack: "ย้อนกลับ" -unlikeConfirm: "ต้องการเลิกถูกใจใช่ไหม?" +unlikeConfirm: "ลบไลค์ของคุณออกจริงๆหรอ" fullView: "มุมมองแบบเต็ม" quitFullView: "ออกจากมุมมองแบบเต็ม" addDescription: "เพิ่มคำอธิบาย" -userPagePinTip: "ปักหมุดโน้ตให้แสดงที่นี่ได้โดยเลือกเมนู “ปักหมุด” ของโน้ตนั้นๆ" +userPagePinTip: "คุณสามารถแสดงผลโน้ตย่อได้ที่นี่โดยเลือก \"ปักหมุดที่โปรไฟล์\" จากเมนูของโน้ตย่อแต่ละรายการนะ" notSpecifiedMentionWarning: "โน้ตนี้มีการกล่าวถึงผู้ใช้งานที่ไม่รวมอยู่ในผู้รับ" info: "เกี่ยวกับ" userInfo: "ข้อมูลผู้ใช้" unknown: "ไม่ทราบสถานะ" onlineStatus: "สถานะออนไลน์" hideOnlineStatus: "ซ่อนสถานะออนไลน์" -hideOnlineStatusDescription: "การซ่อนสถานะออนไลน์อาจทำให้ฟังก์ชันบางอย่าง เช่น การค้นหา สะดวกน้อยลง" +hideOnlineStatusDescription: "การซ่อนสถานะออนไลน์ของคุณช่วยลดความสะดวกของคุณสมบัติบางอย่าง เช่น การค้นหา อ่ะนะ" online: "ออนไลน์" active: "ใช้งานอยู่" offline: "ออฟไลน์" -notRecommended: "ไม่แนะนำ" -botProtection: "การป้องกัน Bot" -instanceBlocking: "เซิร์ฟเวอร์ที่ถูกบล็อก/ปิดปาก" +notRecommended: "ไม่ใช้งาน" +botProtection: "การป้องกัน Bot (or AI)" +instanceBlocking: "อินสแตนซ์ที่ถูกบล็อก" selectAccount: "เลือกบัญชี" switchAccount: "สลับบัญชีผู้ใช้" enabled: "เปิดใช้งาน" disabled: "ปิดการใช้งาน" quickAction: "ปุ่มลัด" -user: "ผู้ใช้" +user: "ผู้ใช้งาน" administration: "การจัดการ" accounts: "บัญชีผู้ใช้" switch: "สลับ" -noMaintainerInformationWarning: "ยังไม่ได้ตั้งค่าข้อมูลของผู้ดูแลระบบ" -noInquiryUrlWarning: "ยังไม่ได้ตั้งค่า URL สำหรับการติดต่อสอบถาม" -noBotProtectionWarning: "ยังไม่ได้ตั้งค่าการป้องกันบอต" -configure: "ตั้งค่า" +noMaintainerInformationWarning: "ข้อมูลผู้ดูแลไม่ได้รับการกำหนดค่านะ" +noBotProtectionWarning: "ไม่ได้กำหนดค่าการป้องกันบอทนะ" +configure: "กำหนดค่า" postToGallery: "สร้างโพสต์แกลเลอรี่ใหม่" postToHashtag: "โพสต์ไปที่แฮชแท็กนี้" gallery: "แกลเลอรี่" @@ -865,19 +800,19 @@ popularPosts: "โพสต์ติดอันดับ" shareWithNote: "แบ่งปันด้วยโน้ต" ads: "โฆษณา" expiration: "กำหนดเวลา" -startingperiod: "เริ่มเมื่อ" -memo: "เมโม" +startingperiod: "เริ่ม" +memo: "ข้อควรจำ" priority: "ลำดับความสำคัญ" high: "สูง" middle: "ปานกลาง" low: "ต่ำ" -emailNotConfiguredWarning: "ยังไม่ได้ตั้งค่าที่อยู่อีเมล" +emailNotConfiguredWarning: "ไม่ได้ตั้งค่าที่อยู่อีเมลนะ" ratio: "อัตราส่วน" previewNoteText: "แสดงตัวอย่าง" customCss: "CSS ที่กำหนดเอง" -customCssWarn: "ควรใช้การตั้งค่านี้เฉพาะต่อเมื่อคุณรู้มันใช้ทำอะไร การตั้งค่าที่ไม่เหมาะสมอาจทำให้ไคลเอ็นต์ไม่สามารถใช้งานได้อย่างถูกต้อง" +customCssWarn: "ควรใช้การตั้งค่านี้เฉพาะต่อเมื่อคุณรู้ว่าการตั้งค่านี้ใช้ทำอะไร การป้อนค่าที่ไม่เหมาะสมอาจทำให้ไคลเอ็นต์หยุดทำงานตามปกติได้นะ" global: "ทั่วโลก" -squareAvatars: "แสดงผลอวตารเป็นสี่เหลี่ยม" +squareAvatars: "แสดงผลอวตารสี่เหลี่ยม" sent: "ส่ง" received: "ได้รับแล้ว" searchResult: "ผลการค้นหา" @@ -893,11 +828,11 @@ accountDeletionInProgress: "กำลังดำเนินการลบบ usernameInfo: "ชื่อที่ระบุบัญชีของคุณจากผู้อื่นในเซิร์ฟเวอร์นี้ คุณสามารถใช้ตัวอักษร (a~z, A~Z), ตัวเลข (0~9) หรือขีดล่าง (_) ชื่อผู้ใช้ไม่สามารถเปลี่ยนแปลงได้ในภายหลัง" aiChanMode: "โหมด Ai " devMode: "โหมดนักพัฒนา" -keepCw: "คงการเตือนเนื้อหาไว้" -pubSub: "บัญชี Pub/Sub" +keepCw: "เก็บคำเตือนเนื้อหา" +pubSub: "บัญชีผับ/ย่อย" lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด" resolved: "คลี่คลายแล้ว" -unresolved: "ยังไม่ได้รับการแก้ไข" +unresolved: "รอการเฉลย" breakFollow: "ลบผู้ติดตาม" breakFollowConfirm: "ลบผู้ติดตามนี้ออกจริงหรอ?" itsOn: "เปิดใช้งาน" @@ -905,39 +840,38 @@ itsOff: "ปิดใช้งาน" on: "เปิด" off: "ปิด" emailRequiredForSignup: "จำเป็นต้องการใช้ที่อยู่อีเมลสำหรับการสมัคร" -unread: "ยังไม่ได้อ่าน" +unread: "ไม่ได้อ่าน" filter: "กรอง" controlPanel: "แผงควบคุม" manageAccounts: "จัดการบัญชี" -makeReactionsPublic: "ตั้งค่าประวัติการรีแอคชั่นเป็นสาธารณะ" -makeReactionsPublicDescription: "การทำเช่นนี้จะทำให้รายการรีแอคชั่นของคุณที่ผ่านมาทั้งหมดปรากฏต่อสาธารณะ" +makeReactionsPublic: "ตั้งค่าประวัติปฏิกิริยาต่อสาธารณะ" +makeReactionsPublicDescription: "การทำเช่นนี้จะทำให้รายการปฏิกิริยาที่ผ่านมาของคุณจะปรากฏต่อสาธารณะนะ" classic: "คลาสสิค" muteThread: "ปิดเสียงเธรด" -unmuteThread: "เลิกปิดเสียงเธรด" -followingVisibility: "การมองเห็นที่เรากำลังติดตาม" -followersVisibility: "การมองเห็นผู้ที่กำลังติดตามเรา" +unmuteThread: "เปิดเสียงเธรด" +ffVisibility: "การมองเห็นผู้ติดตาม/ผู้ติดตาม" +ffVisibilityDescription: "ช่วยให้คุณสามารถกำหนดค่าได้ว่าใครสามารถดูได้ว่าคุณติดตามใครและใครติดตามคุณบ้าง" continueThread: "ดูความต่อเนื่องเธรด" deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?" incorrectPassword: "รหัสผ่านไม่ถูกต้อง" -incorrectTotp: "รหัสยืนยันตัวตนแบบใช้ครั้งเดียวที่ท่านได้ระบุมานั้น ไม่ถูกต้องหรือหมดอายุลงแล้วค่ะ" -voteConfirm: "ต้องการโหวต “{choice}” ใช่ไหม?" +voteConfirm: "ยืนยันการโหวต \"{choice}\" มั้ย?" hide: "ซ่อน" -useDrawerReactionPickerForMobile: "แสดง ตัวจิ้มรีแอคชั่น เป็นแบบลิ้นชัก เมื่อใช้บนมือถือ" -welcomeBackWithName: "ยินดีต้อนรับการกลับมานะคะ, คุณ{name}" -clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์" +useDrawerReactionPickerForMobile: "แสดงผล ตัวเลือกปฏิกิริยาเป็นลิ้นชักบนมือถือ" +welcomeBackWithName: "ยินดีต้อนรับการกลับมานะคะ, {name}" +clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์นะ" overridedDeviceKind: "ประเภทอุปกรณ์" smartphone: "สมาร์ทโฟน" tablet: "แท็บเล็ต" auto: "อัตโนมัติ" -themeColor: "สีธีม" +themeColor: "อินสแตนซ์ Ticker Color" size: "ขนาด" numberOfColumn: "จำนวนคอลัมน์" searchByGoogle: "ค้นหา" -instanceDefaultLightTheme: "ธีมสว่างตามค่าเริ่มต้นของเซิร์ฟเวอร์" -instanceDefaultDarkTheme: "ธีมมืดตามค่าเริ่มต้นของเซิร์ฟเวอร์" +instanceDefaultLightTheme: "ธีมสว่างค่าเริ่มต้นสำหรับอินสแตนซ์" +instanceDefaultDarkTheme: "ธีมมืดค่าเริ่มต้นอินสแตนซ์" instanceDefaultThemeDescription: "ป้อนรหัสธีมในรูปแบบออบเจ็กต์" mutePeriod: "ระยะเวลาปิดเสียง" -period: "ระยะเวลา" +period: "สิ้นสุดการสำรวจความคิดเห็น" indefinitely: "ตลอดไป" tenMinutes: "10 นาที" oneHour: "1 ชั่วโมง" @@ -954,7 +888,7 @@ cropNo: "ใช้ตามที่เป็นอยู่" file: "ไฟล์" recentNHours: "ล่าสุด {n} ชั่วโมงที่แล้ว" recentNDays: "ล่าสุด {n} วันที่แล้ว" -noEmailServerWarning: "ยังไม่ได้ตั้งค่าเซิร์ฟเวอร์ของอีเมล" +noEmailServerWarning: "ไม่ได้กำหนดค่าเซิร์ฟเวอร์อีเมลนี้" thereIsUnresolvedAbuseReportWarning: "มีรายงานที่ยังไม่ได้แก้ไข" recommended: "แนะนำ" check: "ตรวจสอบ" @@ -967,29 +901,29 @@ deleteAccount: "ลบบัญชี" document: "เอกสาร" numberOfPageCache: "จำนวนหน้าเพจที่แคช" numberOfPageCacheDescription: "การเพิ่มจำนวนนี้จะช่วยเพิ่มความสะดวกให้กับผู้ใช้งาน แต่จะทำให้เซิร์ฟเวอร์โหลดมากขึ้นและต้องใช้หน่วยความจำมากขึ้นอีกด้วย" -logoutConfirm: "ต้องการออกจากระบบใช่ไหม?" -lastActiveDate: "ใช้งานล่าสุดเมื่อ" -statusbar: "แถบสถานะ" +logoutConfirm: "คุณแน่ใจว่าต้องการออกจากระบบ?" +lastActiveDate: "ใช้งานล่าสุดที่" +statusbar: "ไอคอนบนแถบสถานะ" pleaseSelect: "ตัวเลือก" -reverse: "พลิก" +reverse: "ย้อนกลับ" colored: "สี" -refreshInterval: "ความถี่ในการอัปเดต" +refreshInterval: "รอบการอัพเดต" label: "ป้ายชื่อ" type: "รูปแบบ" speed: "ความเร็ว" slow: "ช้า" fast: "เร็ว" -sensitiveMediaDetection: "การตรวจจับสื่อที่มีเนื้อหาละเอียดอ่อน" +sensitiveMediaDetection: "การตรวจจับของสื่อ NSFW" localOnly: "เฉพาะท้องถิ่น" -remoteOnly: "ระยะไกลเท่านั้น" +remoteOnly: "รีโมทเท่านั้น" failedToUpload: "การอัปโหลดล้มเหลว" cannotUploadBecauseInappropriate: "ไม่สามารถอัปโหลดไฟล์นี้ได้เนื่องจากระบบตรวจพบบางส่วนของไฟล์ว่านี้อาจจะเป็น NSFW" -cannotUploadBecauseNoFreeSpace: "ไม่สามารถอัปโหลดได้เนื่องจากไม่มีพื้นที่ว่างในไดรฟ์เหลือแล้ว" +cannotUploadBecauseNoFreeSpace: "การอัปโหลดนั้นล้มเหลวเนื่องจากไม่มีความจุของไดรฟ์" cannotUploadBecauseExceedsFileSizeLimit: "ไม่สามารถอัปโหลดไฟล์นี้ได้แล้วเนื่องจากเกินขีดจำกัดของขนาดไฟล์แล้ว" beta: "เบต้า" -enableAutoSensitive: "ทำเครื่องหมายว่ามีเนื้อหาที่ละเอียดอ่อนโดยอัตโนมัติ" -enableAutoSensitiveDescription: "อนุญาตให้ตรวจหาและทำเครื่องหมายสื่อว่ามีเนื้อหาโดยละเอียดอ่อนโดยอัตโนมัติ ผ่าน Machine Learning หากเป็นไปได้ แม้ว่าคุณจะปิดคุณสมบัตินี้ ก็อาจถูกตั้งค่าโดยอัตโนมัติ ทั้งนี้ขึ้นอยู่กับเซิร์ฟเวอร์" -activeEmailValidationDescription: "การตรวจสอบอีเมลของผู้ใช้จะเข้มงวดมากขึ้น โดยพิจารณาว่าเป็นอีเมลชั่วคราวหรือไม่ และสามารถติดต่อได้จริงหรือไม่ หากปิดการตรวจสอบนี้ จะตรวจสอบเพียงว่ารูปแบบอีเมลที่ถูกต้องหรือไม่เท่านั้น" +enableAutoSensitive: "ทำเครื่องหมาย NSFW อัตโนมัติ" +enableAutoSensitiveDescription: "อนุญาตให้ตรวจหาและทำเครื่องหมายสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่องหากเป็นไปได้ แม้ว่าตัวเลือกนี้จะถูกปิดใช้งาน แต่ก็สามารถเปิดใช้งานได้ทั้งอินสแตนซ์นี้" +activeEmailValidationDescription: "เปิดใช้งานการตรวจสอบที่อยู่อีเมลให้มีความเข้มงวดยิ่งขึ้น ซึ่งอาจจะรวมไปถึงการตรวจสอบที่อยู่อีเมล์ที่ใช้แล้วทิ้งและโดยให้พิจารณาว่าสามารถสื่อสารด้วยได้หรือไม่ เมื่อไม่เลือกระบบจะตรวจสอบเฉพาะรูปแบบของอีเมลเท่านั้น" navbar: "แถบนำทาง" shuffle: "สลับ" account: "บัญชีผู้ใช้" @@ -998,36 +932,34 @@ pushNotification: "การแจ้งเตือนแบบพุช" subscribePushNotification: "เปิดการแจ้งเตือนแบบพุช" unsubscribePushNotification: "ปิดการแจ้งเตือนแบบพุช" pushNotificationAlreadySubscribed: "การแจ้งเตือนแบบพุชได้เปิดใช้งานแล้ว" -pushNotificationNotSupported: "เบราว์เซอร์หรือเซิร์ฟเวอร์ไม่รองรับการแจ้งเตือนแบบพุช" +pushNotificationNotSupported: "เบราว์เซอร์หรืออินสแตนซ์ของคุณนั้นไม่รองรับการแจ้งเตือนแบบพุช" sendPushNotificationReadMessage: "ลบการแจ้งเตือนแบบพุชเมื่ออ่านการแจ้งเตือนหรือข้อความที่เกี่ยวข้องแล้ว" -sendPushNotificationReadMessageCaption: "อาจทำให้อุปกรณ์ของคุณใช้พลังงานมากขึ้น" -windowMaximize: "ขยายใหญ่สุด" +sendPushNotificationReadMessageCaption: "การแจ้งเตือนที่มีข้อความ \"{emptyPushNotificationMessage}\" จะแสดงขึ้นมาในช่วงระยะเวลาสั้นๆ การดำเนินการนี้อาจทำให้เพิ่มการใช้งานแบตเตอรี่ของอุปกรณ์ถ้าหากมีนะ" +windowMaximize: "ขยายใหญ่สุดแล้ว" windowMinimize: "ย่อเล็กที่สุด" windowRestore: "เลิกทำ" -caption: "คำอธิบาย" +caption: "รายละเอียด" loggedInAsBot: "ล็อกอินเป็นบอตอยู่ในขณะนี้" tools: "เครื่องมือ" cannotLoad: "ไม่สามารถโหลดได้" numberOfProfileView: "มุมมองโปรไฟล์" -like: "ถูกใจ!" -unlike: "เลิกถูกใจ" -numberOfLikes: "จำนวนยอดถูกใจ" +like: "ชื่นชอบ" +unlike: "ไม่ชอบ" +numberOfLikes: "จำนวนไลค์" show: "แสดงผล" neverShow: "ไม่ต้องแสดงข้อความนี้อีก" remindMeLater: "ไว้ครั้งหน้าแล้วกัน" -didYouLikeMisskey: "คุณชอบ Misskey ไหม?" -pleaseDonate: "Misskey เป็นซอฟต์แวร์ฟรีที่ใช้งานโดย {host} เราขอขอบคุณการสนับสนุนของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้!" -correspondingSourceIsAvailable: "ซอร์สโค้ดที่เกี่ยวข้องมีอยู่ที่ {anchor}" +didYouLikeMisskey: "คุณเคยชอบ Misskey ไหม?" +pleaseDonate: "{host} ใช้ซอฟต์แวร์ฟรี Misskey เราขอขอบคุณการบริจาคของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้นะ!" roles: "บทบาท" role: "บทบาท" noRole: "ไม่พบบทบาท" normalUser: "ผู้ใช้มาตรฐาน" undefined: "ไม่ได้กำหนด" -assign: "มอบหมาย" -unassign: "เลิกมอบหมาย" +assign: "กำหนด" +unassign: "ยังไม่มอบหมาย" color: "สี" -manageCustomEmojis: "จัดการเอโมจิที่กำหนดเอง" -manageAvatarDecorations: "จัดการตกแต่งอวตาร" +manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" youCannotCreateAnymore: "คุณถึงขีดจํากัดการสร้างแล้วนะ" cannotPerformTemporary: "ไม่สามารถใช้การได้ชั่วคราว" cannotPerformTemporaryDescription: "ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้ง" @@ -1041,39 +973,33 @@ achievements: "ความสำเร็จ" gotInvalidResponseError: "การตอบสนองเซิร์ฟเวอร์ไม่ถูกต้อง" gotInvalidResponseErrorDescription: "เซิร์ฟเวอร์อาจไม่สามารถเข้าถึงได้หรืออาจจะกำลังอยู่ในระหว่างปรับปรุง กรุณาลองใหม่อีกครั้งในภายหลังนะคะ" thisPostMayBeAnnoying: "โน้ตนี้อาจจะเป็นการรบกวนผู้อื่นนะคะ" -thisPostMayBeAnnoyingHome: "โพสต์ลงไทม์ไลน์หลักเท่านั้น" -thisPostMayBeAnnoyingCancel: "ยกเลิก" -thisPostMayBeAnnoyingIgnore: "โพสต์ไปเลย ไม่ต้องปรับการมองเห็น" -collapseRenotes: "ยุบรีโน้ตที่คุณเคยเห็นแล้ว" -collapseRenotesDescription: "พับย่อโน้ตที่เคยตอบสนองหรือรีโน้ตแล้ว" +thisPostMayBeAnnoyingHome: "โพสต์ไปยังบ้านไทม์ไลน์" +thisPostMayBeAnnoyingCancel: "เลิก" +thisPostMayBeAnnoyingIgnore: "โพสต์ยังไงก็แล้วแต่" +collapseRenotes: "ยุบ renotes ที่คุณได้เห็นแล้ว" internalServerError: "เซิร์ฟเวอร์ภายในเกิดข้อผิดพลาด" -internalServerErrorDescription: "เกิดข้อผิดพลาดที่ไม่คาดคิดภายในเซิร์ฟเวอร์" +internalServerErrorDescription: "เซิร์ฟเวอร์รันค้นพบข้อผิดพลาดที่ไม่คาดคิด" copyErrorInfo: "คัดลอกรายละเอียดข้อผิดพลาด" -joinThisServer: "ลงทะเบียนในเซิร์ฟเวอร์นี้" -exploreOtherServers: "มองหาเซิร์ฟเวอร์อื่น" -letsLookAtTimeline: "มาดูไทม์ไลน์กัน" -disableFederationConfirm: "ปิดใช้งานสหพันธ์เลยใช่ไหม?" +joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้" +exploreOtherServers: "มองหาอินสแตนซ์อื่น" +letsLookAtTimeline: "ลองดูที่ไทม์ไลน์" +disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?" disableFederationConfirmWarn: "โพสต์จะยังคงเป็นสาธารณะต่อไป เว้นแต่จะตั้งค่าเป็นอย่างอื่น" disableFederationOk: "ปิดการใช้งาน" -invitationRequiredToRegister: "เซิร์ฟเวอร์นี้เป็นแบบรับเชิญ เฉพาะผู้มีรหัสเชิญเท่านั้นถึงสามารถลงทะเบียนได้" -emailNotSupported: "เซิร์ฟเวอร์นี้ไม่รองรับการส่งอีเมล" +invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญ เพื่องลงทะเบียนเข้าใช้งาน" +emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมล" postToTheChannel: "โพสต์ลงช่อง" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ" reactionAcceptance: "การยอมรับรีแอคชั่น" -likeOnly: "ที่ถูกใจเท่านั้น" -likeOnlyForRemote: "ทั้งหมด (เฉพาะการถูกใจจากเซิร์ฟเวอร์ระยะไกล)" -nonSensitiveOnly: "เฉพาะไม่มีเนื้อหาละเอียดอ่อน" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "เฉพาะไม่มีเนื้อหาละเอียดอ่อน (เฉพาะการถูกใจจากระยะไกลเท่านั้น)" +likeOnly: "ที่ชอบเท่านั้น" +likeOnlyForRemote: "ไลค์สำหรับอินสแตนซ์ระยะไกลเท่านั้น" +nonSensitiveOnly: "ไม่มีความอ่อนไหวเท่านั้น" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "ไม่มีความอ่อนไหวเท่านั้น (เฉพาะไลค์จากระยะไกลเท่านั้น)" rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน" -resetPasswordConfirm: "ต้องการรีเซ็ตรหัสผ่านใช่ไหม?" -sensitiveWords: "คำที่มีเนื้อหาละเอียดอ่อน" -sensitiveWordsDescription: "โน้ตที่มีคำที่ระบุไว้จะถูกตั้งค่าการมองเห็นของให้แสดงเฉพาะในหน้าหลักเท่านั้น คั่นคำด้วยการขึ้นบรรทัดใหม่" -sensitiveWordsDescription2: "ถ้าแยกด้วยเว้นวรรคจะเป็นการระบุ AND และถ้าล้อมคำด้วยสแลช (/) จะเป็นการใช้ regular expression" -prohibitedWords: "คำต้องห้าม" -prohibitedWordsDescription: "จะแจ้งเตือนว่าเกิดข้อผิดพลาดเมื่อพยายามโพสต์โน้ตที่มีคำที่กำหนดไว้ สามารถตั้งได้หลายคำด้วยการขึ้นบรรทัดใหม่" -prohibitedWordsDescription2: "ถ้าแยกด้วยเว้นวรรคจะเป็นการระบุ AND และถ้าล้อมคำด้วยสแลช (/) จะเป็นการใช้ regular expression" -hiddenTags: "แฮชแท็กที่ซ่อนอยู่" -hiddenTagsDescription: "เลือกแท็กที่จะไม่แสดงในรายการเทรนด์ สามารถลงทะเบียนหลายแท็กได้โดยขึ้นบรรทัดใหม่" +resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?" +sensitiveWords: "คำที่ละเอียดอ่อน" +sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ" +sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ" notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งาน" license: "ใบอนุญาต" unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?" @@ -1083,426 +1009,122 @@ retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหม retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?" retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ" enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล" -enableChartsForFederatedInstances: "สร้างแผนภูมิของเซิร์ฟเวอร์ระยะไกล" -enableStatsForFederatedInstances: "ดึงข้อมูลสถิติจากเซิร์ฟเวอร์ที่อยู่ห่างไกล" -showClipButtonInNoteFooter: "เพิ่ม “คลิป” ไปยังเมนูสั่งการของโน้ต" -reactionsDisplaySize: "ขนาดของรีแอคชั่น" -limitWidthOfReaction: "จำกัดความกว้างสูงสุดของรีแอคชั่นและแสดงให้เล็กลง" -noteIdOrUrl: "ID ของโน้ต หรือ URL" +enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล" +showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน" +largeNoteReactions: "ขยายรีแอคชั่นการแสดงผล" +noteIdOrUrl: "โน้ต ID หรือ URL" video: "วีดีโอ" videos: "วีดีโอ" -audio: "เสียง" -audioFiles: "เสียง" dataSaver: "ประหยัดข้อมูล" -accountMigration: "โยกย้ายบัญชี" +accountMigration: "การโยกย้ายบัญชี" accountMoved: "ผู้ใช้รายนี้ได้ย้ายไปยังบัญชีใหม่แล้ว:" accountMovedShort: "บัญชีนี้ถูกโอนย้ายไปแล้วค่ะ" -operationForbidden: "การดำเนินการถูกห้าม" +operationForbidden: "ห้ามดำเนินการ" forceShowAds: "แสดงโฆษณาเสมอ" -addMemo: "เพิ่มเมโม" -editMemo: "แก้ไขเมโม" -reactionsList: "รายการรีแอคชั่น" -renotesList: "รายการรีโน้ต" -notificationDisplay: "การแสดงการแจ้งเตือน" +addMemo: "เพิ่มมีโม" +editMemo: "แก้ไขมีโม" +reactionsList: "ปฏิกิริยา" +renotesList: "Renotes รีโน้ต" +notificationDisplay: "การแจ้งเตือน" leftTop: "บนซ้าย" rightTop: "บนขวา" leftBottom: "ล่างซ้าย" rightBottom: "ล่างขวา" stackAxis: "ทิศทางการซ้อน" vertical: "แนวตั้ง" -horizontal: "แนวนอน" +horizontal: "ด้านข้าง" position: "ตำแหน่ง" -serverRules: "กฎของเซิร์ฟเวอร์" -pleaseConfirmBelowBeforeSignup: "หากต้องการลงทะเบียนในเซิร์ฟเวอร์นี้ คุณต้องตรวจสอบและยอมรับสิ่งต่อไปนี้" +serverRules: "กฎของเซิฟเวอร์" +pleaseConfirmBelowBeforeSignup: "โปรดยืนยันที่ด้านล่างก่อนสมัครใช้งาน" pleaseAgreeAllToContinue: "คุณต้องยอมรับทุกช่องตรงด้านบนเพื่อดำเนินการต่อค่ะ" continue: "ดำเนินการต่อ" preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้" -preservedUsernamesDescription: "ระบุชื่อผู้ใช้ที่จะสงวนชื่อไว้ คั่นด้วยการขึ้นบรรทัดใหม่ ชื่อผู้ใช้ที่ระบุที่นี่จะไม่สามารถใช้งานได้อีกต่อไปเมื่อสร้างบัญชีใหม่ ยกเว้นเมื่อผู้ดูแลระบบสร้างบัญชี นอกจากนี้ บัญชีที่มีอยู่แล้วจะไม่ได้รับผลกระทบ" +preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ที่จะสำรองโดยคั่นด้วยการแบ่งบรรทัดนั้น เพราะสิ่งเหล่านี้จะไม่สามารถทำได้ในระหว่างการสร้างบัญชีตามปกติ บัญชีที่มีอยู่แล้วนั้นโดยใช้ชื่อผู้ใช้เหล่านี้จะไม่ได้รับผลกระทบอะไร" createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้" archive: "เก็บถาวร" -archived: "เก็บถาวรแล้ว" -unarchive: "เลิกการเก็บถาวร" -channelArchiveConfirmTitle: "ต้องการเก็บถาวรเจ้า {name} ใช่ไหม?" -channelArchiveConfirmDescription: "เมื่อเก็บถาวรแล้ว จะไม่ปรากฏในรายการช่องหรือผลการค้นหาอีกต่อไป และจะไม่สามารถโพสต์ใหม่ได้อีกต่อไป" +channelArchiveConfirmTitle: "เก็บถาวรจริงๆ {name} มั้ย?" thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ" displayOfNote: "การแสดงโน้ต" initialAccountSetting: "ตั้งค่าโปรไฟล์" youFollowing: "ติดตามแล้ว" -preventAiLearning: "ปฏิเสธการเรียนรู้ด้วย generative AI" -preventAiLearningDescription: "ส่งคำร้องขอไม่ให้ใช้ ข้อความในโน้ตที่โพสต์, หรือเนื้อหารูปภาพ ฯลฯ ในการเรียนรู้ของเครื่อง(machine learning) / Predictive AI / Generative AI โดยการเพิ่มแฟล็ก “noai” ลง HTML-Response ให้กับเนื้อหาที่เกี่ยวข้อง แต่ทั้งนี้ ไม่ได้ป้องกัน AI จากการเรียนรู้ได้อย่างสมบูรณ์ เนื่องจากมี AI บางตัวเท่านั้นที่จะเคารพคำขอดังกล่าว" +preventAiLearning: "ปฏิเสธการใช้งาน ในการเรียนรู้ของเครื่อง (Generative AI)" options: "ตัวเลือกบทบาท" specifyUser: "ผู้ใช้เฉพาะ" -lookupConfirm: "ต้องการเรียกดูข้อมูลใช่ไหม?" -openTagPageConfirm: "ต้องการเปิดหน้าแฮชแท็กใช่ไหม?" -specifyHost: "ระบุโฮสต์" failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้" update: "อัปเดต" -rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้เอโมจินี้เป็นรีแอคชั่นได้" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ถ้าหากไม่ได้ระบุบทบาท ใคร ๆ ก็สามารถใช้เอโมจินี้เพื่อรีแอคชั่นได้" +rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้อิโมจินี้เป็นรีแอคชั่นได้" rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "บทบาทเหล่านี้ต้องเป็นสาธารณะ" -cancelReactionConfirm: "ต้องการลบรีแอคชั่นใช่ไหม?" -changeReactionConfirm: "ต้องการเปลี่ยนรีแอคชั่นใช่ไหม?" +cancelReactionConfirm: "ต้องการลบรีแอคชั่นของคุณจริงๆหรอ?" +changeReactionConfirm: "ต้องการเปลี่ยนรีแอคชั่นของคุณจริงๆหรอ?" later: "ไว้ทีหลัง" goToMisskey: "ถึง Misskey" -additionalEmojiDictionary: "พจนานุกรมเอโมจิเพิ่มเติม" +additionalEmojiDictionary: "พจนานุกรมอีโมจิเพิ่มเติม" installed: "ติดตั้งแล้ว" branding: "แบรนดิ้ง" enableServerMachineStats: "เผยแพร่สถานะฮาร์ดแวร์ของเซิร์ฟเวอร์" enableIdenticonGeneration: "เปิดใช้งานผู้ใช้สร้างตัวระบุ" turnOffToImprovePerformance: "การปิดส่วนนี้สามารถเพิ่มประสิทธิภาพได้" -createInviteCode: "สร้างรหัสเชิญ" -createWithOptions: "สร้างด้วยตัวเลือก" -createCount: "จำนวนรหัสเชิญ" -inviteCodeCreated: "สร้างรหัสเชิญแล้ว" -inviteLimitExceeded: "จำนวนรหัสเชิญที่สามารถสร้างได้ถึงขีดจำกัดแล้ว" -createLimitRemaining: "รหัสเชิญที่สามารถสร้างได้: เหลืออยู่ {limit} รหัส" -inviteLimitResetCycle: "สามารถสร้างรหัสเชิญได้อีกสูงสุด {limit} รหัส ภายใน {time}" -expirationDate: "วันที่หมดอายุ" -noExpirationDate: "ไม่มีหมดอายุ" -inviteCodeUsedAt: "วันเวลาที่ใช้รหัสเชิญ" -registeredUserUsingInviteCode: "ผู้ใช้ที่ใช้รหัสเชิญ" -waitingForMailAuth: "กำลังรอการยืนยันอีเมล" -inviteCodeCreator: "ผู้ใช้ที่สร้างรหัสเชิญ" -usedAt: "วันเวลาที่ถูกใช้" -unused: "ยังไม่ได้ใช้" -used: "ถูกใช้แล้ว" -expired: "หมดอายุแล้ว" -doYouAgree: "ยอมรับไหม?" -beSureToReadThisAsItIsImportant: "กรุณาอ่านข้อมูลที่สำคัญอันนี้" -iHaveReadXCarefullyAndAgree: "ฉันได้อ่านและยินยอมเนื้อหาของ “{x}”" -dialog: "ไดอะล็อก" -icon: "ไอคอน" -forYou: "สำหรับคุณ" -currentAnnouncements: "ประกาศในปัจจุบัน" -pastAnnouncements: "ประกาศที่ผ่านมา" -youHaveUnreadAnnouncements: "มีการประกาศที่ยังไม่ได้อ่าน" -useSecurityKey: "โปรดปฏิบัติตามคำแนะนำของเบราว์เซอร์หรืออุปกรณ์ของคุณเพื่อใช้ security key หรือ passkey" -replies: "ตอบกลับ" -renotes: "รีโน้ต" -loadReplies: "แสดงการตอบกลับ" -loadConversation: "แสดงบทสนทนา" -pinnedList: "รายชื่อที่ปักหมุดไว้" -keepScreenOn: "เปิดหน้าจออุปกรณ์ค้างไว้" -verifiedLink: "ความเป็นเจ้าของลิงก์ได้รับการยืนยันแล้ว" -notifyNotes: "แจ้งเตือนเกี่ยวกับโพสต์ใหม่" -unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่" -authentication: "การตรวจสอบสิทธิ์" -authenticationRequiredToContinue: "กรุณายืนยันตัวตนทางอิเล็กทรอนิกส์เพื่อดำเนินการต่อ" -dateAndTime: "วันเวลา" -showRenotes: "แสดงรีโน้ต" -edited: "แก้ไขแล้ว" -notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน" -mutualFollow: "ติดตามซึ่งกันและกัน" -followingOrFollower: "กำลังติดตามหรือผู้ติดตาม" -fileAttachedOnly: "เฉพาะโน้ตที่มีไฟล์เท่านั้น" -showRepliesToOthersInTimeline: "แสดงการตอบกลับผู้อื่นลงในไทม์ไลน์" -hideRepliesToOthersInTimeline: "ไม่แสดงการตอบกลับผู้อื่นลงในไทม์ไลน์" -showRepliesToOthersInTimelineAll: "รวมตอบกลับจากทุกคนที่คุณติดตามไว้ในไทม์ไลน์ของคุณ" -hideRepliesToOthersInTimelineAll: "ซ่อนตอบกลับจากทุกคนที่คุณติดตามไปจากไทม์ไลน์ของคุณ" -confirmShowRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการแสดงการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ ใส่ลงไทม์ไลน์ใช่ไหม?" -confirmHideRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการซ่อนการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ ไปจากไทม์ไลน์ใช่ไหม?" -externalServices: "บริการภายนอก" -sourceCode: "ซอร์สโค้ด" -sourceCodeIsNotYetProvided: "ซอร์สโค้ดยังไม่พร้อมใช้งาน โปรดติดต่อผู้ดูแลระบบเพื่อแก้ไขปัญหานี้" -repositoryUrl: "URL ของ repository" -repositoryUrlDescription: "หากมีที่เก็บซอร์สโค้ดที่เปิดเผยต่อสาธารณะ ให้ป้อน URL ที่เก็บซอร์สโค้ดนั้น แต่หากคุณใช้ Misskey ตามต้นฉบับ (ไม่มีการเปลี่ยนแปลงซอร์สโค้ด) ให้ป้อน https://github.com/misskey-dev/misskey" -repositoryUrlOrTarballRequired: "หากคุณไม่มี repository สาธารณะ คุณจะต้องจัดเตรียม tarball แทน ดู .config/example.yml สำหรับรายละเอียด" -feedback: "ฟีดแบ็ก" -feedbackUrl: "URLของฟีดแบ็ก" -impressum: "อิมเพรสชั่น" -impressumUrl: "URL อิมเพรสชั่น" -impressumDescription: "การติดป้ายกำกับ (Impressum) มีผลบังคับใช้ในบางประเทศและภูมิภาค เช่น ประเทศเยอรมนี" -privacyPolicy: "นโยบายความเป็นส่วนตัว" -privacyPolicyUrl: "URL นโยบายความเป็นส่วนตัว" -tosAndPrivacyPolicy: "เงื่อนไขในการให้บริการและนโยบายความเป็นส่วนตัว" -avatarDecorations: "การตกแต่งอวตาร" -attach: "แนบ" -detach: "นำออก" -detachAll: "เอาออกทั้งหมด" -angle: "แองเกิล" -flip: "พลิก" -showAvatarDecorations: "แสดงตกแต่งอวตาร" -releaseToRefresh: "ปล่อยเพื่อรีเฟรช" -refreshing: "กำลังรีเฟรช..." -pullDownToRefresh: "ดึงลงเพื่อรีเฟรช" -useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว" -signupPendingError: "มีปัญหาในการตรวจสอบที่อยู่อีเมลลิงก์อาจหมดอายุแล้ว" -cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย" -doReaction: "เพิ่มรีแอคชั่น" -code: "โค้ด" -reloadRequiredToApplySettings: "จำเป็นต้องมีการโหลดซ้ำเพื่อให้การตั้งค่ามีผล" -remainingN: "เหลือ : {n}" -overwriteContentConfirm: "แน่ใจหรือไม่ว่าต้องการเขียนทับเนื้อหาปัจจุบัน?" -seasonalScreenEffect: "เอฟเฟกต์หน้าจอตามฤดูกาล" -decorate: "ตกแต่ง" -addMfmFunction: "เพิ่มการตกแต่ง" -enableQuickAddMfmFunction: "แสดงตัวจิ้มเลือก MFM ขั้นสูง" -bubbleGame: "เกมบับเบิ้ล" -sfx: "เสียงเอฟเฟ็กต์" -soundWillBePlayed: "จะมีการเล่นเอฟเฟกต์เสียง" -showReplay: "ดูรีเพลย์" -replay: "รีเพลย์" -replaying: "กำลังรีเพลย์" -endReplay: "ออกจากรีเพลย์" -copyReplayData: "คัดลอกข้อมูลรีเพลย์" -ranking: "อันดับ" -lastNDays: "ล่าสุด {n} วันที่แล้ว" -backToTitle: "กลับไปหน้าไตเติ้ล" -hemisphere: "พื้นที่ที่อาศัยอยู่" -withSensitive: "แสดงโน้ตที่มีไฟล์เนื้อหาละเอียดอ่อน" -userSaysSomethingSensitive: "โพสต์ที่มีไฟล์เนื้อหาละเอียดอ่อนของ {name}" -enableHorizontalSwipe: "ปัดเพื่อสลับแท็บ" -loading: "กำลังโหลด" -surrender: "ยอมแพ้" -gameRetry: "เริ่มเกมใหม่" -notUsePleaseLeaveBlank: "หากไม่ได้ใช้กรุณาเว้นว่างไว้" -useTotp: "ใช้รหัสผ่านแบบใช้ครั้งเดียว (TOTP)" -useBackupCode: "ใช้รหัสแบ๊กอัป" -launchApp: "เริ่มแอป" -useNativeUIForVideoAudioPlayer: "ใช้ UI ของเบราว์เซอร์เพื่อเล่นวิดีโอ/เสียง" -keepOriginalFilename: "คงชื่อไฟล์เดิมไว้" -keepOriginalFilenameDescription: "หากปิดการตั้งค่านี้ ในระหว่างการอัปโหลดชื่อไฟล์จะถูกแทนที่ด้วยสตริงแบบสุ่มโดยอัตโนมัติ" -noDescription: "ไม่มีข้อความอธิบาย" -alwaysConfirmFollow: "แสดงข้อความยืนยันเมื่อกดติดตาม" -inquiry: "ติดต่อเรา" -tryAgain: "โปรดลองอีกครั้ง" -confirmWhenRevealingSensitiveMedia: "ตรวจสอบก่อนแสดงสื่อที่มีเนื้อหาละเอียดอ่อน" -sensitiveMediaRevealConfirm: "สื่อนี้มีเนื้อหาละเอียดอ่อน, ต้องการแสดงใช่ไหม?" -createdLists: "รายชื่อที่ถูกสร้าง" -createdAntennas: "เสาอากาศที่ถูกสร้าง" -fromX: "จาก {x}" -genEmbedCode: "สร้างรหัสฝัง" -noteOfThisUser: "โน้ตโดยผู้ใช้นี้" -clipNoteLimitExceeded: "ไม่สามารถเพิ่มโน้ตเพิ่มเติมในคลิปนี้ได้อีกแล้ว" -performance: "ประสิทธิภาพ​" -modified: "แก้ไข" -discard: "ละทิ้ง" -thereAreNChanges: "มีอยู่ {n} เปลี่ยนแปลง(s)" -signinWithPasskey: "ลงชื่อเข้าใช้ด้วย Passkey" -unknownWebAuthnKey: "พาสคีย์ไม่ถูกต้องค่ะ" -passkeyVerificationFailed: "การยืนยันกุญแจดิจิทัลไม่สำเร็จค่ะ" -passkeyVerificationSucceededButPasswordlessLoginDisabled: "การยืนยันพาสคีย์สำเร็จแล้ว แต่การลงชื่อเข้าใช้แบบไม่ต้องใส่รหัสผ่านถูกปิดใช้งานแล้ว" -messageToFollower: "ข้อความถึงผู้ติดตาม" -target: "เป้า" -testCaptchaWarning: "ฟังก์ชันนี้มีไว้สำหรับทดสอบ CAPTCHA เท่านั้น\nห้ามนำไปใช้ในระบบจริงโดยเด็ดขาด" -prohibitedWordsForNameOfUser: "คำนี้ไม่สามารถใช้เป็นชื่อผู้ใช้ได้" -prohibitedWordsForNameOfUserDescription: "หากมีสตริงใดๆ ในรายการนี้ปรากฏอยู่ในชื่อของผู้ใช้ ชื่อนั้นจะถูกปฏิเสธ ผู้ใช้ที่มีสิทธิ์แต่ผู้ดูแลระบบนั้นจะไม่ได้รับผลกระทบใดๆจากข้อจำกัดนี้ค่ะ" -yourNameContainsProhibitedWords: "ชื่อของคุณนั้นมีคำที่ต้องห้าม" -yourNameContainsProhibitedWordsDescription: "ถ้าหากคุณต้องการใช้ชื่อนี้ กรุณาติดต่อผู้ดูแลระบบของเซิร์ฟเวอร์นะค่ะ" -federationDisabled: "เซิร์ฟเวอร์นี้ปิดการใช้งานการรวมกลุ่ม คุณไม่สามารถโต้ตอบกับผู้ใช้บนเซิร์ฟเวอร์อื่นได้" -reactAreYouSure: "คุณต้องการที่จะตอบสนองต่อ \" {emoji}\" หรือไม่?" -markAsSensitiveConfirm: "คุณต้องการทำเครื่องหมายสื่อนี้ว่าละเอียดอ่อนหรือไม่?" -unmarkAsSensitiveConfirm: "คุณต้องการลบการกำหนดความไวของสื่อนี้หรือไม่?" -postForm: "แบบฟอร์มการโพสต์" -information: "เกี่ยวกับ" -right: "ขวา" -bottom: "ภายใต้" -_chat: - invitations: "คำเชิญ" - noHistory: "ไม่มีประวัติ" - members: "สมาชิก" - home: "หน้าหลัก" - send: "ส่ง" -_settings: - webhook: "Webhook" -_accountSettings: - requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา" - requireSigninToViewContentsDescription1: "ต้องเข้าสู่ระบบเพื่อดูบันทึกและเนื้อหาอื่น ๆ ทั้งหมดที่คุณสร้าง คาดว่าจะมีประสิทธิผลในการป้องกันไม่ให้ข้อมูลถูกเก็บรวบรวมโดยโปรแกรมรวบรวมข้อมูล" - requireSigninToViewContentsDescription2: "นอกจากนี้ จะไม่สามารถดูจากเซิร์ฟเวอร์ที่ไม่รองรับการดูตัวอย่าง URL (OGP), การฝังในหน้าเว็บ หรือการอ้างอิงหมายเหตุได้" - requireSigninToViewContentsDescription3: "เนื้อหาที่ถูกรวมเข้ากับเซิร์ฟเวอร์ระยะไกลอาจไม่อยู่ภายใต้ข้อจำกัดเหล่านี้" -_abuseUserReport: - forward: "ส่ง​ต่อ" - forwardDescription: "ส่งรายงานไปยังเซิร์ฟเวอร์ระยะไกลโดยใช้บัญชีระบบที่ไม่ระบุตัวตน" - resolve: "แก้ไข" - accept: "ยอมรับ" - reject: "ปฏิเสธ" - resolveTutorial: "ถ้าหากรายงานนี้มีเนื้อหาถูกต้อง ให้เลือก \"ยอมรับ\" เพื่อปิดเคสกรณีนี้โดยถือว่าได้รับการแก้ไขแล้ว\nถ้าหากเนื้อหาในรายงานนี้นั้นไม่ถูกต้อง ให้เลือก \"ปฏิเสธ\" เพื่อปิดเคสกรณีนี้โดยถือว่าไม่ได้รับการแก้ไข" -_delivery: - status: "สถานะการจัดส่ง" - stop: "ระงับการส่ง" - resume: "จัดส่งต่อ" - _type: - none: "กำลังเผยแพร่" - manuallySuspended: "หยุดชั่วคราวด้วยตนเอง" - goneSuspended: "เซิร์ฟเวอร์ถูกระงับเนื่องจากมีการลบเซิร์ฟเวอร์นี้" - autoSuspendedForNotResponding: "เซิร์ฟเวอร์ถูกระงับเนื่องจากไม่ตอบสนอง" -_bubbleGame: - howToPlay: "วิธีเล่น" - hold: "ถือไว้" - _score: - score: "คะแนน" - scoreYen: "จำนวนเงินที่ได้รับ" - highScore: "คะแนนสูงสุด" - maxChain: "จำนวน chain สูงสุด" - yen: "{yen} เยน" - estimatedQty: "{qty} อัน" - scoreSweets: "โอนิงิริ {onigiriQtyWithUnit}" - _howToPlay: - section1: "ขยับตำแหน่งและวางวัตถุลงในกล่อง" - section2: "เมื่อวัตถุประเภทเดียวกันมารวมกัน พวกมันจะกลายเป็นวัตถุใหม่และคุณจะได้รับคะแนน" - section3: "หากวัตถุล้นออกมาจากกล่อง เกมก็จะจบลง ตั้งเป้าทำคะแนนให้สูงด้วยการหลอมวัตถุต่าง ๆ โดยไม่ทำให้ล้นกล่อง!" -_announcement: - forExistingUsers: "ผู้ใช้งานที่มีอยู่ตอนนี้เท่านั้น" - forExistingUsersDescription: "หากเปิดใช้งาน การประกาศนี้จะแสดงเฉพาะกับผู้ใช้ที่สร้างบัญชีก่อน/ที่มีอยู่ในขณะที่สร้างประกาศนี้เท่านั้น หากปิดใช้งาน การประกาศนี้จะแสดงกับผู้ใช้ที่สร้างบัญชีหลังจากสร้างประกาศนี้ด้วย" - needConfirmationToRead: "จำเป็นต้องยืนยันว่าอ่านแล้ว" - needConfirmationToReadDescription: "กล่องโต้ตอบการยืนยันจะปรากฏขึ้นเมื่อจะทำเครื่องหมายว่าอ่านแล้ว นอกจากนี้ยังทำให้ประกาศนี้ยังไม่ถูกอ่านเมื่อใช้ฟังก์ชั่น “ทำเครื่องหมายฯ ทั้งหมดว่าอ่านแล้ว”" - end: "เก็บประกาศ" - tooManyActiveAnnouncementDescription: "เนื่องจากมีการประกาศที่ยังใช้งานอยู่จำนวนมาก อาจทำให้ UX ลดลง แนะนำให้พิจารณาการเก็บประกาศที่สิ้นสุดไปแล้ว" - readConfirmTitle: "ทำเครื่องหมายว่าอ่านแล้วเลยไหม?" - readConfirmText: "จะทำเครื่องหมายใส่ “{title}” ว่าอ่านแล้ว" - shouldNotBeUsedToPresentPermanentInfo: "เนื่องจากมีความเป็นไปได้สูงที่จะส่งผลเสียต่อง UX ของผู้ใช้ใหม่ จึงขอแนะนำให้ใช้ประกาศสำหรับข้อมูลที่ต้องการการตอบสนองในทันที ไม่ใช่ข้อมูลที่ต้องการแสดงตลอดเวลา" - dialogAnnouncementUxWarn: "เราขอแนะนำให้ใช้ด้วยความระมัดระวัง เนื่องจากการแจ้งเตือนแบบกล่องโต้ตอบตั้งแต่ 2 รายการขึ้นไปพร้อมกันอาจส่งผลเสียต่อ UX ได้อย่างมาก" - silence: "ไม่มีการแจ้งเตือน" - silenceDescription: "หากเปิดใช้งาน จะไม่มีการแจ้งเตือนประกาศนี้ และผู้ใช้จะไม่จำเป็นต้องทำเครื่องหมายว่าอ่านแล้ว" _initialAccountSetting: - accountCreated: "สร้างบัญชีเสร็จสมบูรณ์!" + accountCreated: "คุณได้สร้างบัญชีของคุณสำเร็จเรียบร้อยแล้ว!" letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ" letsFillYourProfile: "ก่อนอื่นมาตั้งค่าโปรไฟล์ของคุณ" profileSetting: "ตั้งค่าโปรไฟล์" privacySetting: "ตั้งค่าความเป็นส่วนตัว" theseSettingsCanEditLater: "คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ในภายหลังได้ตลอดเวลานะ" - youCanEditMoreSettingsInSettingsPageLater: "สามารถตั้งค่าเพิ่มเติมได้ที่หน้า “การตั้งค่า” อย่าลืมไปเยี่ยมชมภายหลังด้วย" - followUsers: "ลองติดตามผู้ใช้ที่สนใจเพื่อสร้างไทม์ไลน์ดูสิ" - pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ" initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!" - haveFun: "ขอให้สนุกกับ {name}!" - youCanContinueTutorial: "คุณสามารถดำเนินการต่อด้วยบทช่วยสอนเกี่ยวกับวิธีใช้ {name} (Misskey) หรือออกจากบทช่วยสอนแล้วเริ่มใช้งานได้ทันที" - startTutorial: "เริ่มการฝึกสอน" + haveFun: "สนุกกับ {name}!" skipAreYouSure: "ต้องการข้ามการตั้งค่าโปรไฟล์จริงๆแบบนั้นหรอ?" laterAreYouSure: "ต้องการตั้งค่าโปรไฟล์ในภายหลังจริงๆอย่างงั้นหรอ?" -_initialTutorial: - launchTutorial: "เริ่มบทช่วยสอน" - title: "บทช่วยสอน" - wellDone: "ทำได้ดีมาก!" - skipAreYouSure: "ต้องการออกจากบทช่วยสอนใช่ไหม?" - _landing: - title: "ยินดีต้อนรับสู่บทช่วยสอน" - description: "คุณสามารถตรวจสอบการใช้งานและฟังก์ชั่นพื้นฐานของ Misskey ได้ที่นี่" - _note: - title: "โน้ตคืออะไร?" - description: "โพสต์ใน Misskey เรียกว่า “โน้ต” ซึ่งจะจัดเรียงตามลำดับเวลาบนไทม์ไลน์และอัปเดตแบบเรียลไทม์" - reply: "คุณสามารถตอบกลับได้ และคุณยังสามารถตอบกลับใส่การตอบกลับเพื่อสนทนาต่อได้เสมือนดั่งเธรด" - renote: "คุณสามารถแชร์โน้ตไปยังไทม์ไลน์ของคุณเอง คุณยังสามารถเพิ่มข้อความและเครื่องหมายคำพูดได้" - reaction: "คุณสามารถเพิ่มรีแอคชั่นได้ รายละเอียดจะอธิบายอยู่ในหน้าถัดไป" - menu: "คุณสามารถดูรายละเอียดโน้ต คัดลอกลิงก์ และดำเนินการอื่นๆ ได้" - _reaction: - title: "รีแอคชั่นคืออะไร?" - description: "โน้ตสามารถ“รีแอคชั่น”ด้วยเอโมจิต่างๆ ซึ่งทำให้สามารถแสดงความแตกต่างเล็กๆ น้อยๆ ที่อาจไม่สามารถสื่อออกมาได้ด้วยการแค่การกด “ถูกใจ”" - letsTryReacting: "คุณสามารถเพิ่มรีแอคชั่นได้ด้วยการคลิกปุ่ม “+” บนโน้ต ลองรีแอคชั่นโน้ตตัวอย่างนี้ดูสิ!" - reactToContinue: "เพิ่มรีแอคชั่นเพื่อดำเนินการต่อ" - reactNotification: "คุณจะได้รับการแจ้งเตือนแบบเรียลไทม์เมื่อมีคนตอบรีแอคชั่นโน้ตของคุณ" - reactDone: "คุณสามารถยกเลิกรีแอคชั่นได้โดยการกดปุ่ม “-”" - _timeline: - title: "แนวคิดเรื่องของไทม์ไลน์" - description1: "Misskey มีหลายไทม์ไลน์ขึ้นอยู่กับวิธีการใช้งานของคุณ (บางไทม์ไลน์อาจไม่สามารถใช้ได้ขึ้นอยู่กับนโยบายของเซิร์ฟเวอร์)" - home: "คุณสามารถดูโพสต์จากบัญชีที่คุณติดตามได้" - local: "คุณสามารถดูโพสต์จากผู้ใช้ทั้งหมดบนเซิร์ฟเวอร์นี้" - social: "จะแสดงโพสต์ทั้งจากไทม์ไลน์หลักและไทม์ไลน์ท้องถิ่น" - global: "คุณสามารถดูโพสต์จากเซิร์ฟเวอร์ที่เชื่อมต่ออื่นๆ ทั้งหมดได้" - description2: "คุณสามารถสลับระหว่างแต่ละไทม์ไลน์ได้ตลอดเวลาได้ที่บริเวณด้านบนของหน้าจอ" - description3: "นอกจากนี้ยังมีรายการไทม์ไลน์ ไทม์ไลน์ของช่อง ฯลฯ โปรดดู {link} สำหรับรายละเอียดเพิ่มเติม" - _postNote: - title: "ตั้งค่าการโพสต์โน้ต" - description1: "เมื่อโพสต์โน้ตบน Misskey คุณสามารถตั้งค่าตัวเลือกต่างๆ ได้ แบบฟอร์มการส่งมีลักษณะดังนี้" - _visibility: - description: "คุณสามารถจำกัดผู้ที่สามารถดูโน้ตของคุณได้นะ" - public: "โน้ตของคุณนั้นจะปรากฏแก่ผู้ใช้งานทุกคน" - home: "เผยแพร่บนไทม์ไลน์หลักเท่านั้น แต่ผู้ติดตาม ผู้ที่เข้ามาดูโปรไฟล์ และผู้ที่เห็นจากรีโน้ตยังสามารถดูโพสต์นี้ได้" - followers: "มองเห็นได้เฉพาะผู้ติดตามเท่านั้น ไม่มีใครอื่นนอกจากตัวคุณเองที่สามารถรีโน้ตได้ และมีเพียงผู้ติดตามของคุณเท่านั้นที่สามารถดูได้" - direct: "เปิดให้เห็นเฉพาะผู้ใช้ที่ระบุเท่านั้น และพวกเขาจะได้รับแจ้งเตือนด้วย คุณสามารถใช้มันแทนข้อความโดยตรง (dm)" - doNotSendConfidencialOnDirect1: "โปรดใช้ความระมัดระวังในการส่งข้อมูลที่ละเอียดอ่อน" - doNotSendConfidencialOnDirect2: "ผู้ดูแลระบบเซิร์ฟเวอร์ปลายทางสามารถดูเนื้อหาที่โพสต์ได้ ดังนั้นหากคุณส่งโพสต์โดยตรงไปยังผู้ใช้บนเซิร์ฟเวอร์ที่ไม่น่าเชื่อถือ คุณจะต้องใช้ความระมัดระวังในการจัดการข้อมูลที่เป็นความลับ" - localOnly: "การโพสต์ด้วย flag นี้จะไม่รวมโน้ตไปยังเซิร์ฟเวอร์อื่น ผู้ใช้บนเซิร์ฟเวอร์อื่นจะไม่สามารถดูโน้ตเหล่านี้ได้โดยตรง โดยไม่คำนึงถึงการตั้งค่าการแสดงผลข้างต้น" - _cw: - title: "คำเตือนเกี่ยวกับเนื้อหา" - description: "เนื้อหาที่เขียนใน “คำอธิบายประกอบ” จะแสดงแทนเนื้อหาหลัก ต้องคลิก “ดูเพิ่มเติม” เพื่อให้เนื้อหาหลักแสดง" - _exampleNote: - cw: " ห้ามดู ระวังหิว" - note: "เพิ่งไปกินโดนัทเคลือบช็อคโกแลตมา 🍩😋" - useCases: "ใช้สิ่งนี้เพื่อระบุโน้ตที่ต้องตามแนวทางปฏิบัติของเซิร์ฟเวอร์ หรือเพื่อควบคุมการสปอยล์และข้อความที่ละเอียดอ่อนด้วยตนเอง" - _howToMakeAttachmentsSensitive: - title: "จะทำเครื่องหมายไฟล์แนบว่ามีเนื้อหาละเอียดอ่อนได้อย่างไร?" - description: "ทำเครื่องหมายไฟล์แนบว่า “มีเนื้อหาละเอียดอ่อน” เมื่อจำเป็นตามแนวทางของเซิร์ฟเวอร์ หรือเมื่อไฟล์แนบไม่ควรปรากฏให้เห็น" - tryThisFile: "ลองทำให้รูปภาพที่แนบมากับแบบฟอร์มนี้มีเนื้อหาละเอียดอ่อน!" - _exampleNote: - note: "อุ้ย นัตโตะ ฝาเปิดเละเทะ..." - method: "หากต้องการทำให้ไฟล์แนบมีเนื้อหาละเอียดอ่อน ให้คลิกไฟล์เพื่อเปิดเมนูแล้วคลิก “ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน”" - sensitiveSucceeded: "เมื่อแนบไฟล์ โปรดตั้งค่าเครื่องหมายว่ามีเนื้อหาละเอียดอ่อนตามแนวทางของเซิร์ฟเวอร์" - doItToContinue: "ทำเครื่องหมายกับรูปภาพว่ามีเนื้อหาละเอียดอ่อน เพื่อดำเนินการต่อ" - _done: - title: "บทเรียนจบลงแล้วจ้า เย่เย่เย่ 🎉" - description: "คุณสมบัติที่แนะนำในที่นี่เป็นเพียงบางส่วนเท่านั้น หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้ Misskey โปรดไปที่ {link}" -_timelineDescription: - home: "บนไทม์ไลน์หลัก คุณสามารถดูโพสต์จากบัญชีที่ติดตามอยู่ได้" - local: "ไทม์ไลน์ท้องถิ่นช่วยให้เห็นโพสต์จากผู้ใช้ทั้งหมดบนเซิร์ฟเวอร์นี้" - social: "ไทม์ไลน์โซเชียลจะแสดงโพสต์จากทั้งไทม์ไลน์หลักและไทม์ไลน์ท้องถิ่น" - global: "ในไทม์ไลน์ทั่วโลก คุณสามารถดูโน้ตจากเซิร์ฟเวอร์ที่เชื่อมต่อทั้งหมดได้" _serverRules: description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ" -_serverSettings: - iconUrl: "URL ไอคอน" - appIconDescription: "ระบุไอคอนที่จะใช้เมื่อ {host} แสดงเป็นแอป" - appIconUsageExample: "ตัวอย่างเช่น เมื่อถูกเพิ่มเป็น PWA หรือบุ๊กมาร์กบนหน้าจอหลักในสมาร์ทโฟน" - appIconStyleRecommendation: "เนื่องจากอาจถูกครอบตัดเป็นสี่เหลี่ยมหรือวงกลม จึงแนะนำให้ใช้ภาพที่เผื่อพื้นที่รอบๆ ตัวโลโก้ไอคอนไว้" - appIconResolutionMustBe: "ความละเอียดขั้นต่ำไว้คือ {resolution}." - manifestJsonOverride: "เขียนทับ manifest.json" - shortName: "ชื่อย่อ" - shortNameDescription: "ตัวย่อหรือชื่อทั่วไปที่สามารถแสดงแทนชื่ออย่างเป็นทางการแบบยาวของเซิร์ฟเวอร์" - fanoutTimelineDescription: "เพิ่มประสิทธิภาพการดึงข้อมูลไทม์ไลน์อย่างมาก และลดภาระในฐานข้อมูลเมื่อเปิดใช้งาน ในทางกลับกัน การใช้หน่วยความจำของ Redis จะเพิ่มขึ้น ลองปิดการใช้งานนี้ในกรณีที่หน่วยความจำเซิร์ฟเวอร์เหลือน้อยหรือเซิร์ฟเวอร์ไม่เสถียร" - fanoutTimelineDbFallback: "ฟอลแบ๊กกลับฐานข้อมูล" - fanoutTimelineDbFallbackDescription: "เมื่อเปิดใช้งาน หากไม่ได้แคชไทม์ไลน์ ไทม์ไลน์จะฟอลแบ๊กไปยังฐานข้อมูลสำหรับการ query เพิ่มเติม การปิดใช้งานจะช่วยลดภาระของเซิร์ฟเวอร์ด้วยการกำจัดกระบวนฟอลแบ๊ก แต่มันก็จะจำกัดช่วงเวลาไทม์ไลน์ที่สามารถดึงข้อมูลได้" - reactionsBufferingDescription: "เมื่อเปิดใช้งานฟังก์ชันนี้ก็จะช่วยลด latency ในการสร้างปฏิกิริยา แต่อาจจะส่งผลให้ memory footprint ของ Redis เพิ่มขึ้นนะ" - inquiryUrl: "URL สำหรับการติดต่อสอบถาม" - inquiryUrlDescription: "ระบุ URL ของหน้าเว็บที่มีแบบฟอร์มสำหรับติดต่อผู้ดูแลเซิร์ฟเวอร์ หรือข้อมูลการติดต่อของผู้ดูแลเซิร์ฟเวอร์" - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "ถ้าหากไม่มีการตรวจสอบจากผู้ดูแลระบบหรือไม่มีความเคลื่อนไหวมาเป็นระยะเวลาหนึ่ง ระบบจะทำการปิดใช้งานฟังก์ชันนี้โดยอัตโนมัติ เพื่อลดความเสี่ยงในการถูกโจมตีด้วยสแปมและอื่นๆ" _accountMigration: - moveFrom: "ย้ายจากบัญชีอื่นมาที่บัญชีนี้" + moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง" moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น" - moveFromLabel: "บัญชีที่จะย้ายจาก #{n}" - moveFromDescription: "หากต้องการโอนข้อมูลจากบัญชีอื่นมายังบัญชีนี้ จำเป็นต้องสร้างบัญชีนามแฝง (alias) ไว้ที่นี่ด้วย\nกรุณากรอกบัญชีเดิมในรูปแบบ: @username@server.example.com\nหากต้องการลบ alias, ให้เว้นว่างไว้แล้วบันทึก (ไม่แนะนำ)" - moveTo: "ย้ายบัญชีนี้ไปยังบัญชีใหม่" + moveFromLabel: "บัญชีที่จะย้ายจาก:" + moveFromDescription: "ถ้าหากคุณต้องการโอนข้อมูล คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี หลังจากนั้นป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com" + moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง" moveToLabel: "บัญชีที่จะย้ายไปที่:" moveCannotBeUndone: "ไม่สามารถยกเลิกการโอนย้ายบัญชีได้" - moveAccountDescription: "การดำเนินการนี้จะย้ายบัญชีของคุณไปยังบัญชีอื่น\n・ผู้ที่กำลังติดตามคุณจากบัญชีนี้จะถูกย้ายไปยังบัญชีใหม่โดยอัตโนมัติ\n・บัญชีนี้จะเลิกติดตามผู้ใช้ทั้งหมดที่กำลังติดตามอยู่\n・คุณจะไม่สามารถสร้างโน้ต ฯลฯ ในบัญชีนี้ได้\n\nแม้ว่าการย้ายผู้ที่ติดตามคุณจะเป็นไปโดยอัตโนมัติ แต่คุณต้องเตรียมขั้นตอนบางอย่างด้วยตนเอง เพื่อย้ายรายชื่อผู้ใช้ที่คุณกำลังติดตาม โดยดำเนินการส่งออกรายชื่อแล้วค่อยนำเข้ามาภายหลังในเมนูการตั้งค่าของบัญชีใหม่ ใช้ขั้นตอนเดียวกันนี้ใช้รายชื่อผู้ใช้ที่ถูกปิดเสียงและถูกบล็อก\n\n(คำอธิบายนี้ใช้กับ Misskey v13.12.0 ขึ้นไป, ซอฟต์แวร์ ActivityPub อื่นๆ เช่น Mastodon อาจทำงานแตกต่างออกไป)" - moveAccountHowTo: "การย้ายบัญชีจะเริ่มต้นโดยการสร้างบัญชีนามแฝง (alias) ของบัญชีนี้ ณ บัญชีที่เป็นปลายทาง หลังจากสร้างนามแฝงแล้ว ให้ป้อนบัญชีปลายทางในรูปแบบดังนี้: @username@server.example.com" + moveAccountDescription: "การกระทำนี้ไม่สามารถย้อนกลับได้นะ ขั้นตอนแรก ต้องสร้างนามแฝงสำหรับบัญชีนี้ในบัญชีที่คุณต้องการย้ายไป หลังจากนั้นแล้ว ป้อนบัญชีที่จะย้ายไปในรูปแบบดังต่อไปนี้: @person@instance.com" + moveAccountHowTo: "หากต้องการย้ายข้อมูลก่อนอื่นให้สร้างชื่อแทนสำหรับบัญชีนี้ ในบัญชีที่จะต้องการย้ายไป\nหลังจากที่คุณสร้างนามแฝงนั้นแล้ว ให้ป้อนบัญชีที่ต้องการจะย้ายไปในรูปแบบดังต่อไปนี้: @username@server.example.com" startMigration: "โอนย้าย" migrationConfirm: "ยืนยันการย้ายข้อมูลบัญชีนี้ไปที่ {account} เมื่อเริ่มแล้วจะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี" - movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถยกเลิกการโอนย้ายได้" - postMigrationNote: "บัญชีนี้จะดำเนินการยกเลิกการติดตามทั้งหมดหลังจากการย้ายข้อมูลไปแล้ว 24 ชั่วโมง จำนวนกำลังติดตามและจำนวนผู้ติดตามของบัญชีนี้จะเป็น 0 และเพื่อหลีกเลี่ยงไม่ให้ผู้ติดตามคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามฯได้ การยกเลิกการติดตามจะไม่กระทบกับผู้ติดตามคุณ ดังนั้นผู้ติดตามคุณยังคงสามารถดูโพสต์ของบัญชีนี้ได้" - movedTo: "บัญชีที่จะย้ายไป:" + movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถย้อนกลับโอนย้ายข้อมูลได้" + postMigrationNote: "บัญชีนี้จะถูกเลิกติดตามบัญชีทั้งหมดที่กำลังติดตามภายใน 24 ชั่วโมงหลังจากการย้ายข้อมูลนั้นเสร็จสิ้น ทั้งจำนวนผู้ติดตามและผู้ติดตามนั้นจะกลายเป็นศูนย์ เพื่อหลีกเลี่ยงป้องกันไม่ให้ผู้ติดตามของคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามของบัญชีนี้ได้ แต่อย่างไรก็ตามแล้วพวกเขาจะยังคงติดตามบัญชีนี้ต่อไป" + movedTo: "บัญชีที่จะย้ายไปที่:" _achievements: earnedAt: "ได้รับเมื่อ" _types: _notes1: - title: "just setting up my msky" - description: "โพสต์โน้ตเป็นครั้งแรก" + title: "เพียงแค่ตั้งค่า msky ของฉัน" + description: "โพสต์โน้ตครั้งแรกของคุณ" flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!" _notes10: - title: "โน้ตไม่กี่ชิ้น" + title: "โน้ตบางอย่าง" description: "โพสต์ 10 โน้ต" _notes100: - title: "โน้ตเยอะอยู่" + title: "โน้ตจำนวนมาก" description: "โพสต์ 100 โน้ต" _notes500: - title: "จมคากองโน้ต" + title: "ครอบคลุมในโน้ต" description: "โพสต์ 500 โน้ต" _notes1000: title: "ภูเขาแห่งโน้ต" description: "โพสต์ 1,000 โน้ต" _notes5000: - title: "โน้ตล้นไปแล้ว" + title: "โน้ตล้น" description: "โพสต์ 5,000 โน้ต" _notes10000: title: "ซุปเปอร์โน้ต" description: "โพสต์ 10,000 โน้ต" _notes20000: - title: "ต้ อ ง ก า ร โ น้ ต เ พิ่ ม อี ก !" + title: "ต้องการ... เพิ่มเติม... โน้ต..." description: "โพสต์ 20,000 โน้ต" _notes30000: title: "โน้ต โน้ต โน้ต!" description: "โพสต์ 30,000 โน้ต" _notes40000: - title: "โรงงานผลิตโน้ต" + title: "โน้ตโรงงาน" description: "โพสต์ 40,000 โน้ต" _notes50000: title: "ดาวเคราะห์แห่งโน้ต" @@ -1511,39 +1133,39 @@ _achievements: title: "โน้ตควอซาร์" description: "โพสต์ 60,000 โน้ต" _notes70000: - title: "หลุม-โน้ต-ดำ" + title: "โน้ตหลุมดำ" description: "โพสต์ 70,000 โน้ต" _notes80000: - title: "ดาราจักรโน้ต" + title: "โน้ต กาแล็กซี่" description: "โพสต์ 80,000 โน้ต" _notes90000: - title: "จักรวาลโน้ต" + title: "โน้ต จักรวาล" description: "โพสต์ 90,000 โน้ต" _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" description: "โพสต์ 100,000 โน้ต" - flavor: "มีเรื่องจะเขียนมากขนาดนั้นเลยเหรอนั่น?" + flavor: "นายแน่ใจล่ะก็ มีอะไรพูดมาได้นะ" _login3: title: "มือใหม่ I" description: "เข้าสู่ระบบเป็นเวลารวม 3 วัน" - flavor: "ตั้งแต่วันนี้เป็นต้นไป ฉันคือมิสคิสต์" + flavor: "เริ่มตั้งแต่วันนี้ เรียกฉันว่ามิสคิสต์" _login7: title: "มือใหม่ II" description: "เข้าสู่ระบบเป็นเวลารวม 7 วัน" - flavor: "ชินกับมันแล้วหรือยัง?" + flavor: "รู้สึกเหมือนคุณได้แขวนของสิ่งต่างๆ หรือยังคะ?" _login15: title: "มือใหม่ III" description: "เข้าสู่ระบบเป็นเวลารวม 15 วัน" _login30: - title: "มิสคิสต์ I" + title: "มิสคิสท์ I" description: "เข้าสู่ระบบเป็นเวลารวม 30 วัน" _login60: - title: "มิสคิสต์ II" + title: "มิสคิสท์ II" description: "เข้าสู่ระบบเป็นเวลารวม 60 วัน" _login100: - title: "มิสคิสต์ III" + title: "มิสคิสท์ III" description: "เข้าสู่ระบบเป็นเวลารวม 100 วัน" - flavor: "Violent Misskist (ทำไมเหมือนชื่อหนังสักเรื่องจังเลยนะ)" + flavor: "ความรุนแรง Misskist" _login200: title: "ลูกค้าประจำ I" description: "เข้าสู่ระบบเป็นเวลารวม 200 วัน" @@ -1556,7 +1178,7 @@ _achievements: _login500: title: "ผู้เชี่ยวชาญ I" description: "เข้าสู่ระบบเป็นเวลารวม 500 วัน" - flavor: "ทุกท่าน ผมชอบโน้ต (กล่าวโดย เดอะ เ_เ_อร์)" + flavor: "เพื่อนของผมนะมักจะกล่าวว่าผมนะชอบจดโน้ต" _login600: title: "ผู้เชี่ยวชาญ II" description: "เข้าสู่ระบบเป็นเวลารวม 600 วัน" @@ -1574,24 +1196,24 @@ _achievements: description: "เข้าสู่ระบบเป็นเวลารวม 1,000 วัน" flavor: "ขอบคุณที่ใช้ Misskey นะ !" _noteClipped1: - title: "อดไม่ได้ที่จะต้องคลิปมันเอาไว้" - description: "คลิปโน้ตเป็นครั้งแรก" + title: "จะต้อง... คลิป..." + description: "คลิปโน้ตตัวแรกของคุณ" _noteFavorited1: title: "สตาร์เกเซอร์" - description: "ใส่โน้ตเป็นรายการโปรดเป็นครั้งแรก" + description: "ชื่นชอบโน้ตแรกของคุณ" _myNoteFavorited1: title: "แสวงหาดวงดาว" - description: "โน้ตตัวเองถูกคนอื่นเพิ่มลงรายการโปรดของเขา" + description: "มีคนอื่นๆที่ชื่นชอบหนึ่งในโน้ตของคุณ" _profileFilled: - title: "เตรียมตัวอย่างดี" - description: "ตั้งค่าโปรไฟล์" + title: "เตรียมไว้อย่างดี" + description: "ตั้งค่าโปรไฟล์ของคุณ" _markedAsCat: title: "ฉันเป็นแมว" - description: "ตั้งค่าบัญชีเป็นแมวเมี้ยวเมี้ยว" - flavor: "แมวน้อยไร้ชื่อ" + description: "ทำเครื่องหมายบัญชีของคุณว่าเป็นแมว" + flavor: "ฉันจะให้ชื่อคุณภายหลังนะ" _following1: - title: "ก้าวแรกสู่...กดติดตาม" - description: "กดติดตามชาวบ้านครั้งแรก" + title: "กำลังติดตามผู้ใช้คนแรกของคุณ" + description: "ติดตามผู้ใช้" _following10: title: "ทำต่อไป... ทำต่อไป..." description: "ติดตาม 10 บัญชีผู้ใช้" @@ -1602,7 +1224,7 @@ _achievements: title: "เพื่อน 100 คน" description: "ติดตาม 100 บัญชี" _following300: - title: "มีเพื่อนมากเกินไปละ" + title: "เพื่อนโอเวอร์โหลด" description: "ติดตาม 300 บัญชี" _followers1: title: "ผู้ติดตามคนแรก" @@ -1629,12 +1251,12 @@ _achievements: title: "นักสะสมความสำเร็จ" description: "ได้รับความสำเร็จ 30 ครั้ง" _viewAchievements3min: - title: "ชอบบรรลุความสําเร็จ" - description: "มองดูรายการความสำเร็จเป็นเวลานานกว่า 3 นาที" + title: "ชอบบรรลุผลสําเร็จ" + description: "มองดูรายการความสำเร็จของคุณเป็นเวลาอย่างน้อย 3 นาที" _iLoveMisskey: title: "ฉันรัก Misskey" - description: "โพสต์ “I ❤ #Misskey”" - flavor: "ขอบคุณพระคุณเป็นอย่างสูงที่ท่านใช้ Misskey นะคะ ! by ทีมผู้พัฒนา" + description: "โพสต์ \"I ❤ #Misskey\"" + flavor: "ทีมผู้พัฒนา Misskey ได้ขอบคุณสำหรับการสนับสนุนของคุณ!" _foundTreasure: title: "ล่าสมบัติ" description: "คุณพบสมบัติที่ซ่อนอยู่" @@ -1642,28 +1264,28 @@ _achievements: title: "พักผ่อนสักหน่อย" description: "ใช้เวลา 30 นาทีบน Misskey" _client60min: - title: "Misskey ต้องไม่มีสิ่งใด “Miss”" + title: "ไม่พบ \"Miss\" ใน Misskey " description: "เปิด Misskey ค้างไว้แล้วอย่างน้อย 60 นาที" _noteDeletedWithin1min: title: "ไม่เป็นไร" description: "ลบโน้ตภายในหนึ่งนาทีหลังจากที่โพสต์" _postedAtLateNight: - title: "ออกหากินยามดึกดื่น" + title: "กลางคืน" description: "โพสต์โน้ตตอนดึกๆ" flavor: "ได้เวลาเข้านอนแล้วนะ" _postedAt0min0sec: - title: "นาฬิกาเทียบเวลา" - description: "โพสต์โน้ตเมื่อเวลา 00:00 น." - flavor: "โป๊ะ โป๊ะ โป๊ะ ปิ้งงงงง" + title: "นาฬิกาพูดได้" + description: "โพสต์บนโน้ตเมื่อเวลา 00:00 น." + flavor: "คลิก คลิก คลิก แกล๊งๆ" _selfQuote: title: "อ้างอิงตนเอง" - description: "อ้างอิงโน้ตตัวเอง" + description: "อ้างโน้ตย่อของคุณเอง" _htl20npm: title: "ไทม์ไลน์ไหล" - description: "มีการทำความเร็วของไทม์ไลน์หลักเกิน 20 npm (โน้ตต่อนาที)" + description: "มีการทำความเร็วของไทม์ไลน์ที่บ้านของคุณเกิน 20 npm (โน้ตต่อนาที)" _viewInstanceChart: title: "วิเคราะห์" - description: "ดูแผนภูมิของเซิร์ฟเวอร์" + description: "ดูแผนภูมิอินสแตนซ์ของคุณ" _outputHelloWorldOnScratchpad: title: "หวัดดีชาวโลก!" description: "เอาพุต \"hello world\" ใน Scratchpad" @@ -1677,23 +1299,23 @@ _achievements: title: "คุณอ่านมันจริงๆหรือเปล่า?" description: "มีการโต้ตอบกับโน้ตที่มีความยาวมากกว่า 100 ตัวอักษรภายใน 3 วินาทีหลังจากที่โพสต์" _clickedClickHere: - title: "คลิกที่นี่" + title: "คลิ๊กที่นี่" description: "คุณได้คลิกที่นี่" _justPlainLucky: title: "แค่ลัคกี้ธรรมดา" description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที" _setNameToSyuilo: - title: "คอมเพล็กซ์ของพระเจ้า" - description: "ตั้งชื่อเป็น “syuilo”" + title: "พระเจ้าคอมเพล็กซ์" + description: "ตั้งชื่อของคุณเป็น \"syuilo\"" _passedSinceAccountCreated1: title: "ครบรอบหนึ่งปี" - description: "ผ่านไป 1 ปีนับตั้งแต่สร้างบัญชี" + description: "ผ่านไปหนึ่งปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" _passedSinceAccountCreated2: title: "ครบรอบสองปี" - description: "ผ่านไป 2 ปีนับตั้งแต่สร้างบัญชี" + description: "ผ่านไปสองปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" _passedSinceAccountCreated3: title: "ครบรอบสามปี" - description: "ผ่านไป 3 ปีนับตั้งแต่สร้างบัญชี" + description: "ผ่านไปสามปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" _loggedInOnBirthday: title: "สุขสันต์วันเกิด" description: "เข้าสู่ระบบในวันเกิดของคุณ" @@ -1704,74 +1326,53 @@ _achievements: _cookieClicked: title: "เกมที่คุณคลิกที่คุกกี้" description: "คลิกคุกกี้" - flavor: "ใช่หรอ? แน่ใจว่าซอฟต์แวร์ทำงานถูกต้องนะ?" + flavor: "เดี๋ยวก่อนนะ คุณอยู่ในเว็บไซต์ที่ถูกต้องแน่อย่างงั้นเหรอ?" _brainDiver: title: "Brain Diver" description: "โพสต์ลิงก์ไปยัง Brain Diver" flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "ทดสอบโอเวอร์โฟลว์" - description: "ทดสอบการแจ้งเตือนทริกเกอร์ซ้ำๆ ภายในระยะเวลาอันสั้นๆ" - _tutorialCompleted: - title: "ใบรับรองการสำเร็จหลักสูตร Misskey มือใหม่" - description: "เสร็จสิ้นการสอนแล้ว" - _bubbleGameExplodingHead: - title: "🤯" - description: "สร้างวัตถุที่ใหญ่ที่สุดในเกมบับเบิ้ล" - _bubbleGameDoubleExplodingHead: - title: "ดับเบิ้ล" - description: "สร้างวัตถุที่ใหญ่ที่สุดในเกมบับเบิ้ลสองชิ้นในเวลาเดียวกัน" - flavor: "ปิ่นโตขนาดนี้ น่าจะเพิ่ม 🤯 🤯 เข้าไปนิดหน่อย" _role: new: "บทบาทใหม่" edit: "แก้ไขบทบาท" name: "ชื่อบทบาท" description: "คำอธิบายบทบาท" permission: "สิทธิ์ตามบทบาท" - descriptionOfPermission: "ผู้ควบคุม สามารถดำเนินการดูแลขั้นพื้นฐานได้\nผู้ดูแลระบบ สามารถเปลี่ยนการตั้งค่าทั้งหมดของเซิร์ฟเวอร์ได้" + descriptionOfPermission: "ผู้ดูแลกลั่นกรองเนื้อหา สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\nผู้ดูแลระบบ สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ" assignTarget: "มอบหมาย" - descriptionOfAssignTarget: "แบบปรับเอง เพิ่มถอนบทบาทนี้แก่ผู้ใช้ด้วยตัวเอง\nแบบมีเงื่อนไข เพิ่มถอนบทบาทนี้แก่ผู้ใช้โดยอัตโนมัติหากเข้าเงื่อนไขใดต่อไปนี้" + descriptionOfAssignTarget: "แมนนวล เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\nเงื่อนไข เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง" manual: "ปรับเอง" - manualRoles: "บทบาทแบบทำมือ" conditional: "มีเงื่อนไข" - conditionalRoles: "บทบาทแบบมีเงื่อนไข" condition: "เงื่อนไข" isConditionalRole: "นี่คือบทบาทที่มีเงื่อนไข" - isPublic: "ทำให้บทบาทเปิดเผยต่อสาธารณะ" - descriptionOfIsPublic: "บทบาทจะปรากฏบนโปรไฟล์ของผู้ใช้และเปิดเผยต่อสาธารณะ (ทุกคนสามารถเห็นได้ว่าผู้ใช้รายนี้มีบทบาทนี้)" + isPublic: "บทบาทสาธารณะ" + descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย" options: "ตัวเลือกบทบาท" policies: "นโยบาย" - baseRole: "แม่แบบบทบาท" - useBaseValue: "ใช้ตามแม่แบบบทบาท" + baseRole: "บทบาทพื้นฐาน" + useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" - iconUrl: "URL ไอคอน" + iconUrl: "ไอคอน URL" asBadge: "แสดงเป็นตรา" - descriptionOfAsBadge: "เมื่อเปิดใช้งาน ไอคอนบทบาทจะปรากฏถัดจากชื่อผู้ใช้" - isExplorable: "ค้นหาผู้ใช้ได้ง่ายขึ้นโดยดูจากบทบาท" - descriptionOfIsExplorable: "เมื่อเปิดใช้งาน ไทมไลน์บทบาทนี้และสมาชิกที่มีบทบาทนี้จะเปิดเผยเป็นสาธารณะ" - displayOrder: "ลำดับการแสดงผล" - descriptionOfDisplayOrder: "เลขที่สูงกว่าจะแสดงบน UI ก่อน" - canEditMembersByModerator: "อนุญาตให้ผู้ควบคุมแก้ไขสมาชิก" - descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ นอกเหนือจากผู้ควบคุมและผู้ดูแลระบบแล้ว จะสามารถเพิ่มถอนบทบาทนี้แก่ผู้ใช้ได้ แต่เมื่อปิดใช้ จะมีเฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถดำเนินการได้" + descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน" + isExplorable: "บทบาทไทม์ไลน์เป็นแบบสาธารณะ" + descriptionOfIsExplorable: "ไทม์ไลน์ของบทบาทนี้จะสามารถเข้าถึงได้แบบสาธารณะถ้าหากเปิดใช้งาน เส้นเวลาของบทบาทนั้นจะไม่ถูกเปิดเผยต่อสาธารณะ ถึงแม้ว่าจะไม่เปิดเผยต่อสาธารณะแม้แต่ว่า...จะตั้งค่าไว้ยังไงก็ตาม" + displayOrder: "ตำแหน่ง" + descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำแหน่ง UI ก็ยิ่งสูงขึ้นนะ" + canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" + descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" priority: "ลำดับความสำคัญ" _priority: low: "ต่ำ" middle: "ปานกลาง" high: "สูง" _options: - gtlAvailable: "สามารถดูไทม์ไลน์ทั่วโลกได้" - ltlAvailable: "สามารถดูไทม์ไลน์ท้องถิ่นได้" - canPublicNote: "สามารถโพสต์แบบสาธารณะ" - mentionMax: "จำนวนการกล่าวถึงสูงสุดต่อโน้ต" - canInvite: "สร้างรหัสเชิญเข้าเซิร์ฟเวอร์" - inviteLimit: "จำกัดการเชิญ" - inviteLimitCycle: "คูลดาวน์ในการเชิญ" - inviteExpirationTime: "วันหมดอายุของรหัสการเชิญ" - canManageCustomEmojis: "จัดการเอโมจิที่กำหนดเอง" - canManageAvatarDecorations: "จัดการตกแต่งอวตาร" + gtlAvailable: "การดูไทม์ไลน์ทั่วโลก" + ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น" + canPublicNote: "สามารถส่งโน้ตสาธารณะ" + canInvite: "สร้างรหัสเชิญอินสแตนซ์" + canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" driveCapacity: "ความจุของไดรฟ์" alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ" - canUpdateBioMedia: "อนุญาตให้ปรับปรุงไอคอนและแบนเนอร์" pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้" antennaMax: "จำนวนสูงสุดของเสาอากาศ" wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ" @@ -1780,26 +1381,13 @@ _role: noteEachClipsMax: "จำนวนโน้ตสูงสุดภายในคลิป" userListMax: "จำนวนรายชื่อผู้ใช้สูงสุด" userEachUserListsMax: "จำนวนผู้ใช้สูงสุดภายในรายการผู้ใช้" - rateLimitFactor: "อัตราการจำกัด" - descriptionOfRateLimitFactor: "ยิ่งตัวเลขน้อยก็ยิ่งจำกัดน้อย ยิ่งมากก็ยิ่งเข้มงวดมากขึ้น" + rateLimitFactor: "ขีดจำกัดอัตรา" + descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า" canHideAds: "ซ่อนโฆษณา" canSearchNotes: "การใช้การค้นหาโน้ต" - canUseTranslator: "การใช้งานแปล" - avatarDecorationLimit: "จำนวนการตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้" - canImportAntennas: "อนุญาตให้นำเข้าเสาอากาศ" - canImportBlocking: "อนุญาตให้นำเข้าการบล็อก" - canImportFollowing: "อนุญาตให้นำเข้ารายการต่อไปนี้" - canImportMuting: "อนุญาตให้นำเข้าการปิดกั้น" - canImportUserLists: "อนุญาตให้นำเข้ารายการ" _condition: - roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ" - isLocal: "ผู้ใช้ท้องถิ่น" + isLocal: "ผู้ใช้ภายใน" isRemote: "ผู้ใช้ระยะไกล" - isCat: "ผู้ใช้ที่เป็นแมว" - isBot: "ผู้ใช้ที่เป็นบอต" - isSuspended: "ผู้ใช้ที่ถูกระงับ" - isLocked: "ผู้ใช้บัญชีไม่เปิดเผยสาธารณะ" - isExplorable: "ผู้ใช้ที่เปิดใช้งาน “ทำให้บัญชีของฉันค้นหาได้ง่ายขึ้น”" createdLessThan: "สร้างน้อยกว่า" createdMoreThan: "สร้างมากกว่า" followersLessThanOrEq: "จำนวนผู้ติดตามน้อยกว่าหรือเท่ากับ\n" @@ -1812,32 +1400,31 @@ _role: or: "หรือ" not: "ไม่" _sensitiveMediaDetection: - description: "ใช้ Machine Learning เพื่อตรวจจับสื่อที่มีเนื้อหาละเอียดอ่อนโดยอัตโนมัติและใช้เพื่อการกลั่นกรอง ภาระของเซิร์ฟเวอร์จะเพิ่มขึ้นเล็กน้อย" - sensitivity: "ความไวในการตรวจจับ" - sensitivityDescription: "เมื่อความไวต่ำ Misdetection (ผลบวกลวง) จะลดลง, เมื่อความไวสูง Missed detection (ผลลบลวง) จะลดลง" - setSensitiveFlagAutomatically: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน" + description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" + sensitivity: "การตรวจจับความไว" + sensitivityDescription: "การลดความไวนั้นจะนำไปสู่การตรวจจับที่ผิดพลาดน้อยลง (ผลบวกที่ผิดพลาด) แต่ในขณะที่การเพิ่มนั้นจะนำไปสู่การตรวจหาที่พลาดน้อยลง (ผลลบเท็จ)" + setSensitiveFlagAutomatically: "ทำเครื่องหมายว่าเป็น NSFW" setSensitiveFlagAutomaticallyDescription: "ผลลัพธ์ของการตรวจจับภายในนั้นจะยังคงอยู่ ถึงแม้ว่าจะปิดตัวเลือกนี้" analyzeVideos: "เปิดใช้งานวิเคราะห์ของวิดีโอ" analyzeVideosDescription: "การวิเคราะห์วิดีโอนอกเหนือจากรูปภาพนั้น การทำสิ่งนี้จะทำให้เพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" _emailUnavailable: used: "ที่อยู่อีเมลนี้ได้ถูกใช้ไปแล้ว" format: "รูปแบบของที่อยู่อีเมลนี้ไม่ถูกต้อง" - disposable: "ไม่สามารถใช้อีเมลชั่วคราวได้" + disposable: "ที่อยู่อีเมลที่ใช้แล้วทิ้งนั้นไม่สามารถใช้ได้" mx: "เซิร์ฟเวอร์อีเมลนี้ไม่ถูกต้อง" smtp: "เซิร์ฟเวอร์อีเมลนี้ไม่มีการตอบสนอง" - banned: "คุณไม่สามารถลงทะเบียนด้วยที่อยู่อีเมลนี้ได้" _ffVisibility: - public: "สาธารณะ" + public: "เผยแพร่" followers: "ปรากฏให้แก่ผู้ติดตามเท่านั้น" private: "ส่วนตัว" _signup: - almostThere: "เกือบจะเสร็จแล้ว" - emailAddressInfo: "กรุณากรอกที่อยู่อีเมลที่คุณใช้ ที่อยู่อีเมลของคุณจะไม่ถูกเผยแพร่สู่สาธารณชน" - emailSent: "อีเมลยืนยันได้ถูกส่งไปยังที่อยู่อีเมลที่คุณป้อน ({email}) แล้ว กรุณาติดตามลิงก์ในอีเมลเพื่อสร้างบัญชีให้เสร็จสมบูรณ์ ลิงก์ที่ให้ไว้จะหมดอายุใน 30 นาที" + almostThere: "เกือบจะมี" + emailAddressInfo: "โปรดกรอกอีเมลของคุณ มันจะไม่เปิดเผยต่อสาธารณะ" + emailSent: "เราได้ส่งอีเมลยืนยันไปยังที่อยู่อีเมลของคุณแล้วนะ ({email}) โปรดคลิกลิงก์ที่รวมไว้เพื่อสร้างบัญชีให้เสร็จสิ้น" _accountDelete: accountDelete: "ลบบัญชีผู้ใช้" mayTakeTime: "เนื่องจากการลบบัญชีนี้จะเป็นกระบวนการที่ต้องใช้ทรัพยากรมาก จึงอาจจะต้องใช้เวลาสักครู่ถึงจะเสร็จสมบูรณ์ ทั้งนี้ขึ้นอยู่กับจำนวนเนื้อหาที่คุณสร้างและจำนวนไฟล์ที่คุณอัปโหลดนะ" - sendEmail: "เมื่อการลบบัญชีเสร็จสิ้น การแจ้งเตือนจะถูกส่งไปยังที่อยู่อีเมลที่ลงทะเบียนไว้" + sendEmail: "เมื่อการลบบัญชีนี้เสร็จสิ้น เราอาจจะส่งอีเมลไปยังที่อยู่อีเมลของคุณที่เคยลงทะเบียนไว้กับบัญชีนี้นะ" requestAccountDelete: "ร้องขอให้ลบบัญชี" started: "การลบได้เริ่มต้นขึ้น" inProgress: "ปัจจุบันกำลังดำเนินการลบอยู่" @@ -1846,19 +1433,15 @@ _ad: reduceFrequencyOfThisAd: "แสดงโฆษณานี้ให้น้อยลง" hide: "ไม่ต้องแสดง" timezoneinfo: "วันในสัปดาห์นี้จะถูกกำหนดจากโซนเวลาของเซิร์ฟเวอร์" - adsSettings: "ตั้งค่าการโฆษณา" - notesPerOneAd: "อัปเดตช่วงเวลาตำแหน่งโฆษณาแบบเรียลไทม์ (จำนวนโน้ตต่อโฆษณา)" - setZeroToDisable: "ตั้งค่านี้ให้เป็น 0 เพื่อปิดใช้งานโฆษณาอัปเดตแบบเรียลไทม์" - adsTooClose: "เนื่องจากช่วงเวลาการแสดงโฆษณาสั้นมาก ประสบการณ์ผู้ใช้จึงอาจลดลงอย่างมาก" _forgotPassword: enterEmail: "ป้อนที่อยู่อีเมลที่คุณเคยใช้ในการลงทะเบียนไว้ ลิงก์ที่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูกส่งไปนะ" - ifNoEmail: "หากลงทะเบียนแบบไม่ใช้อีเมล โปรดติดต่อผู้ดูแลระบบ" - contactAdmin: "เนื่องจากเซิร์ฟเวอร์นี้ไม่รองรับการส่งอีเมล หากต้องการรีเซ็ตรหัสผ่าน กรุณาติดต่อผู้ดูแลระบบ" + ifNoEmail: "ถ้าหากคุณไม่ได้ใช้อีเมลระหว่างการลงทะเบียน กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์แทนนะ" + contactAdmin: "อินสแตนซ์นี้ไม่รองรับการใช้งานที่อยู่อีเมลนี้ กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์เพื่อรีเซ็ตรหัสผ่านของคุณแทน" _gallery: my: "แกลลอรี่ของฉัน" liked: "โพสต์ที่ถูกใจ" - like: "ถูกใจ!" - unlike: "เลิกถูกใจ" + like: "ชื่นชอบ" + unlike: "ลบไลค์" _email: _follow: title: "ได้ติดตามคุณ" @@ -1868,26 +1451,24 @@ _plugin: install: "ติดตั้งปลั๊กอิน" installWarn: "กรุณาอย่าติดตั้งปลั๊กอินที่ไม่น่าเชื่อถือนะคะ" manage: "จัดการปลั๊กอิน" - viewSource: "ดูต้นฉบับ" - viewLog: "แสดงปูม" _preferencesBackups: - list: "การตั้งค่าที่สำรองไว้" - saveNew: "บันทึกการตั้งค่าสำรองใหม่" + list: "สร้างการสำรองข้อมูล" + saveNew: "บันทึกใหม่" loadFile: "โหลดจากไฟล์" apply: "นำไปใช้กับอุปกรณ์นี้" save: "บันทึก" - inputName: "กรุณาป้อนชื่อการตั้งค่าสำรองนี้" + inputName: "กรุณาป้อนชื่อสำหรับข้อมูลสำรองนี้" cannotSave: "การบันทึกล้มเหลว" - nameAlreadyExists: "มีการตั้งค่าสำรองชื่อ “{name}” อยู่แล้ว กรุณาป้อนชื่ออื่น" - applyConfirm: "ต้องการใช้การตั้งค่าสำรอง “{name}” กับอุปกรณ์นี้ใช่ไหม? การตั้งค่าที่มีอยู่บนอุปกรณ์นี้จะถูกเขียนทับ" - saveConfirm: "บันทึกการตั้งค่าสำรองเป็น {name} ใช่ไหม?" - deleteConfirm: "ต้องการลบ {name} ใช่ไหม?" - renameConfirm: "ต้องการเปลี่ยนชื่อจาก “{old}” เป็น “{new}” ใช่ไหม?" - noBackups: "ไม่มีการตั้งค่าสำรอง สามารถบันทึกการตั้งค่าไคลเอนต์ปัจจุบันไปยังเซิร์ฟเวอร์ด้วย “บันทึกการตั้งค่าสำรองใหม่”" + nameAlreadyExists: "มีข้อมูลสำรองชื่อ \"{name}\" นี้อยู่แล้ว กรุณาป้อนชื่ออื่นนะ" + applyConfirm: "คุณต้องการใช้ข้อมูลสำรอง \"{name}\" กับอุปกรณ์นี้อย่างงั้นจริงหรอ การตั้งค่าที่มีอยู่ของอุปกรณ์นี้จะถูกเขียนทับนะ" + saveConfirm: "บันทึกข้อมูลสำรองเป็น {name} มั้ย?" + deleteConfirm: "ลบข้อมูลสำรอง {name} มั้ย?" + renameConfirm: "เปลี่ยนชื่อข้อมูลสำรองนี้จาก \"{old}\" เป็น \"{new}\" หรือป่าว" + noBackups: "ไม่มีข้อมูลสำรองนะ คุณสามารถสำรองข้อมูลการตั้งค่าไคลเอนต์ของคุณบนเซิร์ฟเวอร์นี้โดยใช้ \"สร้างการสำรองข้อมูลใหม่\"ได้นะ" createdAt: "สร้างเมื่อ: {date} {time}" updatedAt: "อัปเดตเมื่อ: {date} {time}" cannotLoad: "การโหลดล้มเหลว" - invalidFile: "รูปแบบไฟล์ไม่ถูกต้อง" + invalidFile: "รูปแบบไฟล์ไม่ถูกต้องนะ" _registry: scope: "สโคป" key: "คีย์" @@ -1899,17 +1480,10 @@ _aboutMisskey: contributors: "ผู้สนับสนุนหลัก" allContributors: "ผู้มีส่วนร่วมทั้งหมด" source: "ซอร์สโค้ด" - original: "ต้นฉบับ" - thisIsModifiedVersion: "{name} ใช้ Misskey เวอร์ชันดัดแปลง" - translation: "แปลภาษา Misskey" + translation: "รับแปลภาษา Misskey" donate: "บริจาคให้กับ Misskey" - morePatrons: "และอีกหลายท่านที่ไม่ได้เอ่ยนาม ขอบคุณที่ร่วมช่วยเหลือตลอดมานะคะ 🥰" - patrons: "ผู้อุปถัมภ์" - projectMembers: "สมาชิกในโครงการ" -_displayOfSensitiveMedia: - respect: "ซ่อนสื่อที่มีเนื้อหาละเอียดอ่อน" - ignore: "แสดงสื่อที่มีเนื้อหาละเอียดอ่อน" - force: "ซ่อนสื่อทั้งหมด" + morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ ขอขอบคุณ! 🥰" + patrons: "สมาชิกพันธมิตร" _instanceTicker: none: "ไม่ต้องแสดง" remote: "แสดงสำหรับผู้ใช้ระยะไกล" @@ -1919,18 +1493,17 @@ _serverDisconnectedBehavior: dialog: "แสดงกล่องโต้ตอบคำเตือน" quiet: "แสดงคำเตือนที่ไม่เป็นการรบกวน" _channel: - create: "สร้างช่องใหม่" - edit: "แก้ไขช่อง" + create: "สร้างแชนแนลใหม่" + edit: "แก้ไขแชนแนล" setBanner: "เซตแบนเนอร์" removeBanner: "ลบแบนเนอร์" featured: "เทรนด์" owned: "เจ้าของ" following: "ติดตามแล้ว" usersCount: "{n} ผู้เข้าร่วม" - notesCount: "มี {n} โน้ต" + notesCount: "{n} โน้ต" nameAndDescription: "ชื่อและคำอธิบาย" nameOnly: "ชื่อเท่านั้น" - allowRenoteToExternal: "อนุญาตให้รีโน้ตและอ้างอิงนอกช่องได้" _menuDisplay: sideFull: "ด้านข้าง" sideIcon: "ด้านข้าง (ไอคอน)" @@ -1938,13 +1511,18 @@ _menuDisplay: hide: "ซ่อน" _wordMute: muteWords: "ปิดเสียงคำ" - muteWordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่อนไข AND, หรือขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR" + muteWordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ" muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป" + softDescription: "ซ่อนโน้ตให้ตรงตามเงื่อนไขที่ตั้งไว้จากไทม์ไลน์" + hardDescription: "ป้องกันไม่ให้โน้ตย่อที่ตรงตามเงื่อนไขที่ตั้งไว้ไม่ให้ถูกเพิ่มลงในไทม์ไลน์ นอกจากนี้ โน้ตเหล่านี้จะไม่ถูกเพิ่มลงในไทม์ไลน์แม้ว่าจะมีการเปลี่ยนแปลงเงื่อนไขยังไงก็ตาม" + soft: "ซอฟ" + hard: "ยาก" + mutedNotes: "ปิดเสียงโน้ต" _instanceMute: - instanceMuteDescription: "ปิดเสียง “โน้ต/รีโน้ต” ทั้งหมดจากเซิร์ฟเวอร์ที่ระบุไว้ รวมถึงโน้ตของผู้ใช้ที่ตอบกลับผู้ใช้จากเซิร์ฟเวอร์ที่ถูกปิดเสียง" + instanceMuteDescription: "การดำเนินการนี้จะปิดเสียง\"โน้ต/รีโน้ต\"จากอินสแตนซ์ที่อยู่ในรายการ รวมถึงบันทึกของผู้ใช้ที่ตอบกลับผู้ใช้จากอินสแตนซ์ที่ปิดเสียง" instanceMuteDescription2: "คั่นด้วยการขึ้นบรรทัดใหม่" - title: "ซ่อนโน้ตจากเซิร์ฟเวอร์ที่มีระบุไว้" - heading: "เซิร์ฟเวอร์ที่ถูกปิดเสียง" + title: "ซ่อนโน้ตจากอินสแตนซ์ที่มีอยู่ในรายการ" + heading: "รายชื่ออินสแตนซ์ที่ถูกปิดเสียง" _theme: explore: "สำรวจธีม" install: "ติดตั้งธีม" @@ -1976,8 +1554,8 @@ _theme: importInfo: "ถ้าหากต้องการป้อนโค้ดที่นี่ คุณยังสามารถนำเข้าไปยังโปรแกรมแก้ไขธีมได้" deleteConstantConfirm: "คุณต้องการลบค่าคงที่ {const} หรือป่าว?" keys: - accent: "สีหลัก" - bg: "พื้นหลัง" + accent: "เน้น" + bg: "ภาพพื้นหลัง" fg: "ข้อความ" focus: "โฟกัส" indicator: "ตัวบ่งชี้" @@ -1986,6 +1564,7 @@ _theme: header: "ส่วนหัว" navBg: "พื้นหลังแถบด้านข้าง" navFg: "ข้อความแถบด้านข้าง" + navHoverFg: "ข้อความแถบด้านข้าง (โฮเวอร์)" navActive: "ข้อความแถบด้านข้าง (ใช้งานอยู่)" navIndicator: "ตัวระบุแถบด้านข้าง" link: "ลิงก์" @@ -2002,27 +1581,30 @@ _theme: infoFg: "ข้อความข้อมูล" infoWarnBg: "คำเตือนพื้นหลัง" infoWarnFg: "คำเตือนข้อความ" + cwBg: "ปุ่ม CW พื้นหลัง" + cwFg: "ปุ่ม CW ข้อความ" + cwHoverBg: "ปุ่ม CW พื้นหลัง (โฮเวอร์)" toastBg: "ประวัติการแจ้งเตือน" toastFg: "ข้อความแจ้งเตือน" buttonBg: "ปุ่มพื้นหลัง" buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวอร์)" inputBorder: "เส้นขอบของช่องป้อนข้อมูล" + listItemHoverBg: "รายการไอเทมพื้นหลัง (โฮเวอร์)" + driveFolderBg: "พื้นหลังโฟลเดอร์ไดรฟ์" + wallpaperOverlay: "วอลล์เปเปอร์ซ้อนทับ" badge: "ตรา" messageBg: "พื้นหลังแชท" + accentDarken: "เน้น (มืด)" + accentLighten: "เน้น (สว่าง)" fgHighlighted: "ข้อความที่ไฮไลต์" _sfx: - note: "โน้ต" + note: "หมายเหตุ" noteMy: "โน้ตของตัวเอง" notification: "การเเจ้งเตือน" - reaction: "เมื่อเลือกรีแอคชั่น" -_soundSettings: - driveFile: "ใช้เสียงจากไดรฟ์" - driveFileWarn: "เลือกไฟล์ในไดรฟ์ของคุณ" - driveFileTypeWarn: "ไม่รองรับไฟล์นี้" - driveFileTypeWarnDescription: "กรุณาเลือกไฟล์เสียง" - driveFileDurationWarn: "เสียงยาวเกินไป" - driveFileDurationWarnDescription: "การใช้เสียงที่ยาว อาจรบกวนการใช้งาน Misskey, ต้องการดำเนินการต่อใช่ไหม?" - driveFileError: "ไม่สามารถโหลดไฟล์เสียงได้ กรุณาเปลี่ยนแปลงการตั้งค่า" + chat: "แชท" + chatBg: "แชท (พื้นหลัง)" + antenna: "เสาอากาศ" + channel: "การแจ้งเตือนช่อง" _ago: future: "อนาคต" justNow: "เมื่อกี๊นี้" @@ -2034,138 +1616,81 @@ _ago: monthsAgo: "{n} เดือนที่แล้ว" yearsAgo: "{n} ปีที่ผ่านมา" invalid: "ไม่พบผลลัพธ์" -_timeIn: - seconds: "ใน {n} วินาที" - minutes: "ใน {n} นาที" - hours: "ใน {n} ชั่วโมง" - days: "ใน {n} วัน" - weeks: "ใน {n} สัปดาห์" - months: "ใน {n} เดือน" - years: "ใน {n} ปี" _time: second: "วินาที" minute: "นาที" hour: "ชั่วโมง" day: "วัน" +_timelineTutorial: + title: "วิธีใช้งาน Misskey" + step2_1: "มาลองโพสต์โน้ตต่อไปกัน คุณสามารถทำได้โดยการกดปุ่มที่มีไอคอนดินสอ" + step2_2: "ยังไงไหนลองเขียนแนะนำตัวเองหรือแค่ \"สวัสดี {name}!\" ถ้าคุณไม่รู้สึกเหมือนมัน?" + step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?" + step3_2: "ไชโย! ตอนนี้โน้ตย่อแรกของคุณได้ปรากฏบนไทม์ไลน์ของคุณแล้วนะ" + step4_1: "คุณสามารถเพิ่ม \"การตอบสนอง\" ในโน้ตได้" + step4_2: "หากต้องการแนบการแสดงความรู้สึก ให้กดเครื่องหมาย \"+\" บนโน้ตแล้วเลือกอิโมจิที่คุณต้องการแสดงความรู้สึกที่ตนเองชอบได้เลย" _2fa: alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว" registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์" + passwordToTOTP: "กรอกรหัสผ่าน" step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ" step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้" - step2Uri: "ป้อนใส่ URL ดังต่อไปนี้ถ้าหากคุณใช้โปรแกรมเดสก์ท็อป" + step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้" + step2Url: "คุณยังสามารถป้อนบน URL นี้หากคุณใช้โปรแกรมเดสก์ท็อป:" step3Title: "ป้อนรหัสยืนยัน" step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า" - setupCompleted: "ตั้งค่าสำเร็จแล้ว" step4: "นับจากนี้เป็นต้นไปการพยายามเข้าสู่ระบบในอนาคตนั้น อาจจะต้องขอโทเค็นในการเข้าสู่ระบบดังกล่าว" securityKeyNotSupported: "เบราว์เซอร์ของคุณไม่รองรับคีย์ความปลอดภัยนะ" registerTOTPBeforeKey: "กรุณาตั้งค่าแอปยืนยันตัวตนเพื่อลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" securityKeyInfo: "นอกจากนี้การตรวจสอบความถูกต้องด้วยลายนิ้วมือหรือ PIN แล้ว คุณยังสามารถตั้งค่าการตรวจสอบสิทธิ์ผ่านคีย์ความปลอดภัยของฮาร์ดแวร์ที่รองรับ FIDO2 เพื่อเพิ่มความปลอดภัยให้กับบัญชีของคุณ" + chromePasskeyNotSupported: "ขณะนี้ยังไม่รองรับรหัสผ่านของ Chrome" registerSecurityKey: "ลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" securityKeyName: "ป้อนชื่อคีย์" tapSecurityKey: "กรุณาทำตามเบราว์เซอร์ของคุณเพื่อลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" removeKey: "ลบคีย์ความปลอดภัยออก" removeKeyConfirm: "ลบข้อมูลสำรอง {name} มั้ย?" whyTOTPOnlyRenew: "ไม่สามารถลบแอปตัวรับรองความถูกต้องได้ตราบใดที่มีการลงทะเบียนคีย์ความปลอดภัยไว้แล้ว" - renewTOTP: "ตั้งค่าแอปยืนยันตัวตน" + renewTOTP: "กำหนดค่าแอพตัวตรวจสอบสิทธิ์ใหม่" renewTOTPConfirm: "วิธีการแบบนี้จะทําให้รหัสยืนยันจากแอพก่อนหน้าของคุณหยุดทํางานเลยนะ" renewTOTPOk: "ตั้งค่าคอนฟิกใหม่" renewTOTPCancel: "ไม่เป็นไร" - checkBackupCodesBeforeCloseThisWizard: "โปรดตรวจสอบรหัสแบ๊กอัปด้านล่างก่อนที่จะปิดวิซาร์ดนี้" - backupCodes: "รหัสแบ๊กอัป" - backupCodesDescription: "หากแอปยืนยันตัวตนของคุณไม่พร้อมใช้งาน คุณสามารถใช้รหัสสำรองด้านล่างเพื่อเข้าถึงบัญชีของคุณได้ อย่าลืมเก็บรหัสเหล่านี้ไว้ในที่ปลอดภัย แต่ละรหัสสามารถใช้ได้เพียงครั้งเดียวเท่านั้น" - backupCodeUsedWarning: "รหัสแบ๊กอัปถูกใช้งานแล้ว หากแอปพลิเคชันการยืนยันตัวตนไม่สามารถใช้งานได้ ให้รีบทำการตั้งค่าแอปฯใหม่โดยเร็วที่สุด" - backupCodesExhaustedWarning: "รหัสแบ๊กอัปทั้งหมดถูกใช้งานแล้ว หากยังไม่สามารถใช้แอปพลิเคชันการยืนยันตัวตนได้ก็จะไม่สามารถเข้าถึงบัญชีนี้ได้อีกต่อไป กรุณาลงทะเบียนแอปพลิเคชันการยืนยันตัวตนใหม่" - moreDetailedGuideHere: "คลิกที่นี่เพื่อดูคำแนะนำโดยละเอียด" _permissions: - "read:account": "ดูข้อมูลบัญชี" - "write:account": "แก้ไขข้อมูลบัญชี" - "read:blocks": "ดูรายชื่อผู้ใช้ที่ถูกบล็อก" - "write:blocks": "แก้ไขรายชื่อผู้ใช้ที่ถูกบล็อก" - "read:drive": "เข้าถึงไดรฟ์" - "write:drive": "จัดการไดรฟ์" + "read:account": "ดูข้อมูลบัญชีของคุณ" + "write:account": "แก้ไขข้อมูลบัญชีของคุณ" + "read:blocks": "ดูรายชื่อผู้ใช้ที่ถูกบล็อกของคุณ" + "write:blocks": "แก้ไขรายชื่อผู้ใช้ที่ถูกบล็อกของคุณ" + "read:drive": "เข้าถึงไฟล์และโฟลเดอร์ในไดรฟ์ของคุณ" + "write:drive": "แก้ไขหรือลบไฟล์และโฟลเดอร์ในไดรฟ์ของคุณ" "read:favorites": "ดูรายการโปรด" "write:favorites": "แก้ไขรายการโปรด" "read:following": "ดูข้อมูลว่าใครที่คุณติดตาม" "write:following": "ติดตามหรือเลิกติดตามบัญชีอื่น" - "read:messaging": "ดูแชท" + "read:messaging": "ดูแชทของคุณ" "write:messaging": "เขียนหรือลบข้อความแชท" - "read:mutes": "ดูรายชื่อผู้ใช้ที่ถูกปิดเสียง" + "read:mutes": "ดูรายชื่อผู้ใช้ที่ปิดเสียงของคุณ" "write:mutes": "แก้ไขรายชื่อผู้ใช้ที่ถูกปิดเสียง" "write:notes": "เขียนหรือลบโน้ต" - "read:notifications": "ดูการแจ้งเตือน" - "write:notifications": "จัดการแจ้งเตือน" - "read:reactions": "ดูรีแอคชั่น" - "write:reactions": "แก้ไขรีแอคชั่น" + "read:notifications": "ดูการแจ้งเตือนของคุณ" + "write:notifications": "จัดการแจ้งเตือนของคุณ" + "read:reactions": "ดูปฏิกิริยาของคุณ" + "write:reactions": "แก้ไขปฏิกิริยาของคุณ" "write:votes": "โหวตบนสำรวจความคิดเห็น" - "read:pages": "ดูหน้าเพจ" - "write:pages": "แก้ไขหรือลบเพจ" - "read:page-likes": "ดูรายการเพจที่ถูกใจไว้" - "write:page-likes": "แก้ไขรายการเพจที่ถูกใจ" - "read:user-groups": "ดูกลุ่มผู้ใช้" - "write:user-groups": "แก้ไขหรือลบกลุ่มผู้ใช้" - "read:channels": "ดูช่อง" - "write:channels": "แก้ไขช่อง" + "read:pages": "ดูหน้า" + "write:pages": "แก้ไขหรือลบเพจของคุณ" + "read:page-likes": "ดูไลค์ของคุณบนเพจ" + "write:page-likes": "แก้ไขการถูกใจของคุณบนเพจ" + "read:user-groups": "ดูกลุ่มผู้ใช้ของคุณ" + "write:user-groups": "แก้ไขหรือลบกลุ่มผู้ใช้ของคุณ" + "read:channels": "ดูแชนแนลของคุณ" + "write:channels": "แก้ไขแชนแนลของคุณ" "read:gallery": "ดูแกลเลอรี่" - "write:gallery": "แก้ไขแกลเลอรี" - "read:gallery-likes": "ดูแกลเลอรีที่ถูกใจไว้" - "write:gallery-likes": "จัดการแกลเลอรีที่ถูกใจไว้" - "read:flash": "ดู Play" - "write:flash": "แก้ไข Play" - "read:flash-likes": "ดูรายการ play ที่ถูกใจไว้" - "write:flash-likes": "แก้ไขรายการ play ที่ถูกใจไว้" - "read:admin:abuse-user-reports": "ดูรายงานจากผู้ใช้" - "write:admin:delete-account": "ลบบัญชีผู้ใช้" - "write:admin:delete-all-files-of-a-user": "ลบไฟล์ทั้งหมดของผู้ใช้" - "read:admin:index-stats": "ดูข้อมูลเกี่ยวกับดัชนีฐานข้อมูล" - "read:admin:table-stats": "ดูข้อมูลเกี่ยวกับตารางในฐานข้อมูล" - "read:admin:user-ips": "ดูที่อยู่ IP ของผู้ใช้" - "read:admin:meta": "ดูข้อมูลอภิพันธุ์ของอินสแตนซ์" - "write:admin:reset-password": "รีเซ็ตรหัสผ่านของผู้ใช้" - "write:admin:resolve-abuse-user-report": "แก้ไขรายงานจากผู้ใช้" - "write:admin:send-email": "ส่งอีเมล" - "read:admin:server-info": "ดูข้อมูลเซิร์ฟเวอร์" - "read:admin:show-moderation-log": "ดูปูมการควบคุมดูแล" - "read:admin:show-user": "ดูข้อมูลส่วนตัวของผู้ใช้" - "write:admin:suspend-user": "ระงับผู้ใช้" - "write:admin:unset-user-avatar": "ลบอวตารผู้ใช้" - "write:admin:unset-user-banner": "ลบแบนเนอร์ผู้ใช้" - "write:admin:unsuspend-user": "ยกเลิกการระงับผู้ใช้" - "write:admin:meta": "จัดการข้อมูลอภิพันธุ์ของอินสแตนซ์" - "write:admin:user-note": "จัดการโน้ตการกลั่นกรอง" - "write:admin:roles": "จัดการบทบาท" - "read:admin:roles": "ดูบทบาท" - "write:admin:relays": "จัดการรีเลย์" - "read:admin:relays": "ดูรีเลย์" - "write:admin:invite-codes": "จัดการรหัสเชิญ" - "read:admin:invite-codes": "ดูรหัสเชิญ" - "write:admin:announcements": "จัดการประกาศ" - "read:admin:announcements": "ดูประกาศ" - "write:admin:avatar-decorations": "จัดการการตกแต่งอวตาร" - "read:admin:avatar-decorations": "ดูการตกแต่งอวตาร" - "write:admin:federation": "จัดการข้อมูลเกี่ยวกับสหพันธ์" - "write:admin:account": "จัดการบัญชีผู้ใช้" - "read:admin:account": "ดูข้อมูลเกี่ยวกับผู้ใช้" - "write:admin:emoji": "จัดการเอโมจิ" - "read:admin:emoji": "ดูเอโมจิ" - "write:admin:queue": "จัดการคิวงาน" - "read:admin:queue": "ดูข้อมูลเกี่ยวกับคิวงาน" - "write:admin:promo": "จัดการโน้ตโปรโมชั่น" - "write:admin:drive": "จัดการไดรฟ์ของผู้ใช้" - "read:admin:drive": "ดูข้อมูลเกี่ยวกับไดรฟ์ของผู้ใช้" - "read:admin:stream": "ใช้ Websocket API สำหรับผู้ดูแลระบบ" - "write:admin:ad": "จัดการโฆษณา" - "read:admin:ad": "ดูโฆษณา" - "write:invite-codes": "สร้างรหัสเชิญ" - "read:invite-codes": "รับรหัสเชิญ" - "write:clip-favorite": "จัดการคลิปที่ถูกใจ" - "read:clip-favorite": "ดูคลิปที่ถูกใจ" - "read:federation": "รับข้อมูลเกี่ยวกับสหพันธ์" - "write:report-abuse": "รายงานการละเมิด" - "write:chat": "เขียนหรือลบข้อความแชท" + "write:gallery": "แก้ไขแกลเลอรี่ของคุณ" + "read:gallery-likes": "ดูรายการโพสต์ในแกลเลอรีที่ชอบของคุณ" + "write:gallery-likes": "แก้ไขรายการโพสต์ในแกลเลอรีที่ชอบของคุณ" _auth: shareAccessTitle: "การให้สิทธิ์แอปพลิเคชัน" shareAccess: "คุณต้องการอนุญาตให้ \"{name}\" เข้าถึงบัญชีนี้เลยมั้ย?" - shareAccessAsk: "ต้องการอนุญาตให้แอปพลิเคชันนี้เข้าถึงบัญชีของคุณใช่ไหม?" + shareAccessAsk: "คุณแน่ใจแล้วจริงๆหรอว่าต้องการอนุญาตให้แอปพลิเคชันนี้เข้าถึงบัญชีของคุณแน่ใจแล้วหรอ?" permission: "{name} ได้ขอสิทธิ์การเข้าถึงดังต่อไปนี้" permissionAsk: "แอปพลิเคชันนี้ขอสิทธิ์ดังต่อไปนี้" pleaseGoBack: "กรุณากลับไปที่แอปพลิเคชัน" @@ -2177,7 +1702,6 @@ _antennaSources: homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม" users: "โน้ตจากผู้ใช้ที่เฉพาะเจาะจง" userList: "โน้ตจากรายชื่อผู้ใช้ที่ระบุ" - userBlacklist: "โน้ตทั้งหมดยกเว้นโน้ตของผู้ใช้ที่ต้องระบุเจาะจงตั้งแต่หนึ่งรายขึ้นไป" _weekday: sunday: "วันอาทิตย์" monday: "วันจันทร์" @@ -2188,7 +1712,7 @@ _weekday: saturday: "วันเสาร์" _widgets: profile: "โปรไฟล์" - instanceInfo: "ข้อมูลเซิร์ฟเวอร์" + instanceInfo: "ข้อมูล อินสแตนซ์" memo: "โน้ตแปะ" notifications: "การเเจ้งเตือน" timeline: "ไทม์ไลน์" @@ -2202,21 +1726,20 @@ _widgets: digitalClock: "นาฬิกาดิจิตอล" unixClock: "นาฬิกา UNIX" federation: "สหพันธ์" - instanceCloud: "กลุ่มเมฆเซิร์ฟเวอร์" + instanceCloud: "อินสแตนซ์คลาวด์" postForm: "แบบฟอร์มการโพสต์" slideshow: "แสดงภาพนิ่ง" button: "ปุ่ม" onlineUsers: "ผู้ใช้ที่ออนไลน์" jobQueue: "คิวงาน" serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์" - aiscript: " คอนโซล AiScript" - aiscriptApp: "แอป AiScript" - aichan: "藍 (ไอ)" + aiscript: "AiScript คอนโซล" + aiscriptApp: "AiScript แอพ" + aichan: "เอไอ" userList: "รายชื่อผู้ใช้" _userList: - chooseList: "เลือกรายชื่อ" + chooseList: "เลือกรายการ" clicker: "คลิกเกอร์" - birthdayFollowings: "วันเกิดผู้ใช้ในวันนี้" _cw: hide: "ซ่อน" show: "โหลดเพิ่มเติม" @@ -2224,53 +1747,53 @@ _cw: files: "{count} ไฟล์" _poll: noOnlyOneChoice: "จำเป็นต้องมีอย่างน้อยสองตัวเลือก" - choiceN: "ตัวเลือกที่ {n}" - noMore: "เพิ่มตัวเลือกอีกไม่ได้แล้ว" + choiceN: "ตัวเลือก {n}" + noMore: "คุณไม่สามารถเพิ่มตัวเลือกอื่นได้" canMultipleVote: "สามารถตอบได้หลายคำตอบ" - expiration: "สิ้นสุดโพล" - infinite: "ไม่กำหนดระยะเวลา" - at: "ระบุวันเวลา" - after: "ระบุระยะเวลา" + expiration: "สิ้นสุดการสำรวจความคิดเห็น" + infinite: "ไม่ต้องเลย" + at: "จบที่..." + after: "สิ้นสุดหลัง..." deadlineDate: "วันสิ้นสุด" - deadlineTime: "เวลา" + deadlineTime: "ชั่วโมง" duration: "ระยะเวลา" votesCount: "{n} คะแนนเสียง" - totalVotes: "ทั้งหมด {n} คะแนนเสียง" + totalVotes: "{n} คะแนนเสียงทั้งหมด" vote: "โหวต" showResult: "ดูผลลัพธ์" voted: "โหวตแล้ว" closed: "สิ้นสุดแล้ว" - remainingDays: "เหลืออีก {d} วัน {h} ชั่วโมง" - remainingHours: "เหลืออีก {h} ชั่วโมง {m} นาที" - remainingMinutes: "เหลืออีก {m} นาที {s} วินาที" - remainingSeconds: "เหลืออีก {s} วินาที" + remainingDays: "{d} วัน(s) {h} ชั่วโมง(s) ที่เหลืออยู่" + remainingHours: "{h} ชั่วโมง(s) {m} นาที(s) ที่เหลืออยู่" + remainingMinutes: "{m} นาที(s) {s} วินาที(s) ที่เหลืออยู่" + remainingSeconds: "{s} นาที(s) ที่เหลืออยู่" _visibility: public: "สาธารณะ" publicDescription: "โน้ตของคุณจะปรากฏแก่ผู้ใช้ทุกคน" - home: "หน้าหลัก" - homeDescription: "โพสต์ลงไทม์ไลน์หลักเท่านั้น" + home: "หน้าแรก" + homeDescription: "โพสลงไทม์ไลน์ที่บ้านเท่านั้น" followers: "ผู้ติดตาม" - followersDescription: "เฉพาะผู้ติดตามเท่านั้นที่มองเห็นได้" + followersDescription: "ทำให้ผู้ติดตามนั้นมองเห็นแค่คุณเท่านั้น" specified: "ไดเร็ค" specifiedDescription: "ทำให้มองเห็นได้เฉพาะผู้ใช้ที่ระบุเท่านั้น" - disableFederation: "การปิดใช้งานสหพันธ์" - disableFederationDescription: "อย่าส่งข้อมูลไปยังเซิร์ฟเวอร์อื่น" + disableFederation: "ไม่มีสหภาพ" + disableFederationDescription: "อย่าส่งไปยังอินสแตนซ์อื่น" _postForm: replyPlaceholder: "ตอบกลับโน้ตนี้..." quotePlaceholder: "อ้างโน้ตนี้..." channelPlaceholder: "โพสต์ลงช่อง..." _placeholders: - a: "ตอนนี้เป็นยังไงบ้าง?" - b: "มีอะไรเกิดขึ้นหรือเปล่า?" - c: "กำลังคิดอะไรอยู่?" - d: "ต้องการจะพูดอะไรไหม?" - e: "มาเขียนกันเถอะ" + a: "คุณเป็นอะไรไปหรอ?" + b: "เกิดอะไรขึ้นรอบตัวคุณ?" + c: "คุณกำลังคิดอะไรอยู่?" + d: "คุณต้องการจะพูดอะไร?" + e: "เริ่มเขียน..." f: "กำลังรอให้คุณเขียน..." _profile: name: "ชื่อ" username: "ชื่อผู้ใช้" - description: "แนะนำตัว" - youCanIncludeHashtags: "คุณสามารถใส่แฮชแท็กในส่วนแนะนำตัวของคุณได้" + description: "ประวัติ" + youCanIncludeHashtags: "คุณยังสามารถใส่แฮชแท็กในประวัติของคุณได้นะ" metadata: "ข้อมูลเพิ่มเติม" metadataEdit: "แก้ไขข้อมูลเพิ่มเติม" metadataDescription: "ใช้สิ่งเหล่านี้ คุณสามารถแสดงฟิลด์ข้อมูลเพิ่มเติมในโปรไฟล์ของคุณ" @@ -2278,82 +1801,77 @@ _profile: metadataContent: "เนื้อหา" changeAvatar: "เปลี่ยนอวาตาร์" changeBanner: "เปลี่ยนแบนเนอร์" - verifiedLinkDescription: "หากป้อน URL ที่มีลิงก์ไปยังโปรไฟล์ของคุณ ไอคอนการยืนยันความเป็นเจ้าของจะแสดงถัดจากฟิลด์นั้น ๆ" - avatarDecorationMax: "คุณสามารถเพิ่มการตกแต่งได้สูงสุด {max}" - followedMessage: "ส่งข้อความเมื่อมีคนกดติดตาม" - followedMessageDescription: "ส่งข้อความเมื่อมีคนกดติดตามแล้ว" - followedMessageDescriptionForLockedAccount: "ถ้าหากคุณตั้งค่าให้คนอื่นต้องขออนุญาตก่อนที่จะติดตามคุณ ระบบจะขึ้นข้อความนี้ในตอนที่คุณอนุมัติให้เขาติดตาม" _exportOrImport: allNotes: "โน้ตทั้งหมด" - favoritedNotes: "โน้ตที่ถูกใจไว้" - clips: "คลิป" + favoritedNotes: "บันทึกที่ชื่นชอบ" followingList: "กำลังติดตาม" muteList: "ปิดเสียง" blockingList: "บล็อค" - userLists: "รายชื่อ" + userLists: "รายการ" excludeMutingUsers: "ยกเว้นผู้ใช้ที่ปิดเสียง" excludeInactiveUsers: "ยกเว้นผู้ใช้ที่ไม่ได้ใช้งาน" - withReplies: "รวมการตอบกลับจากผู้ใช้ที่ถูกนำเข้า ลงไทม์ไลน์" _charts: federation: "สหพันธ์" apRequest: "คำขอ" - usersIncDec: "การเพิ่มลดของจำนวนผู้ใช้" + usersIncDec: "ความแตกต่างของจำนวนผู้ใช้งาน" usersTotal: "จำนวนผู้ใช้งานทั้งหมด" activeUsers: "จำนวนผู้ใช้งานที่ยังมีความเคลื่อนไหวอยู่" - notesIncDec: "การเพิ่มลดของจำนวนโน้ต" - localNotesIncDec: "การเพิ่มลดของจำนวนโน้ตท้องถิ่น" - remoteNotesIncDec: "การเพิ่มลดของจำนวนโน้ตระยะไกล" + notesIncDec: "ความแตกต่างของจำนวนโน้ต" + localNotesIncDec: "ความแตกต่างของจำนวนโน้ตท้องถิ่น" + remoteNotesIncDec: "ความแตกต่างของจำนวนโน้ตระยะไกล" notesTotal: "จำนวนโน้ตทั้งหมด" - filesIncDec: "การเพิ่มลดของจำนวนไฟล์" + filesIncDec: "ความแตกต่างของจำนวนไฟล์" filesTotal: "จำนวนไฟล์ทั้งหมด" - storageUsageIncDec: "การเพิ่มลดในการใช้พื้นที่เก็บข้อมูล" + storageUsageIncDec: "ความแตกต่างในการใช้พื้นที่เก็บข้อมูล" storageUsageTotal: "การใช้พื้นที่เก็บข้อมูลทั้งหมด" _instanceCharts: requests: "คำขอ" - users: "การเพิ่มลดของจำนวนผู้ใช้งาน" + users: "ความแตกต่างของจำนวนผู้ใช้งาน" usersTotal: "จำนวนผู้ใช้งานสะสม" - notes: "การเพิ่มลดของจำนวนโน้ต" + notes: "ความแตกต่างของจำนวนโน้ต" notesTotal: "จำนวนโน้ตสะสม" - ff: "การเพิ่มลดของการติดตาม/ผู้ติดตาม" - ffTotal: "จำนวนสะสมของการติดตาม/ผู้ติดตาม" - cacheSize: "การเพิ่มลดขนาดของแคช" - cacheSizeTotal: "ขนาดแคชสะสม" - files: "การเพิ่มลดของจำนวนไฟล์" + ff: "ความแตกต่างของจำนวนผู้ใช้ที่ติดตาม / ผู้ติดตาม" + ffTotal: "จำนวนผู้ใช้งานที่ติดตามสะสม / ผู้ติดตาม" + cacheSize: "ความแตกต่างในขนาดของแคช" + cacheSizeTotal: "ขนาดแคชรวมที่สะสม" + files: "ความแตกต่างของจำนวนไฟล์" filesTotal: "จำนวนไฟล์สะสม" _timelines: - home: "หน้าหลัก" - local: "ท้องถิ่น" - social: "โซเชียล" + home: "หน้าแรก" + local: "ในพื้นที่" + social: "โซเชี่ยล" global: "ทั่วโลก" _play: - new: "สร้าง Play" - edit: "แก้ไข Play" - created: "สร้าง Play แล้ว" - updated: "แก้ไข Play แล้ว" - deleted: "ลบ Play แล้ว" - pageSetting: "ตั้งค่า Play" + new: "สร้างการเล่น" + edit: "แก้ไขเล่น" + created: "สร้างการเล่นแล้ว" + updated: "แก้ไขการเล่นแล้ว" + deleted: "ลบการเล่นแล้ว" + pageSetting: "ตั้งค่าการเล่น" editThisPage: "แก้ไข Play นี้" viewSource: "ดูต้นฉบับ" - my: "Play ของฉัน" - liked: "Play ที่ถูกใจไว้" + my: "มาย เพลย์" + liked: "ไลค์ เพลย์" featured: "เป็นที่นิยม" title: "หัวข้อ" script: "สคริปต์" - summary: "คำอธิบาย" - visibilityDescription: "หากตั้งค่าเป็นส่วนตัว มันจะไม่ปรากฏในโปรไฟล์อีกต่อไป แต่ผู้ที่ทราบ URL ของมันจะยังสามารถเข้าถึงได้" + summary: "รายละเอียด" _pages: newPage: "สร้างหน้าเพจใหม่" editPage: "แก้ไขหน้าเพจ" readPage: "กำลังดูแหล่งที่มาของเพจนี้" - pageSetting: "การตั้งค่าหน้าเพจ" + created: "สร้างหน้าเพจสำเร็จเรียบร้อยแล้ว" + updated: "แก้ไขหน้าเพจสำเร็จเรียบร้อยแล้ว" + deleted: "ลบหน้าเพจสำเร็จเรียบร้อยแล้ว" + pageSetting: "การตั้งค่าหน้า" nameAlreadyExists: "URL ของหน้าที่ระบุนั้นมีอยู่แล้ว" invalidNameTitle: "URL ของหน้าที่ระบุนั้นไม่ถูกต้อง" invalidNameText: "ตรวจสอบให้แน่ใจนะว่าชื่อหน้าไม่ว่างเปล่า" editThisPage: "แก้ไขเพจนี้" viewSource: "ดูต้นฉบับ" - viewPage: "ดูหน้าเพจ" + viewPage: "ดูหน้า" like: "ถูกใจ" - unlike: "เลิกถูกใจ" + unlike: "ลบไลค์" my: "หน้าเพจของฉัน" liked: "หน้าเพจที่ถูกใจ" featured: "เป็นที่นิยม" @@ -2366,16 +1884,15 @@ _pages: summary: "สรุปเพจ" alignCenter: "เซ็นเตอร์" hideTitleWhenPinned: "ซ่อนชื่อหน้าเพจเมื่อปักหมุดไว้ที่โปรไฟล์" - font: "แบบอักษร" + font: "ตัวอักษร" fontSerif: "Serif" fontSansSerif: "Sans Serif" eyeCatchingImageSet: "ตั้งค่าภาพขนาดย่อ" eyeCatchingImageRemove: "ลบภาพขนาดย่อ" chooseBlock: "เพิ่มบล็อค" - enterSectionTitle: "ป้อนชื่อหัวข้อ" selectType: "เลือกชนิด" contentBlocks: "เนื้อหา" - inputBlocks: "ป้อนข้อมูล" + inputBlocks: "อินพุต" specialBlocks: "พิเศษ" blocks: text: "ข้อความ" @@ -2383,8 +1900,6 @@ _pages: section: "ประเภท" image: "รูปภาพ" button: "ปุ่ม" - dynamic: "บล็อกแบบไดนามิก" - dynamicDescription: "บล็อกนี้ล้าสมัยแล้ว โปรดใช้ {play} แทน นับจากนี้เป็นต้นไป" note: "โน้ตที่ฝังตัว" _note: id: "โน้ต ID" @@ -2395,48 +1910,30 @@ _relayStatus: accepted: "ได้รับการอนุมัติ" rejected: "ถูกปฏิเสธ" _notification: - fileUploaded: "ไฟล์ถูกอัปโหลดแล้ว" + fileUploaded: "ไฟล์ถูกอัพโหลดแล้วน่ะ" youGotMention: "{name} กล่าวถึงคุณ" youGotReply: "{name} ตอบกลับถึงคุณ" - youGotQuote: "{name} อ้างอิงคุณ" + youGotQuote: "{name} อ้างถึงคุณ" youRenoted: "รีโน้ตจาก {name}" youWereFollowed: "ได้ติดตามคุณ" - youReceivedFollowRequest: "ได้รับคำขอติดตาม" - yourFollowRequestAccepted: "คำขอติดตามได้รับการอนุมัติแล้ว" - pollEnded: "ผลโพลออกมาแล้ว" - newNote: "โพสต์ใหม่" + youReceivedFollowRequest: "คุณมีคำขอติดตามใหม่น่ะ" + yourFollowRequestAccepted: "คำขอติดตามของคุณได้รับการยอมรับแล้วน่ะ" + pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน" unreadAntennaNote: "เสาอากาศ {name}" - roleAssigned: "ได้รับบทบาท" - emptyPushNotificationMessage: "อัปเดตการแจ้งเตือนแบบพุชแล้ว" + emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว" achievementEarned: "รับความสำเร็จ" - testNotification: "ทดสอบการแจ้งเตือน" - checkNotificationBehavior: "กดเพื่อดูลักษณะการแจ้งเตือน" - sendTestNotification: "ส่งทดสอบการแจ้งเตือน" - notificationWillBeDisplayedLikeThis: "การแจ้งเตือนมีลักษณะแบบนี้" - reactedBySomeUsers: "ถูกรีแอคชั่นโดยผู้ใช้ {n} ราย" - likedBySomeUsers: "{n} คนถูกใจ" - renotedBySomeUsers: "รีโน้ตจากผู้ใช้ {n} ราย" - followedBySomeUsers: "มีผู้ติดตาม {n} ราย" - flushNotification: "ล้างประวัติการแจ้งเตือน" - exportOfXCompleted: "การดำเนินการส่งออก {x} ได้เสร็จสิ้นลงแล้ว" - login: "มีคนล็อกอิน" _types: all: "ทั้งหมด" - note: "โน้ตใหม่" follow: "กำลังติดตาม" mention: "กล่าวถึง" reply: "ตอบกลับ" renote: "รีโน้ต" - quote: "อ้างอิง" + quote: "อ้างคำพูด" reaction: "รีแอคชั่น" - pollEnded: "โพลสิ้นสุดแล้ว" - receiveFollowRequest: "ได้รับคำร้องขอติดตาม" - followRequestAccepted: "อนุมัติให้ติดตามแล้ว" - roleAssigned: "ให้บทบาท" + pollEnded: "โพลนี้สิ้นสุดลงแล้ว" + receiveFollowRequest: "ได้รับคำขอติดตาม\n" + followRequestAccepted: "ยอมรับคำขอติดตาม" achievementEarned: "ปลดล็อกความสำเร็จแล้ว" - exportCompleted: "กระบวนการส่งออกข้อมูลได้เสร็จสิ้นสมบูรณ์แล้ว" - login: "เข้าสู่ระบบ" - test: "ทดสอบระบบแจ้งเตือน" app: "การแจ้งเตือนจากแอปที่มีลิงก์" _actions: followBack: "ติดตามกลับด้วย" @@ -2446,7 +1943,6 @@ _deck: alwaysShowMainColumn: "แสดงคอลัมน์หลักเสมอ" columnAlign: "จัดแนวคอลัมน์" addColumn: "เพิ่มคอลัมน์" - newNoteNotificationSettings: "ตั้งค่าการแจ้งเตือนเมื่อมีโน้ตใหม่" configureColumn: "ตั้งค่าคอลัมน์" swapLeft: "ขยับไปทางซ้าย" swapRight: "ขยับไปทางขวา" @@ -2460,9 +1956,6 @@ _deck: introduction: "สร้างอินเทอร์เฟซที่สมบูรณ์แบบสำหรับคุณโดยจัดเรียงคอลัมน์ได้อย่างอิสระ!" introduction2: "คลิกที่เครื่องหมาย + ทางขวาของหน้าจอเพื่อเพิ่มคอลัมน์ใหม่ทุกครั้งที่คุณต้องการ" widgetsIntroduction: "กรุณาเลือก \"แก้ไขวิดเจ็ต\" ในเมนูคอลัมน์และเพิ่มวิดเจ็ต" - useSimpleUiForNonRootPages: "แสดง UI ของ Root Page อย่างง่าย " - usedAsMinWidthWhenFlexible: "ความกว้างขั้นต่ำนั้นจะถูกใช้งานสำหรับสิ่งนี้เมื่อเปิดใช้งานตัวเลือก \"ปรับความกว้างอัตโนมัติ\" หากเลือกเปิดใช้งานแล้ว" - flexible: "ปรับความกว้างอัตโนมัติ" _columns: main: "หลัก" widgets: "วิดเจ็ต" @@ -2470,9 +1963,9 @@ _deck: tl: "ไทม์ไลน์" antenna: "เสาอากาศ" list: "รายการ" - channel: "ช่อง" - mentions: "กล่าวถึงคุณ" - direct: "ไดเร็กต์" + channel: "แชนแนล" + mentions: "พูดถึง" + direct: "ไดเร็ค" roleTimeline: "บทบาทไทม์ไลน์" _dialog: charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}" @@ -2485,10 +1978,9 @@ _drivecleaner: orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก" _webhookSettings: createWebhook: "สร้าง Webhook" - modifyWebhook: "แก้ไข Webhook" name: "ชื่อ" secret: "ความลับ" - trigger: "ทริกเกอร์" + events: "อีเว้นท์ Webhook" active: "เปิดใช้งาน" _events: follow: "เมื่อกำลังติดตามผู้ใช้" @@ -2498,227 +1990,3 @@ _webhookSettings: renote: "รีโน้ตแล้วเมื่อ" reaction: "เมื่อได้รับรีแอคชั่น" mention: "เมื่อกำลังถูกกล่าวถึง" - _systemEvents: - abuseReport: "เมื่อมีการรายงานจากผู้ใช้" - abuseReportResolved: "เมื่อมีการจัดการกับการรายงานจากผู้ใช้" - userCreated: "เมื่อผู้ใช้ถูกสร้างขึ้น" - inactiveModeratorsWarning: "เมื่อผู้ดูแลระบบไม่ได้ใช้งานมานานระยะหนึ่ง" - inactiveModeratorsInvitationOnlyChanged: "เมื่อผู้ดูแลระบบที่ไม่ได้ใช้งานมานาน และเซิร์ฟเวอร์เปลี่ยนเป็นแบบเชิญเข้าร่วมเท่านั้น" - deleteConfirm: "ต้องการลบ Webhook ใช่ไหม?" - testRemarks: "คลิกปุ่มทางด้านขวาของสวิตช์เพื่อส่ง Webhook ทดสอบที่มีข้อมูลจำลอง" -_abuseReport: - _notificationRecipient: - createRecipient: "เพิ่มปลายทางการแจ้งเตือนการรายงาน" - modifyRecipient: "แก้ไขปลายทางการแจ้งเตือนการรายงาน" - recipientType: "ประเภทของปลายทางการแจ้งเตือน\n" - _recipientType: - mail: "อีเมล" - webhook: "Webhook" - _captions: - mail: "ส่งการแจ้งเตือนไปยังที่อยู่อีเมลของผู้ควบคุม (เฉพาะเมื่อได้รับการรายงาน)" - webhook: "ส่งการแจ้งเตือนไปยัง SystemWebhook ที่กำหนด (จะส่งเมื่อได้รับการรายงานและเมื่อการรายงานได้รับการแก้ไข)" - keywords: "คีย์เวิร์ด" - notifiedUser: "ผู้ใช้ที่ได้รับการแจ้งเตือน" - notifiedWebhook: "Webhook ที่ใช้" - deleteConfirm: "ต้องการลบปลายทางการแจ้งเตือนใช่ไหม?" -_moderationLogTypes: - createRole: "สร้างบทบาทแล้ว" - deleteRole: "ลบบทบาทแล้ว" - updateRole: "อัปเดตบทบาทแล้ว" - assignRole: "ได้รับมอบหมายบทบาท" - unassignRole: "ถอดออกจากบทบาทแล้ว" - suspend: "ระงับ" - unsuspend: "เลิกระงับ" - addCustomEmoji: "เพิ่มเอโมจิที่กำหนดเองแล้ว" - updateCustomEmoji: "อัปเดตเอโมจิที่กำหนดเองแล้ว" - deleteCustomEmoji: "ลบเอโมจิที่กำหนดเองออกแล้ว" - updateServerSettings: "อัปเดตการตั้งค่าเซิร์ฟเวอร์แล้ว" - updateUserNote: "อัปเดตโน้ตการกลั่นกรองแล้ว" - deleteDriveFile: "ลบไฟล์ออกแล้ว" - deleteNote: "ลบโน้ตออกแล้ว" - createGlobalAnnouncement: "สร้างประกาศทั่วโลกแล้ว" - createUserAnnouncement: "สร้างประกาศผู้ใช้แล้ว" - updateGlobalAnnouncement: "อัปเดตประกาศทั่วโลกแล้ว" - updateUserAnnouncement: "อัปเดตประกาศผู้ใช้แล้ว" - deleteGlobalAnnouncement: "ลบประกาศทั่วโลกออกแล้ว" - deleteUserAnnouncement: "ลบประกาศผู้ใช้ออกแล้ว" - resetPassword: "รีเซ็ตรหัสผ่าน" - suspendRemoteInstance: "ระงับเซิร์ฟเวอร์ระยะไกล" - unsuspendRemoteInstance: "เลิกระงับเซิร์ฟเวอร์ระยะไกล" - updateRemoteInstanceNote: "อัปเดตโน้ตการกลั่นกรองสำหรับเซิร์ฟเวอร์ระยะไกลแล้ว" - markSensitiveDriveFile: "ทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน" - unmarkSensitiveDriveFile: "ยกเลิกทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน" - resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว" - forwardAbuseReport: "ได้ส่งรายงานไปแล้ว" - updateAbuseReportNote: "โน้ตการกลั่นกรองที่รายงานไปนั้น ได้รับการอัปเดตแล้ว" - createInvitation: "สร้างรหัสเชิญ" - createAd: "สร้างโฆษณาแล้ว" - deleteAd: "ลบโฆษณาออกแล้ว" - updateAd: "อัปเดตโฆษณาแล้ว" - createAvatarDecoration: "สร้างการตกแต่งไอคอนแล้ว" - updateAvatarDecoration: "อัปเดตการตกแต่งไอคอนแล้ว" - deleteAvatarDecoration: "ลบการตกแต่งไอคอนแล้ว" - unsetUserAvatar: "ลบไอคอนผู้ใช้" - unsetUserBanner: "ลบแบนเนอร์ผู้ใช้" - createSystemWebhook: "สร้าง SystemWebhook" - updateSystemWebhook: "อัปเดต SystemWebhook" - deleteSystemWebhook: "ลบ SystemWebhook" - createAbuseReportNotificationRecipient: "สร้างปลายทางการแจ้งเตือนการรายงาน" - updateAbuseReportNotificationRecipient: "อัปเดตปลายทางการแจ้งเตือนการรายงาน" - deleteAbuseReportNotificationRecipient: "ลบปลายทางการแจ้งเตือนการรายงาน" - deleteAccount: "บัญชีถูกลบไปแล้ว" - deletePage: "เพจถูกลบออกไปแล้ว" - deleteFlash: "Play ถูกลบออกไปแล้ว" - deleteGalleryPost: "โพสต์แกลเลอรี่ถูกลบออกแล้ว" -_fileViewer: - title: "รายละเอียดไฟล์" - type: "ประเภทไฟล์" - size: "ขนาดไฟล์" - url: "URL" - uploadedAt: "วันที่เข้าร่วม" - attachedNotes: "โน้ตที่แนบมาด้วย" - thisPageCanBeSeenFromTheAuthor: "หน้าเพจนี้จะสามารถปรากฏได้โดยผู้ใช้ที่อัปโหลดไฟล์นี้เท่านั้น" -_externalResourceInstaller: - title: "ติดตั้งจากไซต์ภายนอก" - checkVendorBeforeInstall: "โปรดตรวจสอบให้แน่ใจว่าแหล่งแจกหน่ายมีความน่าเชื่อถือก่อนทำการติดตั้ง" - _plugin: - title: "ต้องการติดตั้งปลั๊กอินนี้ใช่ไหม?" - _theme: - title: "ต้องการติดตั้งธีมนี้ใช่ไหม?" - _meta: - base: "โทนสีพื้นฐาน" - _vendorInfo: - title: "ข้อมูลผู้จัดจำหน่าย" - endpoint: "จุดอ้างอิงปลายทาง (Referenced endpoint)" - hashVerify: "การตรวจสอบแฮช (ความสมบูรณ์ของไฟล์)" - _errors: - _invalidParams: - title: "พารามิเตอร์ไม่ถูกต้อง" - description: "มีสารสนเทศไม่เพียงพอที่จะโหลดข้อมูลจากไซต์ภายนอก โปรดยืนยัน URL ที่ป้อน" - _resourceTypeNotSupported: - title: "ไม่รองรับทรัพยากรภายนอกนี้" - description: "ไม่รองรับประเภทของทรัพยากรภายนอกนี้ โปรดติดต่อผู้ดูแลเว็บไซต์" - _failedToFetch: - title: "รับข้อมูลล้มเหลว" - fetchErrorDescription: "เกิดข้อผิดพลาดในการสื่อสารกับไซต์ภายนอก หากการลองอีกครั้งไม่สามารถแก้ไขปัญหานี้ได้ โปรดติดต่อผู้ดูแลไซต์" - parseErrorDescription: "เกิดข้อผิดพลาดในการประมวลผลข้อมูลที่โหลดจากไซต์ภายนอก โปรดติดต่อผู้ดูแลเว็บไซต์" - _hashUnmatched: - title: "การยืนยัน/ตรวจสอบข้อมูลล้มเหลว" - description: "เกิดข้อผิดพลาดในการตรวจสอบความสมบูรณ์ของข้อมูลที่ดึงมา เพื่อเป็นมาตรการรักษาความปลอดภัย การติดตั้งไม่สามารถดำเนินการต่อได้ โปรดติดต่อผู้ดูแลเว็บไซต์" - _pluginParseFailed: - title: "ข้อผิดพลาด AiScript" - description: "ดึงข้อมูลที่ร้องขอสำเร็จแล้ว แต่มีข้อผิดพลาดเกิดขึ้นระหว่างการแยกวิเคราะห์ AiScript โปรดติดต่อผู้เขียนปลั๊กอิน รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript" - _pluginInstallFailed: - title: "ติดตั้งปลั๊กอินล้มเหลว" - description: "เกิดปัญหาขณะติดตั้งปลั๊กอิน กรุณาลองอีกครั้ง. โปรดดูคอนโซล Javascript สำหรับรายละเอียดข้อผิดพลาด" - _themeParseFailed: - title: "การแยกวิเคราะห์ธีมล้มเหลว" - description: "ดึงข้อมูลที่ร้องขอสำเร็จแล้ว แต่มีข้อผิดพลาดเกิดขึ้นระหว่างการแยกวิเคราะห์ธีม โปรดติดต่อผู้เขียนธีม รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript" - _themeInstallFailed: - title: "ติดตั้งธีมล้มเหลว" - description: "เกิดปัญหาระหว่างการติดตั้งธีม กรุณาลองอีกครั้ง. รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript" -_dataSaver: - _media: - title: "โหลดสื่อ" - description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด" - _avatar: - title: "รูปไอคอน" - description: "ระงับการเคลื่อนไหวของภาพไอคอน ภาพเคลื่อนไหวอาจมีขนาดไฟล์ใหญ่กว่าภาพปกติ ดังนั้นจึงสามารถช่วยในการลดการใช้ข้อมูล" - _code: - title: "ไฮไลต์โค้ด" - description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้" -_hemisphere: - N: "ซีกโลกเหนือ" - S: "ซีกโลกใต้" - caption: "ใช้เพื่อกำหนดฤดูกาลของไคลเอ็นต์" -_reversi: - reversi: "รีเวอร์ซี" - gameSettings: "ตั้งค่าการเล่น" - chooseBoard: "เลือกกระดาน" - blackOrWhite: "ดำ/ขาว" - blackIs: "{name}เป็นสีดำ" - rules: "กฎ" - thisGameIsStartedSoon: "การเล่นจะเริ่มแล้ว" - waitingForOther: "กำลังรออีกฝ่ายเตรียมตัวให้เสร็จ" - waitingForMe: "กำลังรอฝ่ายคุณเตรียมตัวให้เสร็จ" - waitingBoth: "กรุณาเตรียมตัว" - ready: "เตรียมตัวพร้อมแล้ว" - cancelReady: "ยกเลิกการเตรียมตัวพร้อม" - opponentTurn: "ตาอีกฝ่าย" - myTurn: "ตาของคุณ" - turnOf: "ตาของ{name}" - pastTurnOf: "ตาของ{name}" - surrender: "ยอมแพ้" - surrendered: "ยอมแพ้แล้ว" - timeout: "หมดเวลาแล้ว" - drawn: "เสมอ" - won: "{name}ชนะ" - black: "ดำ" - white: "ขาว" - total: "รวมทั้งหมด" - turnCount: "ตาที่{count}" - myGames: "การเล่นของตัวเอง" - allGames: "การเล่นของทุกคน" - ended: "จบ" - playing: "กำลังเล่น" - isLlotheo: "คนที่มีตัวหมากน้อยกว่าชนะ (Roseo)" - loopedMap: "ลูปแมป" - canPutEverywhere: "โหมดที่สามารถวางได้ทุกที่" - timeLimitForEachTurn: "จำกัดเวลาต่อแต่ละตา" - freeMatch: "ฟรีแมตช์" - lookingForPlayer: "กำลังมองหาคู่ต่อสู้อยู่" - gameCanceled: "ยกเลิกการเล่นแล้ว" - shareToTlTheGameWhenStart: "โพสต์ลงไทม์ไลน์เมื่อเริ่มการเล่น" - iStartedAGame: "เริ่มเล่นหมากรีเวอร์ซีแล้ว! #MisskeyReversi" - opponentHasSettingsChanged: "อีกฝ่ายเปลี่ยนการตั้งค่า" - allowIrregularRules: "อนุญาตกฎที่ไม่ปรกติ (โหมดฟรีทุกอย่าง)" - disallowIrregularRules: "ไม่อนุญาตกฎที่ไม่ปรกติ" - showBoardLabels: "แสดงหมายเลขแถว/คอลัมน์บนกระดาน" - useAvatarAsStone: "ใช้รูปอวตารเป็นหมาก" -_offlineScreen: - title: "ออฟไลน์ - ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้" - header: "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้" -_urlPreviewSetting: - title: "การตั้งค่าการแสดงตัวอย่าง URL" - enable: "เปิดใช้งานการแสดงตัวอย่าง URL" - timeout: "เวลาจำกัดในการโหลดตัวอย่าง URL (ms)" - timeoutDescription: "หากเวลาที่ใช้ในการโหลดเกินค่านี้ จะไม่มีการสร้างการแสดงตัวอย่าง" - maximumContentLength: "ค่าสูงสุดของ Content-Length (byte)" - maximumContentLengthDescription: "หาก Content-Length เกินค่านี้ จะไม่มีการสร้างการแสดงตัวอย่าง" - requireContentLength: "สร้างการแสดงตัวอย่างเฉพาะในกรณีที่รับ Content-Length ไหว" - requireContentLengthDescription: "หากเซิร์ฟเวอร์อื่นไม่ส่งคืน Content-Length จะไม่มีการสร้างการแสดงตัวอย่าง" - userAgent: "User-Agent" - userAgentDescription: "ตั้งค่า User-Agent ที่ใช้ในการรับการแสดงตัวอย่าง หากเว้นว่างไว้ ระบบจะใช้ User-Agent เริ่มต้น" - summaryProxy: "endpoint ของพร็อกซีที่สร้างการแสดงตัวอย่าง" - summaryProxyDescription: "สร้างการแสดงตัวอย่างด้วย summary Proxy แทนที่จะใช้เนื้อหา Misskey" - summaryProxyDescription2: "พารามิเตอร์ต่อไปนี้จะถูกใช้เป็นสตริงการสืบค้นเพื่อเชื่อมต่อกับพร็อกซี หากฝั่งพร็อกซีไม่รองรับการตั้งค่าเหล่านี้จะถูกละเว้น" -_mediaControls: - pip: "รูปภาพในรูปภาม" - playbackRate: "ความเร็วในการเล่น" - loop: "เล่นวนซ้ำ" -_contextMenu: - title: "เมนูเนื้อหา" - app: "แอปพลิเคชัน" - appWithShift: "แอปฟลิเคชันด้วยปุ่มยกแคร่ (Shift)" - native: "UI ของเบราว์เซอร์" -_embedCodeGen: - title: "ปรับแต่งโค้ดฝัง" - header: "แสดงส่วนหัว" - autoload: "โหลดเพิ่มโดยอัตโนมัติ (เลิกใช้แล้ว)" - maxHeight: "ความสูงสุด" - maxHeightDescription: "หากถ้าตั้งค่าเป็น 0 จะทำให้ไม่มีการจำกัดความสูงของวิดเจ็ต แต่ควรตั้งค่าเป็นตัวเลขอื่นๆ เพื่อไม่ให้วิดเจ็ตยืดตัวลงไปเรื่อยๆ" - maxHeightWarn: "การจำกัดความสูงสูงสุดถูกปิดใช้งาน (0) หากไม่ได้ตั้งใจให้เป็นเช่นนี้ โปรดตั้งค่าความสูงสูงสุดให้เป็นค่าอื่นๆแทน" - previewIsNotActual: "การแสดงผลนั้นต่างจากการฝังจริงเพราะเกินขอบเขตที่แสดงบนหน้าจอตัวอย่างนะ" - rounded: "ทำให้มันกลม" - border: "เพิ่มขอบให้กับกรอบด้านนอก" - applyToPreview: "นำไปใช้กับการแสดงตัวอย่าง" - generateCode: "สร้างโค้ดสำหรับการฝัง" - codeGenerated: "รหัสถูกสร้างขึ้นแล้ว" - codeGeneratedDescription: "นำโค้ดที่สร้างแล้วไปวางในเว็บไซต์ของคุณเพื่อฝังเนื้อหา" -_remoteLookupErrors: - _noSuchObject: - title: "ไม่พบหน้าที่ต้องการ" -_search: - searchScopeAll: "ทั้งหมด" - searchScopeLocal: "ท้องถิ่น" - searchScopeUser: "ผู้ใช้เฉพาะ" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index f63dcc9467..cc402eec48 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -1,6 +1,5 @@ --- _lang_: "Türkçe" -headlineMisskey: "Notlarla bağlanmış bir ağ" introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀." poweredByMisskeyDescription: "name}Açık kaynak bir platform\nMisskeyDünya'nın en sunucularında biri。" monthAndDay: "{month}Ay {day}Gün" @@ -8,15 +7,11 @@ search: "Arama" notifications: "Bildirim" username: "Kullanıcı Adı" password: "Şifre" -initialPasswordForSetup: "" forgotPassword: "şifremi unuttum" -fetchingAsApObject: "從聯邦宇宙取得中..." ok: "TAMAM" gotIt: "Anladım" cancel: "İptal" -noThankYou: "Hayır, teşekkürler" enterUsername: "Kullanıcı adınızı giriniz" -renotedBy: "{user} tarafından Renotelandı" noNotes: "Notlar mevcut değil." noNotifications: "Bildirim bulunmuyor" instance: "Sunucu" @@ -46,40 +41,19 @@ pin: "Sabitlenmiş" unpin: "Sabitlemeyi kaldır" copyContent: "İçeriği kopyala" copyLink: "Bağlantıyı Kopyala" -copyLinkRenote: "Turkish" delete: "Sil" deleteAndEdit: "Sil ve yeniden düzenle" deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." addToList: "Listeye ekle" -addToAntenna: "Antene ekle" sendMessage: "Mesaj Gönder" copyRSS: "RSSKopyala" copyUsername: "Kullanıcı Adını Kopyala" copyUserId: "KullanıcıyıKopyala" copyNoteId: "Kimlik notunu kopyala" -copyFileId: "Dosya ID'sini kopyala" -copyFolderId: "Klasör ID'sini kopyala" -copyProfileUrl: "Profil URL'sini kopyala" searchUser: "Kullanıcıları ara" reply: "yanıt" loadMore: "Devamını yükle" showMore: "Devamını yükle" -showLess: "Kapat" -youGotNewFollower: "seni takip etti" -receiveFollowRequest: "Takip isteği alındı" -followRequestAccepted: "Takip isteği kabul edildi" -mention: "Bahset" -mentions: "Bahsetmeler" -directNotes: "Kişisel mesajlar" -importAndExport: "İçeri/Dışarı aktar" -import: "İçeri aktar" -export: "Dışa aktar" -files: "Dosyalar" -download: "İndir" -driveFileDeleteConfirm: "\"{name}\" dosyası silinsin mi? Dosya kullanıldığı tüm notlardan kaybolacaktır." -unfollowConfirm: "{name} takipten çıkarılsın mı?" -exportRequested: "Dışa aktarım talep ettiniz. Bu biraz zaman alabilir. İşlem bitince Sürücünüze eklenecektir." -importRequested: "Dışa aktarım talep ettiniz. Bu işlem biraz zaman alabilir." lists: "Listeler" noLists: "Liste yok" note: "not" @@ -90,16 +64,6 @@ followsYou: "seni takip ediyor" createList: "Liste oluştur" manageLists: "Yönetici Listeleri" error: "hata" -somethingHappened: "Bir hata oluştu" -retry: "Tekrar dene" -pageLoadError: "Sayfa yüklenemedi." -pageLoadErrorDescription: "Bu genelde ağ veya tarayıcı ön belleği hatalarından olur. Lütfen ön belleği temizlemeyi veya birkaç dakika beklemeyi ve sayfayı yenilemeyi deneyin." -serverIsDead: "Sunucu yanıt vermiyor. Birkaç dakika sonra tekrar deneyin." -youShouldUpgradeClient: "Sayfayı görüntülemek için yenileyin." -enterListName: "Liste ismi" -privacy: "Gizlilik" -makeFollowManuallyApprove: "Takip istekleri elle onaylansın" -defaultNoteVisibility: "Varsayılan görünürlük" follow: "takipçi" followRequest: "Takip isteği" followRequests: "Takip istekleri" @@ -112,23 +76,9 @@ renoted: "yeniden adlandırılmış" cantRenote: "Ayrılamama" cantReRenote: "not alabilirmiyim" quote: "alıntı" -inChannelRenote: "Kanal içi Renote" -inChannelQuote: "Kanal içi Alıntı" pinnedNote: "Sabitlenen" pinned: "Sabitlenmiş" you: "sen" -clickToShow: "Görüntülemek için tıkla" -sensitive: "Hassas içerik" -add: "Ekle" -reaction: "Tepkiler" -reactions: "Tepkiler" -reactionSettingDescription2: "Sıralamak için sürükleyin, silmek için tıklayın, eklemek için \"+\" tuşuna tıklayın." -rememberNoteVisibility: "Görünürlük ayarlarını hatırla" -attachCancel: "Eki sil" -markAsSensitive: "Hassas içerik olarak işaretle" -unmarkAsSensitive: "Hassas içerik işaretini kaldır" -enterFileName: "Dosya ismini gir" -mute: "Gizle" unmute: "sesi aç" renoteMute: "sesi kapat" renoteUnmute: "sesi açmayı iptal et" @@ -138,325 +88,46 @@ suspend: "askıya al" unsuspend: "askıya alma" blockConfirm: "Onayı engelle" unblockConfirm: "engellemeyi kaldır onayla" -suspendConfirm: "Hesap askıya alınsın mı?" -unsuspendConfirm: "Hesap askıdan kaldırılsın mı" -selectList: "Bir liste seç" -editList: "Listeyi düzenle" selectChannel: "Kanal seç" -selectAntenna: "Bir anten seç" -editAntenna: "Anteni düzenle" -selectWidget: "Araç seç" -editWidgets: "Araçları düzenle" -editWidgetsExit: "Tamam" -customEmojis: "Özel Emoji" -emoji: "Emoji" -emojis: "Emoji" -emojiName: "Emoji adı" -emojiUrl: "Emoji URL'si" -addEmoji: "Emoji ekle" -settingGuide: "Önerilen ayarlar" -cacheRemoteFiles: "Uzak dosyalar ön belleğe alınsın" -cacheRemoteFilesDescription: "Bu ayar açık olduğunda diğer sitelerin dosyaları doğrudan uzak sunucudan yüklenecektir. Bu ayarı kapatmak depolama kullanımını azaltacak ama küçük resimler oluşturulmadığından trafiği arttıracaktır." -youCanCleanRemoteFilesCache: "" -cacheRemoteSensitiveFiles: "Hassas uzak dosyalar ön belleğe alınsın" -cacheRemoteSensitiveFilesDescription: "Bu ayar kapalı olduğunda hassas uzak dosyalar ön belleğe alınmadan doğrudan uzak sunucudan yüklenecektir." flagAsBot: "Bot olarak işaretle" -flagAsBotDescription: "Bu seçeneği hesap bir program tarafından kontrol ediliyorsa işaretleyin. Bu, diğer geliştiricilerin sonsuz etkileşim zincirleri oluşturmasını engellemeye yardımcı olur ve Misskey'in iç sisteminin hesaba bir bot gibi davranmasını sağlar." -flagAsCat: "Kedi hesabı" -flagAsCatDescription: "Kedi hesabı" -flagShowTimelineReplies: "Zaman akışında notlara gelen cevapları göster" -flagShowTimelineRepliesDescription: "Açık olduğu durumda, zaman akışında kullanıcıların başkalarına verdiği cevaplar gözükür." -autoAcceptFollowed: "Takip edilen hesapların takip isteklerini kabul et" -addAccount: "Hesap ekle" -reloadAccountsList: "Hesap listesini güncelle" -loginFailed: "Giriş başarısız oldu" -showOnRemote: "Uzak sunucuda görüntüle" -general: "Genel" -wallpaper: "Duvar kağıdı" -setWallpaper: "Duvar kağıdını ayarla" -removeWallpaper: "Duvar kağıdını sil" -searchWith: "Arama: {q}" -youHaveNoLists: "Hiç listeniz yok" -followConfirm: "{name} takip edilsin mi?" -proxyAccount: "Vekil hesabı" -proxyAccountDescription: "Proxy hesabı, belirli koşullar altında kullanıcılar için uzaktan takipçi işlevi gören bir hesaptır. Örneğin, bir kullanıcı listeye bir uzak kullanıcı eklediğinde, o kullanıcıyı takip eden yerel bir kullanıcı yoksa uzak kullanıcının etkinliği örneğe teslim edilmeyecektir, dolayısıyla bunun yerine proxy hesabı takip edilecektir." -host: "Sağlayıcı" -selectUser: "Kullanıcı seç" -recipient: "Kime" -annotation: "Açıklamalar" -federation: "Federasyon" instances: "Sunucu" -registeredAt: "Katılma tarihi" -latestRequestReceivedAt: "Alınan son talep" -latestStatus: "En son durum" -storageUsage: "Depolama kullanımı" -charts: "Çizelgeler" -perHour: "Saatlik" -perDay: "Günlük" -stopActivityDelivery: "Durum güncellemelerini gönderme" -blockThisInstance: "Bu sunucuyu engelle" -silenceThisInstance: "" -operations: "İşlemler" -software: "Yazılımlar" -version: "Sürüm" -metadata: "Meta Verileri" -withNFiles: "{n} tane dosya" -monitor: "Monitör" -jobQueue: "İşlem sırası" -cpuAndMemory: "İşlemci ve Hafıza" -network: "Ağ" -disk: "Disk" -instanceInfo: "Sunucu Bilgisi" -statistics: "İstatistikler" -clearQueue: "Sırayı temizle" -clearQueueConfirmTitle: "Sıra silinsin mi?" -clearQueueConfirmText: "Sırada kalan hiçbir şey iletilmeyecek. Genelde bu işlem gerekli değildir." -clearCachedFiles: "Ön belleği temizle" -clearCachedFilesConfirm: "Ön belleğe alınmış tüm uzak sunucu dosyaları silinsin mi?" -blockedInstances: "Engellenen sunucular" -blockedInstancesDescription: "Engellemek istediğiniz sunucuların alan adlarını satır sonlarıyla ayırarak yazın. Yazılan sunucular bu sunucuyla iletişime geçemeyecek." -silencedInstances: "Turkısh" -silencedInstancesDescription: "" -muteAndBlock: "Susturma ve Engelleme" -mutedUsers: "Susturulan kullanıcılar" -blockedUsers: "Engellenen kullanıcılar" -noUsers: "Kullanıcı yok" -editProfile: "Profili düzenle" -noteDeleteConfirm: "Bu notu silmek istediğinizden emin misiniz?" -pinLimitExceeded: "Daha fazla not sabitlenemez" -done: "Tamamlandı" -preview: "Önizleme" -default: "Varsayılan" -defaultValueIs: "Varsayılan: {value}" -noCustomEmojis: "Emoji bulunamadı" -noJobs: "Hiç işlem yok" -federating: "Federe ediliyor" -blocked: "Engellenmiş" -suspended: "Askıya alınmış" -all: "Tümü" -subscribing: "Abonelik" -publishing: "Paylaşım" -notResponding: "Cevap yok" -instanceFollowing: "Sunucuda takip edenler" -instanceFollowers: "Sunucu takipçileri" -instanceUsers: "Sunucu kullanıcıları" -changePassword: "Şifreyi değiştir" -security: "Güvenlik" -retypedNotMatch: "Girişler uyuşmuyor." -currentPassword: "Geçerli şifre" -newPassword: "Yeni şifre" -newPasswordRetype: "Yeni şifre (tekrar)" -attachFile: "Dosya ekle" -more: "Daha!" -featured: "Öne Çıkan" -usernameOrUserId: "Kullanıcı adı veya ID'si" -noSuchUser: "Kullanıcı bulunamadı" -lookup: "Sorgu" -announcements: "Duyurular" -imageUrl: "Görsel URL'si" remove: "Sil" -removed: "Silindi" -removeAreYouSure: "\"{x}\" silmek istediğinizden emin misiniz?" -deleteAreYouSure: "\"{x}\" silmek istediğinizden emin misiniz?" -resetAreYouSure: "Sıfırlansın mı?" -saved: "Kaydedildi" -upload: "Yükle" -keepOriginalUploading: "Orijinal görseli koru" -keepOriginalUploadingDescription: "Orijinal olarak yüklenen görüntüyü olduğu gibi kaydeder. Kapatılırsa, yükleme sırasında web'de görüntülenecek bir sürüm oluşturulur." -fromDrive: "Drive Dosyasından" -fromUrl: "Bağlantıdan" -uploadFromUrl: "Bağlantıdan yükle" -uploadFromUrlDescription: "Yüklemek istediğiniz dosyanın bağlantısı" -uploadFromUrlRequested: "Yükleme talep edildi" -uploadFromUrlMayTakeTime: "Yüklemenin tamamlanması biraz süre alabilir." -explore: "Keşfet" -messageRead: "Okundu" -noMoreHistory: "Bundan öncesi yok" -nUsersRead: "{n} kişi okudu" -agreeTo: "Kabul Ediyorum: {0}" -agree: "Kabul Et" -agreeBelow: "Aşağıdakileri kabul ederim" -basicNotesBeforeCreateAccount: "Önemli notlar" -termsOfService: "Şartlar ve Koşullar" -start: "Başla" -home: "Ana sayfa" -remoteUserCaution: "Bu kullanıcı bir uzak sunucudan olduğu için alınan bilgiler tam olmayabilir." -activity: "Etkinlik" -images: "Görseller" -image: "Görseller" -birthday: "Doğum günü" -yearsOld: "{age} yaşında" -registeredDate: "Kayıt tarihi" -location: "Konum" -theme: "Temalar" -themeForLightMode: "Aydınlık Tema" -themeForDarkMode: "Karanlık Tema" -light: "Aydınlık" -dark: "Karanlık" -lightThemes: "Aydınlık Temalar" -darkThemes: "Karanlık Temalar" -syncDeviceDarkMode: "Sistem Koyu Modu ile senkronize et" -drive: "Sürücü" -fileName: "Dosya adı" -selectFile: "Dosya seç" -selectFiles: "Dosya seç" -selectFolder: "Klasör seç" -selectFolders: "Klasör seç" -renameFile: "Dosyayı yeniden adlandır" -folderName: "Klasör adı" -createFolder: "Klasör oluştur" -renameFolder: "Klasörü Yeniden Adlandır" -deleteFolder: "Klasörü sil" -addFile: "Dosya ekle" -emptyDrive: "Sürücü boş" -emptyFolder: "Bu klasör boş" -unableToDelete: "Silme mümkün değil" -inputNewFileName: "Yeni dosya ismini girin" -inputNewDescription: "Yeni bir başlık gir" -inputNewFolderName: "Yeni klasör ismini girin" -circularReferenceFolder: "Hedef klasör taşınan klasörün bir alt klasörü." -hasChildFilesOrFolders: "Klasör boş olmadığından silinemiyor" -copyUrl: "URL'yi kopyala" -rename: "Yeniden adlandır" -avatar: "Avatar" -banner: "Banner" -displayOfSensitiveMedia: "Hassas içerik gösterimi" -whenServerDisconnected: "Sunucu bağlantısı kesildiğinde" -disconnectedFromServer: "Sunucu bağlantısı koptu" -reload: "Yenile" -doNothing: "Bir şey yapma" -reloadConfirm: "Zaman akışı yenilensin mi?" -watch: "İzle" -unwatch: "İzlemeyi bırak" -accept: "Kabul et" -reject: "Reddet" -normal: "Normal" -instanceName: "Sunucu ismi" -instanceDescription: "Sunucu açıklaması" -maintainerName: "Yönetici ismi" -maintainerEmail: "Yöneticinin e-postası" -tosUrl: "Hizmet Koşulları Bağlantısı" -thisYear: "Bu yıl" -thisMonth: "Bu ay" -today: "Bugün" -monthX: "{month} ay" -pages: "Sayfalar" -integration: "Entegrasyon" -basicInfo: "Temel bilgiler" -pinnedUsers: "Sabitlenmiş kullanıcılar" pinnedNotes: "Sabitlenen" -manageAntennas: "Anten ayarları" userList: "Listeler" -resetPassword: "Şifre sıfırlama" -details: "Detaylar" -deck: "Güverte" -smtpHost: "Sağlayıcı" smtpUser: "Kullanıcı Adı" smtpPass: "Şifre" -notificationSetting: "Bildirim ayarları" -instanceTicker: "Notların sunucu bilgileri" -noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında vb. dolaşılmamasını ve dizine eklememesini talep et." -clearCache: "Ön belleği temizle" -onlineUsersCount: "{n} kullanıcı çevrim içi" user: "Kullanıcı" -global: "Küresel" -squareAvatars: "Kare avatarlar" searchByGoogle: "Arama" -file: "Dosyalar" -pushNotification: "Push bildirimleri" -subscribePushNotification: "Push bildirimlerini etkinleştir" -unsubscribePushNotification: "Push bildirimlerini kapat" -pushNotificationAlreadySubscribed: "Push bildirimleri zaten açık" -pushNotificationNotSupported: "Push bildirimleri sunucu veya tarayıcı tarafından desteklenmiyor" -noRole: "Rol bulunamadı" -color: "Renk" -addMemo: "Kısa not ekle" -icon: "Avatar" -replies: "yanıt" -renotes: "vazgeçme" -_chat: - home: "Ana sayfa" -_delivery: - stop: "Askıya alınmış" - _type: - none: "Paylaşım" -_accountDelete: - started: "Silme işlemi başlatıldı" -_email: - _follow: - title: "seni takip etti" _theme: - color: "Renk" keys: - mention: "Bahset" renote: "vazgeçme" _sfx: note: "notlar" notification: "Bildirim" -_2fa: - renewTOTPCancel: "Hayır, teşekkürler" -_permissions: - "read:blocks": "Engellenen hesapları gör" - "write:blocks": "Engellenen hesap listesini düzenle" _widgets: profile: "Profil" - instanceInfo: "Sunucu Bilgisi" notifications: "Bildirim" timeline: "Zaman çizelgesi" - calendar: "Takvim" - clock: "Saat" - activity: "Etkinlik" - federation: "Federasyon" - jobQueue: "İşlem sırası" - _userList: - chooseList: "Bir liste seç" _cw: show: "Devamını yükle" -_poll: - vote: "Oy kullan" _visibility: - publicDescription: "Herkese açık" - home: "Ana sayfa" followers: "takipçi" _profile: username: "Kullanıcı Adı" _exportOrImport: followingList: "takipçi" - muteList: "Gizle" blockingList: "engelle" userLists: "Listeler" -_charts: - federation: "Federasyon" -_timelines: - home: "Ana sayfa" - global: "Küresel" -_pages: - blocks: - image: "Görseller" _notification: - youWereFollowed: "seni takip etti" - unreadAntennaNote: "{name} anteni" _types: follow: "takipçi" - mention: "Bahset" renote: "vazgeçme" quote: "alıntı" - reaction: "Tepkiler" - receiveFollowRequest: "Takip isteği alındı" - followRequestAccepted: "Takip isteği kabul edildi" - login: "Giriş Yap " _actions: reply: "yanıt" renote: "vazgeçme" _deck: - configureColumn: "Sütun seçenekleri" _columns: notifications: "Bildirim" tl: "Zaman çizelgesi" list: "Listeler" - mentions: "Bahsetmeler" -_moderationLogTypes: - suspend: "askıya al" - resetPassword: "Şifre sıfırlama" -_search: - searchScopeAll: "Tümü" diff --git a/locales/ug-CN.yml b/locales/ug-CN.yml index fef26040a5..65ef841259 100644 --- a/locales/ug-CN.yml +++ b/locales/ug-CN.yml @@ -1,22 +1,4 @@ --- _lang_: "ياپونچە" -headlineMisskey: "خاتىرە ئارقىلىق ئۇلانغان تور" -monthAndDay: "{day}-{month}" search: "ئىزدەش" -ok: "ماقۇل" -noThankYou: "ئۇنى توختىتىڭ" -profile: "profile" -login: "كىرىش" -loggingIn: "كىرىش" -pin: "pinned" -delete: "ئۆچۈرۈش" -pinned: "pinned" -remove: "ئۆچۈرۈش" searchByGoogle: "ئىزدەش" -_2fa: - renewTOTPCancel: "ئۇنى توختىتىڭ" -_widgets: - profile: "profile" -_notification: - _types: - login: "كىرىش" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 9f9512d9a0..1ac07ff9b2 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -20,7 +20,6 @@ noNotes: "Немає нотаток" noNotifications: "Немає сповіщень" instance: "Інстанс" settings: "Налаштування" -notificationSettings: "Параметри сповіщень" basicSettings: "Основні налаштування" otherSettings: "Інші налаштування" openInWindow: "Відкрити у вікні" @@ -49,13 +48,9 @@ delete: "Видалити" deleteAndEdit: "Видалити й редагувати" deleteAndEditConfirm: "Ви впевнені, що хочете видалити цю нотатку та відредагувати її? Ви втратите всі реакції, поширення та відповіді на неї." addToList: "Додати до списку" -addToAntenna: "Додати в антени" sendMessage: "Надіслати повідомлення" copyRSS: "Скопіювати RSS" copyUsername: "Скопіювати ім’я користувача" -copyUserId: "Копіювати ID користувача" -copyNoteId: "блокнот ID користувача" -copyFileId: "Скопіювати ідентифікатор файлу." searchUser: "Пошук користувачів" reply: "Відповісти" loadMore: "Показати більше" @@ -116,6 +111,7 @@ sensitive: "NSFW" add: "Додати" reaction: "Реакції" reactions: "Реакції" +reactionSetting: "Налаштування реакцій" reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати." rememberNoteVisibility: "Пам’ятати параметри видимісті" attachCancel: "Видалити вкладення" @@ -133,7 +129,6 @@ unblockConfirm: "Ви впевнені, що хочете розблокуват suspendConfirm: "Ви впевнені, що хочете призупинити цей акаунт?" unsuspendConfirm: "Ви впевнені, що хочете відновити цей акаунт?" selectList: "Виберіть список" -editList: "Редагувати список." selectChannel: "Виберіть канал" selectAntenna: "Виберіть антену" selectWidget: "Виберіть віджет" @@ -208,6 +203,7 @@ noUsers: "Немає користувачів" editProfile: "Редагувати обліковий запис" noteDeleteConfirm: "Ви дійсно хочете видалити цей запис?" pinLimitExceeded: "Більше записів не можна закріпити" +intro: "Встановлення Misskey завершено! Будь ласка, створіть обліковий запис адміністратора." done: "Готово" processing: "Обробка" preview: "Попередній перегляд" @@ -245,6 +241,7 @@ removeAreYouSure: "Ви впевнені, що хочете видалити \"{ deleteAreYouSure: "Ви впевнені, що хочете видалити \"{x}\"?" resetAreYouSure: "Справді скинути?" saved: "Збережено" +messaging: "Чати" upload: "Завантажити" keepOriginalUploading: "Зберегти оригінальне зображення" keepOriginalUploadingDescription: "Зберігає початково завантажене зображення як є. Якщо вимкнено, версія для відображення в Інтернеті буде створена під час завантаження." @@ -257,6 +254,7 @@ uploadFromUrlMayTakeTime: "Завантаження може зайняти де explore: "Огляд" messageRead: "Прочитано" noMoreHistory: "Подальшої історії немає" +startMessaging: "Розпочати діалог" nUsersRead: "Прочитали {n}" agreeTo: "Я погоджуюсь з {0}" agreeBelow: "Я погоджуюся з наведеним нижче" @@ -331,10 +329,12 @@ enableLocalTimeline: "Увімкнути локальну стрічку" enableGlobalTimeline: "Увімкнути глобальну стрічку" disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх стрічок, навіть якщо вони вимкнуті." registration: "Реєстрація" +enableRegistration: "Дозволити реєстрацію" invite: "Запросити" driveCapacityPerLocalAccount: "Об'єм диска на одного локального користувача" driveCapacityPerRemoteAccount: "Об'єм диска на одного віддаленого користувача" inMb: "В мегабайтах" +iconUrl: "URL аватара" bannerUrl: "URL банера" backgroundImageUrl: "URL-адреса фонового зображення" basicInfo: "Основна інформація" @@ -348,8 +348,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Увімкнути hCaptcha" hcaptchaSiteKey: "Ключ сайту" hcaptchaSecretKey: "Секретний ключ" -mcaptchaSiteKey: "Ключ сайту" -mcaptchaSecretKey: "Секретний ключ" recaptcha: "reCAPTCHA" enableRecaptcha: "Увімкнути reCAPTCHA" recaptchaSiteKey: "Ключ сайту" @@ -407,6 +405,7 @@ share: "Поділитись" notFound: "Не знайдено" notFoundDescription: "Сторінка за вказаною адресою не знайдена." uploadFolder: "Місце для завантаження за замовчуванням" +cacheClear: "Очистити кеш" markAsReadAllNotifications: "Позначити всі сповіщення як прочитані" markAsReadAllUnreadNotes: "Позначити всі нотатки як прочитані" markAsReadAllTalkMessages: "Позначити всі повідомлення як прочитані" @@ -424,6 +423,8 @@ retype: "Введіть ще раз" noteOf: "Нотатка {user}" quoteAttached: "Цитата" quoteQuestion: "Ви хочете додати цитату?" +noMessagesYet: "Ще немає повідомлень" +newMessageExists: "Є нові повідомлення" onlyOneFileCanBeAttached: "До повідомлення можна вкласти лише один файл" signinRequired: "Будь ласка, авторизуйтесь" invitations: "Запрошення" @@ -445,7 +446,7 @@ or: "або" language: "Мова" uiLanguage: "Мова інтерфейсу" aboutX: "Про {x}" -native: "місцевий" +disableDrawer: "Не використовувати висувні меню" noHistory: "Історія порожня" signinHistory: "Історія входів" enableAdvancedMfm: "Увімкнути розширений MFM" @@ -523,8 +524,6 @@ output: "Вихід" script: "Скрипт" disablePagesScript: "Вимкнути AiScript на Сторінках" updateRemoteUser: "Оновити інформацію про віддаленого користувача" -unsetUserAvatar: "Деактивувати піктограму." -unsetUserBanner: "Випустити прапор." deleteAllFiles: "Видалити всі файли" deleteAllFilesConfirm: "Ви дійсно хочете видалити всі файли?" removeAllFollowing: "Скасувати всі підписки" @@ -624,7 +623,10 @@ abuseReported: "Дякуємо, вашу скаргу було відправл reporter: "Репортер" reporteeOrigin: "Про кого повідомлено" reporterOrigin: "Хто повідомив" +forwardReport: "Переслати звіт на віддалений інстанс" +forwardReportIsAnonymous: "Замість вашого облікового запису анонімний системний обліковий запис буде відображатися як доповідач на віддаленому інстансі" send: "Відправити" +abuseMarkAsResolved: "Позначити скаргу як вирішену" openInNewTab: "Відкрити в новій вкладці" openInSideView: "Відкрити збоку" defaultNavigationBehaviour: "Поведінка навігації за замовчуванням" @@ -642,7 +644,6 @@ createNewClip: "Створити нотатку" unclip: "Незакріплений" confirmToUnclipAlreadyClippedNote: "Ця нотатка вже включена до кліпу \"{name}\". Ви хочете виключити нотатку з цього кліпу?" public: "Публічний" -private: "Приватне" i18nInfo: "Misskey перекладається на різні мови волонтерами. Ви можете допомогти: {link}" manageAccessTokens: "Керування токенами доступу" accountInfo: "Інформація про акаунт" @@ -680,6 +681,7 @@ experimentalFeatures: "Експериментальні функції" developer: "Розробник" makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\"" makeExplorableDescription: "Вимкніть, щоб обліковий запис не показувався у розділі \"Огляд\"." +showGapBetweenNotesInTimeline: "Показувати розрив між записами у стрічці новин" duplicate: "Дублікат" left: "Лівий" center: "Центр" @@ -808,6 +810,7 @@ makeReactionsPublicDescription: "Це зробить список усіх ва classic: "Класичний" muteThread: "Приглушити тред" unmuteThread: "Скасувати глушіння" +ffVisibility: "Видимість підписок/підписників" continueThread: "Показати продовження треду" deleteAccountConfirm: "Це незворотно видалить ваш акаунт. Продовжити?" incorrectPassword: "Неправильний пароль." @@ -897,24 +900,6 @@ exploreOtherServers: "Знайти інший сервер" letsLookAtTimeline: "Перегляд історії" horizontal: "Збоку" youFollowing: "Підписки" -icon: "Аватар" -replies: "Відповісти" -renotes: "Поширити" -sourceCode: "Вихідний код" -flip: "Перевернути" -lastNDays: "Останні {n} днів" -postForm: "Створення нотатки" -information: "Інформація" -_chat: - invitations: "Запросити" - noHistory: "Історія порожня" - members: "Учасники" - home: "Домівка" - send: "Відправити" -_delivery: - stop: "Призупинено" - _type: - none: "Публікація" _achievements: earnedAt: "Відкрито" _types: @@ -1188,7 +1173,6 @@ _plugin: install: "Встановити плагін" installWarn: "Будь ласка, не встановлюйте плагінів, яким ви не довіряєте." manage: "Керування плагінами" - viewSource: "Переглянути вихідний код" _preferencesBackups: list: "Створені бекапи" saveNew: "Зберегти як новий" @@ -1241,6 +1225,11 @@ _wordMute: muteWords: "Заглушені слова" muteWordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової лінійки для \"АБО\"" muteWordsDescription2: "Для використання RegEx, ключові слова потрібно вписати поміж слешів \"/\"." + softDescription: "Приховати записи які відповідають критеріям зі стрічки подій." + hardDescription: "Приховати записи які відповідають критеріям зі стрічки подій. Також приховані записи не будуть додані до стрічки подій навіть якщо критерії буде змінено." + soft: "М'яко" + hard: "Жорстко" + mutedNotes: "Заблоковані нотатки" _instanceMute: instanceMuteDescription2: "Розділяйте новими рядками" title: "Приховує нотатки з перелічених інстансів." @@ -1281,6 +1270,7 @@ _theme: header: "Заголовок" navBg: "Фон бокової панелі" navFg: "Текст бокової панелі" + navHoverFg: "Текст бокової панелі (під курсором)" navActive: "Текст бокової панелі (активне)" navIndicator: "Індикатор бокової панелі" link: "Посилання" @@ -1297,18 +1287,30 @@ _theme: infoFg: "Текст інформації" infoWarnBg: "Фон попередження" infoWarnFg: "Текст попередження" + cwBg: "Фон чутливого змісту" + cwFg: "Текст чутливого змісту" + cwHoverBg: "Фон чутливого змісту (при наведенні)" toastBg: "Фон повідомлення" toastFg: "Текст повідомлення" buttonBg: "Фон кнопки" buttonHoverBg: "Фон кнопки (при наведенні)" inputBorder: "Край поля вводу" + listItemHoverBg: "Фон елементу в списку (при наведенні)" + driveFolderBg: "Фон папки на диску" + wallpaperOverlay: "Накладання шпалер" badge: "Значок" messageBg: "Фон переписки" + accentDarken: "Акцент (Затемлений)" + accentLighten: "Акцент (Освітлений)" fgHighlighted: "Виділений текст" _sfx: note: "Нотатки" noteMy: "Мої нотатки" notification: "Сповіщення" + chat: "Чати" + chatBg: "Чати (фон)" + antenna: "Прийом антени" + channel: "Повідомлення каналу" _ago: future: "Майбутнє" justNow: "Щойно" @@ -1329,6 +1331,7 @@ _2fa: alreadyRegistered: "Двофакторна автентифікація вже налаштована." step1: "Спершу встановіть на свій пристрій програму автентифікації (наприклад {a} або {b})." step2: "Потім відскануйте QR-код, який відображається на цьому екрані." + step2Url: "Ви також можете ввести цю URL-адресу, якщо використовуєте програму для ПК:" step3: "Щоб завершити налаштування, введіть токен, наданий вашою програмою." step4: "Відтепер будь-які майбутні спроби входу вимагатимуть такого токена." renewTOTPCancel: "Не зараз" @@ -1362,7 +1365,6 @@ _permissions: "read:channels": "Переглядати канали" "write:channels": "Змінювати канали" "read:gallery": "Перегляд галереї" - "write:chat": "Створювати та видаляти повідомлення" _auth: shareAccess: "Ви хочете надати \"{name}\" доступ до цього акаунту?" shareAccessAsk: "Ви впевнені, що хочете надати цій програмі доступ до вашого акаунту?" @@ -1466,7 +1468,6 @@ _profile: changeBanner: "Змінити банер" _exportOrImport: allNotes: "Всі нотатки" - clips: "Добірка" followingList: "Підписки" muteList: "Ігнорувати" blockingList: "Заблокувати" @@ -1511,6 +1512,9 @@ _pages: newPage: "Створити сторінку" editPage: "Редагувати сторінку" readPage: "Перегляд вихідного коду" + created: "Сторінка успішно створена." + updated: "Сторінка успішно оновлена." + deleted: "Сторінку видалено" pageSetting: "Налаштування сторінки" nameAlreadyExists: "Вказана адреса сторінки вже існує." invalidNameTitle: "Вказана адреса сторінки неприпустима." @@ -1577,7 +1581,6 @@ _notification: reaction: "Реакції" receiveFollowRequest: "Запити на підписку" followRequestAccepted: "Прийняті підписки" - login: "Увійти" app: "Сповіщення від додатків" _actions: reply: "Відповісти" @@ -1610,18 +1613,3 @@ _deck: _webhookSettings: name: "Ім'я" active: "Увімкнено" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "E-mail" -_moderationLogTypes: - suspend: "Призупинити" - resetPassword: "Скинути пароль" -_reversi: - total: "Всього" -_remoteLookupErrors: - _noSuchObject: - title: "Не знайдено" -_search: - searchScopeAll: "Всі" - searchScopeLocal: "Локальна" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml deleted file mode 100644 index 612df9e43c..0000000000 --- a/locales/uz-UZ.yml +++ /dev/null @@ -1,1099 +0,0 @@ ---- -_lang_: "O'zbek tili" -headlineMisskey: "Qaydlar tarmog'i" -introMisskey: "Xush kelibsiz! Misskey ochiq kodli, markazlashmagan mikroblogging xizmati.\nO'zingizni fikrlaringizni atrofingizdagilar bilan ulashish uchun \"Qaydlar\" yarating. 📡\nUstiga-ustak, \"Reaktsiyalar\" yordamida siz boshqalarning xatlari haqidagi o'zingizni xissiyotlaringizni bildiring. 👍\nQani, yangi dunyoni kashf qilaylik! 🚀" -poweredByMisskeyDescription: "{name} ochiq manbali Misskey(\"Misskey instance\" deb ataladi) platformasi tomonidan qurilgan servislardan biri. " -monthAndDay: "{day}/{month}" -search: "Izlash" -notifications: "Xabarnomalar" -username: "Foydalanuvchi nomi" -password: "Parol" -forgotPassword: "Parolni unutib qo'ydim" -fetchingAsApObject: "Fediversedan olib kelinmoqda..." -ok: "Ho'p" -gotIt: "Tushunarli!" -cancel: "Bekor qilish" -noThankYou: "Hozir emas" -enterUsername: "Foydalanuvchini nomini kiriting" -renotedBy: "{user} tomonidan qayta qayd etildi" -noNotes: "Qaydlar mavjud emas" -noNotifications: "Xabarlar mavjud emas" -instance: "Server" -settings: "Sozlamalar" -notificationSettings: "Xabarnoma sozlamalari" -basicSettings: "Asosiy sozlamalar" -otherSettings: "Qo‘shimcha sozlamalar" -openInWindow: "Yangi oynada ochish" -profile: "Profil" -timeline: "Xronologiya" -noAccountDescription: "Ushbu foydalanuvchi hali o'zi haqida ma'lumot yozmagan." -login: "Kirish" -loggingIn: "Kirilmoqda" -logout: "Chiqish" -signup: "Ro'yxatdan o'tish" -uploading: "Yuklanmoqda..." -save: "Saqlash" -users: "Foydalanuvchilar" -addUser: "Foydalanuvchi qo'shish" -favorite: "Sevimli" -favorites: "Sevimlilar" -unfavorite: "Sevimlidan chiqarish" -favorited: "sevimli" -alreadyFavorited: "allaqachon sevimlilar orasida" -cantFavorite: "sevimlilarga qo'shib bo'lmadi" -pin: "Profilga qadab qo'yish" -unpin: "Profildan olib tashlash" -copyContent: "Tarkibini nusxalash" -copyLink: "Havolani nusxalash" -delete: "O'chirib tashlash" -deleteAndEdit: "O'chirish va tahrirlash" -deleteAndEditConfirm: "O'chirib, tahrirlamoqchiligingizga ishonchingiz komilmi? Siz bu qaydga tegishli barcha reaktsiyalar va javoblarni yo'qotasiz." -addToList: "Ro‘yxatga qo‘shish" -addToAntenna: "Antennaga qo'shish" -sendMessage: "Xabar yuborish" -copyRSS: "RSS'ni nusxalash" -copyUsername: "Foydalanuvchi nomini nusxalash" -copyUserId: "Foydalanuvchi IDsini nusxalash" -copyNoteId: "Qayd IDsini ko'chirish" -copyFileId: "Fayl ID raqamini nusxalash" -copyFolderId: "Jild ID raqamini nusxalash" -copyProfileUrl: "Profil manzilini nusxalash" -searchUser: "Foydalanuvchini izlash" -reply: "Javob berish" -loadMore: "Ko‘proq ko‘rish" -showMore: "Ko‘proq ko‘rish" -showLess: "Yopish" -youGotNewFollower: "sizga obuna bo'ldi" -receiveFollowRequest: "Obuna bo'lishga ruxsat qabul qilindi" -followRequestAccepted: "Obuna bo'lishga ruxsat berildi" -mention: "Murojat" -mentions: "Eslatib o'tish" -directNotes: "Bevosita qaydlar" -importAndExport: "Import/eksport" -import: "Import" -export: "Eksport" -files: "Fayllar" -download: "Yuklab olish" -driveFileDeleteConfirm: "\"{name}\" o'chirib tashlamoqchimisiz? Ushbu fayldan foydalanadigan har qanday kontent ham oʻchiriladi." -unfollowConfirm: "{name}ga obunani bekor qilmoqchimisiz?" -exportRequested: "Eksport so'raldi. Bu ozgina vaqt olishi mumkin. Tugatilgandan so'ng sizning Diskingizga qo'shiladi" -importRequested: "Import so'raldi. Bu ozgina vaqt olishi mumkin." -lists: "Ro'yxatlar" -noLists: "Hech qanday ro'yxatlar mavjud emas" -note: "Qayd" -notes: "Qaydlar" -following: "Obuna bo‘lish" -followers: "Obunachilar" -followsYou: "Sizning obunachingiz." -createList: "Ro'yxat yaratish" -manageLists: "Ro'yxatlarni boshqarish." -error: "Xato" -somethingHappened: "Xatolik yuz berdi" -retry: "Qayta urinib ko'rish" -pageLoadError: "Sahifani yuklayotganda xatolik yuz berdi" -pageLoadErrorDescription: "Buni odatda tarmoq muammolarni yoki browser keshi keltirib chiqaradi. Keshni tozalab, keyinroq urinib ko'ring" -serverIsDead: "Server javob bermayabdi. Iltimos kuting va keyinroq urinib ko'ring" -youShouldUpgradeClient: "Iltimos, ushbu sahifani ko'rish uchun sahifani yangilang." -enterListName: "Ro'yxatga nom kiriting" -privacy: "Maxfiylik" -makeFollowManuallyApprove: "Yopiq akkaunt" -defaultNoteVisibility: "Standart ko'rinish" -follow: "Obuna bo‘lish" -followRequest: "Obuna bo'lish uchun ruxsat olish" -followRequests: "Obuna bo'lmoqchilar" -unfollow: "obunani bekor qilish" -followRequestPending: "obuna bo'lishga ruxsat kutilmoqda" -enterEmoji: "Emojini kiriting" -renote: "Qayta qayd etish" -unrenote: "Qayta qayd etishni bekor qilish" -renoted: "Qayta qayd etildi" -cantRenote: "Qayta qayd etish mumkin emas" -cantReRenote: "Repostni qayta joylashtirish mumkin emas." -quote: "Iqtibos keltirish" -inChannelRenote: "Faqat kanalga qayta qayd etish" -inChannelQuote: "Kanaldagi eslatmalar" -pinnedNote: "Qadalgan qayd" -pinned: "Profilga qadab qo'yish" -you: "Siz" -clickToShow: "Ko'rsatish uchun bosing" -sensitive: "Sezuvchan" -add: "Qo'shish" -reaction: "Reaktsiyalar" -reactions: "Reaktsiyalar" -reactionSettingDescription2: "Qayta tartiblash uchun ushlab turib siljiting, oʻchirish uchun bosing, qoʻshish uchun “+” tugmasini bosing." -rememberNoteVisibility: "Qaydning ko'rinish sozlamarini eslab qolish" -attachCancel: "Qo'shimchani olib tashlash" -markAsSensitive: "\"Hamma ko'rishi mumkin emas\" deb belgilash" -unmarkAsSensitive: "\"Hamma ko'rishi mumkin\" deb belgilash" -enterFileName: "Fayl nomini kiriting" -mute: "Ovozni o‘chirish" -unmute: "Ovozni yoqish" -renoteMute: "Qayta qaydlarni ovozini o'chirish" -renoteUnmute: "Qayta qaydlarni ovozini yoqish" -block: "Bloklash" -unblock: "Blokdan chiqarish" -suspend: "To'xtatish" -unsuspend: "Blokdan chiqarish" -blockConfirm: "Haqiqatdan ham quyidagi hisobni bloklashni xohlaysizmi? " -unblockConfirm: "Haqiqatdan ham quyidagi hisobni blokdan chiqarishni xohlaysizmi? " -suspendConfirm: "Bu hisobni to‘xtatib qo‘ymoqchi ekanligingizga ishonchingiz komilmi?" -unsuspendConfirm: "Tasdiqlashni to'xtatib turish" -selectList: "Ro'yxat tanlash" -editList: "Roʻyxatni tahrirlash" -selectChannel: "Kanalni tanlang" -selectAntenna: "Antennani tanlang" -editAntenna: "Antennani tahrirlang" -selectWidget: "Vidjet tanlash" -editWidgets: "Vidjetni tahrirlash" -editWidgetsExit: "Tugadi" -customEmojis: "Maxsus emoji" -emoji: "Emoji" -emojis: "Emoji" -emojiName: "Emoji nomi" -emojiUrl: "Emoji URL'i" -addEmoji: "Emoji qo'shish" -settingGuide: "Tavsiya qilingan sozlamalar" -cacheRemoteFiles: "Tashqi fayllarni keshlash" -cacheRemoteFilesDescription: "Ushbu sozlama o'chirilgan bo'lsa tashqi fayllar bevosita tashqi serverdan yuklanadi. Buni o'chirish ombor ishlatilishini kamaytiradi, lekin traffikni ko'paytiradi, chunki eskizlar generatsiya qilinmaydi." -youCanCleanRemoteFilesCache: "Fayl menejeridagi 🗑️ tugmasi yordamida barcha keshlarni oʻchirib tashlashingiz mumkin." -cacheRemoteSensitiveFiles: "Tashqi fayllarni keshlash" -cacheRemoteSensitiveFilesDescription: "Bu sozlama oʻchiq boʻlsa, \"barcha ko'rishi mumkin bo'lmagan\" fayllar keshlashsiz toʻgʻridan-toʻgʻri masofaviy serverdan yuklanadi." -flagAsBot: "Ushbu akkauntni bot sifatida belgilash" -flagAsBotDescription: "Agar bu akkaunt bot tomonidan boshqaralayotgan bo'lsa, bu sozlamani yoqing. Sozlama yoqilganda, boshqa foydalanuvchilar uchun belgi sifatida ishlaydi, va Misskey ichki tizimlari bu akkauntni bot ekanini biladi." -flagAsCat: "Bu akkauntni mushuk sifatida belgilash" -flagAsCatDescription: "Ushbu akkauntni mushuk sifatida belgilash uchun ushbu sozlamani yoqing." -flagShowTimelineReplies: "Javoblarni xronogoliya bo'yicha ko'rsatish" -flagShowTimelineRepliesDescription: "Bu parametr yoqilganda, lentada foydalanuvchi xabarlariga javob berilgan xabarlar ham ko'rinadi" -autoAcceptFollowed: "Obunachilarni avtomatik ravishda qabul qilish" -addAccount: "Akkaunt qo'shish" -reloadAccountsList: "Hisoblar ro'yxatini yangilash" -loginFailed: "Tizimga kirishda xatolik yuz berdi" -showOnRemote: "Masofaviy boshqaruvni ko'rish" -general: "Asosiy" -wallpaper: "Fon rasmi" -setWallpaper: "Fon rasmini o'rnatish" -removeWallpaper: "Fon rasmini olib tashlash" -searchWith: "Izlash: {q}" -youHaveNoLists: "Sizda hech qanday ro'yxatlar mavjud emas" -followConfirm: "{name} ga obuna bo'lmoqchimisiz?" -proxyAccount: "Proksi hisob" -proxyAccountDescription: "Proksi-hisob qaydnomasi - bu ma'lum shartlar ostida foydalanuvchi uchun masofaviy kuzatuvchi sifatida ishlaydigan hisob. Misol uchun, foydalanuvchi uzoq foydalanuvchini roʻyxatga qoʻyganda, roʻyxatdagi foydalanuvchini hech kim kuzatib turmasa, faoliyat serverga yetkazilmaydi, shuning uchun biz proksi hisobi ularning oʻrniga ularni kuzatishini xohlaymiz." -host: "Host" -selectUser: "Foydalanuvchini tanlang" -recipient: "Qabul qiluvchi" -annotation: "Izohlar" -federation: "Federatsiya" -instances: "Serverlar" -registeredAt: "Ro'yhatdan o'tgan" -latestRequestReceivedAt: "Oxirgi qabul qilingan so'rov" -latestStatus: "So'nggi holat" -storageUsage: "Ishlatilgan xotira" -charts: "Diagrammalar" -perHour: "Soatbay" -perDay: "Kunbay" -stopActivityDelivery: "Faollikni jo'natishi to'xtatish" -blockThisInstance: "Ko;rsatilgan serverni bloklash" -operations: "Amallar" -software: "Dastur" -version: "Versiya" -metadata: "Meta ma'lumot" -withNFiles: "{n} ta fayl(lar)" -monitor: "Kuzatish" -jobQueue: "Vazifalar navbati" -cpuAndMemory: "CPU va Xotira" -network: "Tarmoq" -disk: "Disk" -instanceInfo: "Instans haqida ma'lumot" -statistics: "Statistika" -clearQueue: "Navbatni tozalash" -clearQueueConfirmTitle: "Navbatni tozalamoqchimisiz?" -clearQueueConfirmText: "Yetkazib berilmagan xabarlar yetkazilmaydi. Odatda buni qilish shart emas." -clearCachedFiles: "Keshni tozalash" -clearCachedFilesConfirm: "Barcha keshlangan masofaviy fayllar oʻchirilsinmi?" -blockedInstances: "Bloklangan serverlar" -blockedInstancesDescription: "Bloklanmoqchi bo'lgan serverlaringiz hostlarini yangi qatorlar bilan ajrating. Bloklangan server bu server bilan o‘zaro aloqada bo‘lmaydi. Subdomenlar ham bloklangan." -muteAndBlock: "Ovozsiz va Bloklangan" -mutedUsers: "Ovozsiz foydalanuvchilar" -blockedUsers: "Bloklangan foydalanuvchilar" -noUsers: "Foydalanuvchilar yo‘q" -editProfile: "Profilni o'zgartirish" -noteDeleteConfirm: "Haqiqatan ham bu qaydni oʻchirib tashlamoqchimisiz?" -pinLimitExceeded: "Siz boshqa qaydlarni mahkamlay olmaysiz" -done: "Bajarildi" -processing: "Amaliyotda" -preview: "Ko'rish" -default: "Odatiy" -defaultValueIs: "Sukut bo'yicha: {value}" -noCustomEmojis: "Emojilar mavjud emas" -noJobs: "Vazifalar yo'q" -federating: "Ittifoqdosh" -blocked: "Bloklangan" -suspended: "To'xtatilgan" -all: "Barcha" -subscribing: "Obuna bo'lish" -publishing: "Yuborilmoqda" -notResponding: "Javob bermayapti" -instanceFollowing: "server obuna bo'ladi" -instanceFollowers: "server obunachisi" -instanceUsers: "server foydalanuvchisi" -changePassword: "Parolni o‘zgartirish" -security: "Xavfsizlik" -retypedNotMatch: "Maydonlar mos kelmayapti" -currentPassword: "Joriy parol" -newPassword: "Yangi parol" -newPasswordRetype: "Yangi parolni boshqatdan tering" -attachFile: "Fayl biriktirish" -more: "Ko'proq!" -featured: "ta'kidlash" -usernameOrUserId: "Foydalanuvchi nomi yoki identifikatori" -noSuchUser: "Foydalanuvchi topilmadi" -lookup: "So'rov" -announcements: "Bildirishnomalar" -imageUrl: "Rasm URL" -remove: "O'chirib tashlash" -removed: "Muvaffaqiyatli o'chirildi" -removeAreYouSure: "“{x}”ni olib tashlamoqchi ekanligingizga ishonchingiz komilmi?" -deleteAreYouSure: "“{x}”ni chindan ham yo'q qilmoqchimisiz?" -resetAreYouSure: "Haqiqatan ham qayta tiklansinmi?" -saved: "Saqlandi" -upload: "Yuklash" -keepOriginalUploading: "Asl rasmni saqlang" -keepOriginalUploadingDescription: "Rasmlarni yuklashda asl nusxasini saqlaydi. Agar o'chirilgan bo'lsa, brauzer yuklangandan keyin nashr qilish uchun rasm yaratadi." -fromDrive: "Drive orqali" -fromUrl: "URL dan" -uploadFromUrl: "URL orqali yuklash" -uploadFromUrlDescription: "Yuklamoqchi bo'lgan faylingizga havola" -uploadFromUrlRequested: "yuklab olish so'ralgan" -uploadFromUrlMayTakeTime: "Yuklash tugallanishi uchun biroz vaqt ketishi mumkin." -explore: "Ko'rib chiqish" -messageRead: "O‘qildi" -noMoreHistory: "Buning ortida hech qanday hikoya yo'q" -nUsersRead: "{n} tomonidan o'qildi" -agreeTo: "Men {0} ga roziman" -agree: "Rozi bo'lish" -agreeBelow: "Men quyidagilarga roziman" -basicNotesBeforeCreateAccount: "Muhim qaydlar" -termsOfService: "Foydalanish shartlari" -start: "Boshlash" -home: "Bosh sahifa" -remoteUserCaution: "Bu foydalanuvchi uzoqda bo'lganligi sababli, ko'rsatilgan ma'lumotlar to'liq bo'lmasligi mumkin." -activity: "Faollik" -images: "Rasmlar" -image: "Rasm" -birthday: "Tug'ilgan kun" -yearsOld: "{age} yashar" -registeredDate: "Ro'yxatdan o'tgan sanasi" -location: "Manzil" -theme: "Rang sxemasi" -themeForLightMode: "Yorug' rejim uchun rang sxemasi" -themeForDarkMode: "Qorong'i rejim uchun rang sxemasi" -light: "Yorug'" -dark: "Qorongʻi" -lightThemes: "Yorug‘ rang sxemasi" -darkThemes: "Qorong'i rang sxemasi" -syncDeviceDarkMode: "Qurilmangizning qorong‘i rejimi bilan sinxronlashtiring" -drive: "Disk" -fileName: "Fayl nomi" -selectFile: "Faylni tanlang" -selectFiles: "Fayllarni tanlang" -selectFolder: "Jildni tanlang" -selectFolders: "Jildlarni tanlang" -renameFile: "Faylni nomini tahrirlash" -folderName: "Jild nomi" -createFolder: "Papka qo'shish" -renameFolder: "Papka nomini o‘zgartirish" -deleteFolder: "Papkani o‘chirish" -addFile: "Fayl qo‘shish" -emptyDrive: "Diskingiz bo'sh" -emptyFolder: "Ushbu papka bo'sh" -unableToDelete: "O'chirilmadi" -inputNewFileName: "Yangi fayl nomini kiriting" -inputNewDescription: "Iltimos, yangi sarlavha kiriting." -inputNewFolderName: "Yangi papka nomini kiriting" -circularReferenceFolder: "Belgilangan papka siz ko'chirmoqchi bo'lgan jildning pastki jildidir." -hasChildFilesOrFolders: "Bu papka boʻsh emas va uni oʻchirib boʻlmaydi." -copyUrl: "Bog'lamadan nusxa olish" -rename: "Qayta nomlash" -avatar: "Avatar" -banner: "Banner" -displayOfSensitiveMedia: "Nozik kontentni ko'rish" -whenServerDisconnected: "server bilan aloqa uzilganda" -disconnectedFromServer: "Server bilan ulanish uzulib qoldi" -reload: "Yangilash" -doNothing: "E'tiborsiz qoldirish" -reloadConfirm: "Timeline'ni yangilashni xohlaysizmi?" -watch: "Kuzatmoq" -unwatch: "Kuzatishni to'xtatish" -accept: "Ruxsat" -reject: "Rad etish" -normal: "Yaxshi" -instanceName: "Server nomi" -instanceDescription: "Server tavsifi" -maintainerName: "Qo'llab-quvvatlovchi" -maintainerEmail: "Administratorning elektron pochtasi" -tosUrl: "Foydalanish shartlariga havola" -thisYear: "Joriy yil" -thisMonth: "Shu oy" -today: "Bugun" -dayX: "{day}" -monthX: "{month}" -yearX: "{year}" -pages: "Sahifalar" -integration: "Integratsiya" -connectService: "Ulash" -disconnectService: "Uzish" -enableLocalTimeline: "Mahalliy vaqt mintaqasini yoqing" -enableGlobalTimeline: "Global vaqt mintaqasini yoqing" -disablingTimelinesInfo: "Administratorlar va Moderatorlar har doim barcha vaqt jadvallariga kirish huquqiga ega bo'ladilar, hatto ular yoqilmagan bo'lsa ham." -registration: "Ro'yxatdan o'tish" -invite: "Taklif qilish" -driveCapacityPerLocalAccount: "Har bir mahalliy foydalanuvchi uchun disk maydoni" -driveCapacityPerRemoteAccount: "Har bir masofaviy foydalanuvchi uchun disk maydoni" -inMb: "Megabaytlarda" -bannerUrl: "Banner URLi" -backgroundImageUrl: "Fon rasmi URL manzili" -basicInfo: "Asosiy ma'lumot" -pinnedUsers: "Qadalgan foydalanuvchilar" -pinnedUsersDescription: "Har bir qatorga bitta foydalanuvchi nomini kiriting. Bu yerda sanab oʻtilgan foydalanuvchilar “Oʻrganish” yorligʻiga bogʻlanadi." -pinnedPages: "Qadalgan Sahifalar" -pinnedClipId: "Qadalgan xabar IDsi" -pinnedNotes: "Qadalgan qayd" -hcaptcha: "hCaptcha" -enableHcaptcha: "hCaptchani yoqish" -hcaptchaSiteKey: "Sayt kaliti" -hcaptchaSecretKey: "Mahfiy kalit" -mcaptchaSiteKey: "Sayt kaliti" -mcaptchaSecretKey: "Maxfiy kalit" -recaptcha: "reCAPTCHA" -enableRecaptcha: "reCAPTCHA ni yoqish" -recaptchaSiteKey: "Sayt kaliti" -recaptchaSecretKey: "Maxfiy kalit" -turnstile: "Turniket" -enableTurnstile: "Turniketni yoqish" -turnstileSiteKey: "Sayt kaliti" -turnstileSecretKey: "Maxfiy kalit" -avoidMultiCaptchaConfirm: "\nBir nechta Captcha tizimlaridan foydalanish ular o'rtasida noqulaylik olib kelishi mumkin. Hozirda faol bo'lgan boshqa Captcha tizimlarini o'chirib qo'ymoqchimisiz? Agar siz ularning faol bo'lishini istasangiz, bekor qilish tugmasini bosing." -antennas: "Antennalar" -manageAntennas: "Antennalarni boshqarish" -name: "Ism" -antennaSource: "Antenna manbai" -antennaKeywords: "Kalit so'zni qabul qilish" -antennaExcludeKeywords: "Istisno qilingan kalit so'zlar" -antennaKeywordsDescription: "VA sharti uchun bo'shliqlar bilan yoki YOKI sharti uchun qator uzilishlari bilan ajrating." -notifyAntenna: "Yangi qaydlar haqida menga xabar bering" -withFileAntenna: "Faqatgina fayli bor qaydlar" -enableServiceworker: "Bildirish nomalarni olish" -antennaUsersDescription: "Har bir foydalunvchi nomini alohida qatorga yozing" -caseSensitive: "Katta-kichik harfni farqlash" -withReplies: "Javob yo'llash" -connectedTo: "Quyidagi akkountlarga ulangan" -notesAndReplies: "Qaydlar va javoblar" -withFiles: "Fayllar" -silence: "Jim qilish" -silenceConfirm: "Rostdan ham ushbu foydalanuvchini jim qilmoqchimisiz?" -unsilence: "Jim qilishni bekor qilish" -unsilenceConfirm: "Rostdan ham ushbu foydalanuvchini ovozsiz \nqilmoqchimisiz?" -popularUsers: "Mashhur foydalanuvchilar." -recentlyUpdatedUsers: "Yaqinda ro'yxatdan o'tgan foydalanuvchilar" -recentlyRegisteredUsers: "Yaqinda ro'yxatdan o'tgan foydalanuvchilar" -recentlyDiscoveredUsers: "Yangi foydalanuvchilar" -exploreUsersCount: "{count} ta foydalanuvchi bor" -exploreFediverse: "Fediversni ko'rib chiqing" -popularTags: "Ommabop teglar" -userList: "Ro'yxatlar" -about: "Haqida" -aboutMisskey: "Misskey haqida" -administrator: "Administrator" -token: "Tasdiqlash" -2fa: "Ikki faktorli autentifikatsiya" -totp: "Autentifikatsiya ilovasi" -totpDescription: "Bir martalik parollarni kiritish uchun autentifikatsiya ilovasidan foydalaning" -moderator: "Moderator" -moderation: "Moderatsiya" -nUsersMentioned: "{n} tomonidan chop etilgan" -securityKeyAndPasskey: "Xavfsizlik kaliti va maxfiy so'z" -securityKey: "Xavfsizlik kaliti" -lastUsed: "Oxirgi marta foydalanilgan" -lastUsedAt: "Oxirgi marta {t} da foydalanilgan" -unregister: "ro'yxatdan chiqarish" -passwordLessLogin: "Parolsiz kirshni sozlash" -passwordLessLoginDescription: "Parolsiz kirish" -resetPassword: "Parolni tiklash" -newPasswordIs: "Yangi parolingiz {password}" -reduceUiAnimation: "Interfeysdagi animatsiyani kamaytirish" -share: "Yuborish" -notFound: "Topilmadi" -notFoundDescription: "Ushbu sahifa topilmadi" -uploadFolder: "Jildni yuklash" -markAsReadAllNotifications: "Bildirishnomalarni o'qilgan deb belgilash" -markAsReadAllUnreadNotes: "Barch xabarlarni oq'ilgan deb belgilash" -markAsReadAllTalkMessages: "Barcha suhbatlarni o'qilgan deb belgilang" -help: "Yordam" -inputMessageHere: "Xabar kiriting" -close: "Yopish" -invites: "Taklif qilish" -members: "A'zolar" -transfer: "topshiriq" -title: "Sarlavha" -text: "Matn" -enable: "Yoqish" -next: "Keyingisi" -retype: "Qayta kiriting" -noteOf: "{user} tomonidan joylandi\n" -quoteAttached: "Iqtibos" -quoteQuestion: "Iqtibos sifatida qo'shilsinmi?" -onlyOneFileCanBeAttached: "Faqat bitta faylni biriktirish mumkin" -signinRequired: "Davom etishdan oldin ro'yhatdan o'tishingiz yoki tizimga kirishingiz kerak" -invitations: "Taklif qilish" -invitationCode: "taklif qilish kodi" -checking: "Tekshirilmoqda" -available: "Mavjud" -unavailable: "Mavjud emas" -usernameInvalidFormat: "Siz a~z, A~Z, 0~9, _ dan foydalanishingiz mumkin" -tooShort: "Juda qisqa" -tooLong: "juda uzun" -weakPassword: "Zaif parol" -normalPassword: "Oddiy parol" -strongPassword: "Kuchli parol" -passwordMatched: "Mos keldi" -passwordNotMatched: "mos kelmadi" -signinWith: "{x} bilan tizimga kirish" -signinFailed: "Tizimga kirishda xatolik yuz berdi. Iltimos, foydalanuvchi nomingiz va parolingizni tekshiring." -or: "yoki" -language: "til" -uiLanguage: "Interfeys tili" -aboutX: "{x} haqida" -emojiStyle: "Emoji ko'rinishi" -native: "Mahalliy" -showNoteActionsOnlyHover: "Eslatma amallarini faqat sichqonchani olib borganda ko‘rsatish" -noHistory: "Tarix yo'q" -signinHistory: "kirish tarixi" -enableAdvancedMfm: "MFMni faollashtirish" -doing: "Bajarilmoqda..." -category: "kategoriya" -tags: "teg" -docSource: "Ushbu hujjatning manbasi" -createAccount: "Akkaunt yaratish" -existingAccount: "mavjud akkaunt" -regenerate: "regeneratsiya" -fontSize: "shrift hajmi" -limitTo: "{x} gacha" -noFollowRequests: "obuna uchun so'rov yo'q" -openImageInNewTab: "Rasmni boshqa oynada ochish" -dashboard: "Boshqaruv paneli" -local: "Mahalliy" -remote: "masofaviy" -total: "Jami" -weekOverWeekChanges: "Oxirgi haftadagi o'zgarishlar" -dayOverDayChanges: "Kecha bo'lgan o'zgarishlar" -appearance: "Tasgqi ko'rinish" -clientSettings: "Klient sozlamalari" -accountSettings: "Profil sozlamalari" -promotion: "rag'batlantirish" -promote: "targ'ib qilish" -numberOfDays: "kunlar soni" -hideThisNote: "bu eslatmani yashiring" -showFeaturedNotesInTimeline: "Tanlangan qaydlarni Timelineda ko'rsatish" -objectStorage: "ob'ektni saqlash" -useObjectStorage: "Ob'ektni saqlashdan foydalaning" -objectStorageBaseUrl: "Asosiy URL" -objectStorageBaseUrlDesc: "Malumot va foydalanish uchun URL. Agar siz CDN yoki proksi-serverdan foydalanayotgan bo'lsangiz, URL manzili, S3: 'https://.s3.amazonaws.com', GCS va boshqalar: 'https://storage.googleapis.com/'." -objectStorageBucket: "Bucket" -objectStorageBucketDesc: "Iltimos, foydalaniladigan xizmatning bucket nomini belgilang." -objectStoragePrefix: "Prefix" -objectStorageEndpoint: "Endpoint" -objectStorageRegion: "Mintaqa" -objectStorageRegionDesc: "'xx-east-1' kabi mintaqani belgilang. Agar xizmatingizda mintaqa tushunchasi bo'lmasa, `us-east-1` dan foydalaning. AWS konfiguratsiya fayllari yoki muhit oʻzgaruvchilariga havola qilishda boʻsh qoldiring." -objectStorageUseSSL: "SSL dan foydalaning" -objectStorageUseSSLDesc: "API ulanishlari uchun https dan foydalanmasangiz, belgini olib tashlang" -objectStorageUseProxy: "Proksi-serverdan foydalaning" -objectStorageUseProxyDesc: "Proksi-serverdan foydalanishni xohlamasangiz, uni o'chiring" -objectStorageSetPublicRead: "Yuklashda \"public-read\" ni o'rnating" -serverLogs: "Server protokoli" -deleteAll: "Hammasini o'chirib tashlash" -showFixedPostForm: "Taqdim etish shaklini vaqt jadvalining yuqori qismida ko'rsating" -newNoteRecived: "Yangi qaydlar mavjud emas" -sounds: "Tovushlar" -sound: "ovoz" -listen: "Eshitish" -none: "Hechnima" -showInPage: "Sahifada ko'rsatish" -popout: "Oching" -volume: "Ovoz balandligi" -details: "Batafsil" -chooseEmoji: "Emojini tanlang" -unableToProcess: "Opertsiya bajarilmadi" -recentUsed: "Oxirgi ishlatilganlar" -install: "O‘rnatish" -uninstall: "O‘chirib tashlash" -installedApps: "O'rnatilgan ilovalar" -nothing: "Hech narsa yo'q" -installedDate: "O'rnatish sanasi" -lastUsedDate: "Oxirgi marta ishlatilgan sana" -state: "Holat" -sort: "saralamoq" -ascendingOrder: "O'sish bo'yicha" -descendingOrder: "Kamayish bo'yicha" -scratchpad: "Qoralama" -output: "Chiqish" -script: "Skript" -disablePagesScript: "AiScriptni sahifalardan o'chirish" -updateRemoteUser: "Masofaviy foydalanuvchi ma'lumotlarini yangilash" -deleteAllFiles: "barcha fayllarni o'chirish" -deleteAllFilesConfirm: "Barcha fayllar oʻchirilsinmi?" -removeAllFollowing: "Barcha obunalarni o'chirish" -userSuspended: "Bu foydalanuvchi muzlatilgan." -userSilenced: "Ushbu foydalanuvchi jim qilingan" -yourAccountSuspendedTitle: "akkaunt muzlatilgan" -yourAccountSuspendedDescription: "Ushbu akkaunt serverning xizmat ko'rsatish shartlarini buzish kabi sabablarga ko'ra to'xtatilgan. Tafsilotlar uchun administratoringizga murojaat qiling. Iltimos, yangi akkaunt yaratmang." -tokenRevoked: "token yaroqsiz" -tokenRevokedDescription: "Kirish tokeningizni muddati tugagan. Iltimos, qaytadan kiring." -accountDeleted: "akkaunt o'chirildi" -accountDeletedDescription: "Bu akkaunt oʻchirildi." -menu: "Menyu" -divider: "Ajratrmoq" -addItem: "Element qo'shish" -rearrange: "Qayta saralash" -inboxUrl: "Qabul qilingan xabarlar URL manzili" -serviceworkerInfo: "bildirishnomalar uchun yoqilgan bo'lishi kerak." -deletedNote: "Oʻchirilgan post" -visibility: "Ko'rinishi" -poll: "So'ro'vnoma" -useCw: "Kontentni yashirish" -enablePlayer: "Video pleyerni ochish" -disablePlayer: "Video pleyerni yopish" -expandTweet: "Xabarni kengaytirish" -themeEditor: "Rang sxemasi muharriri" -description: "tavsif" -describeFile: "sarlavha qo'shing" -enterFileDescription: "sarlavha kiriting" -author: "muallif" -leaveConfirm: "Sizda saqlanmagan oʻzgarishlar bor. Bekor qilinsinmi?" -manage: "Administratsiya" -plugins: "Kengaytmalar, plaginlar" -preferencesBackups: "Sozlamalarni zahiralash" -useBlurEffectForModal: "Modal uchun xiralashtirish effektidan foydalaning" -useFullReactionPicker: "Katta oynada reaksiya tanlash" -width: "kengligi" -height: "balandligi" -large: "Katta" -medium: "O'rta" -small: "kichik" -generateAccessToken: "Kirish tokenini yaratish" -permission: "Ruxsatlar" -enableAll: "Yoqish" -disableAll: "hammasini o'chirib qo'ying" -tokenRequested: "Hisobga kirish" -pluginTokenRequestedDescription: "Bu plagin shu yerda belgilanganlarga qodir bo'ladi" -notificationType: "Bildirishnoma turi" -edit: "Tahrirlash" -emailServer: "Email server" -email: "Email" -emailAddress: "E-pochtangiz:" -smtpConfig: "SMTP server sozlamalari" -smtpHost: "Host" -smtpPort: "Port" -smtpUser: "Foydalanuvchi nomi" -smtpPass: "Parol" -testEmail: "Email jo'natmani testlash" -userSaysSomething: "{name} nimadir dedi" -makeActive: "Faol" -display: "Displey" -copy: "Nusxa olish" -metrics: "Metrikalar" -overview: "Umumiy ma'lumot" -logs: "Jurnallar" -delayed: "Kechiktirildi" -database: "Ma'lumotlar bazasi" -channel: "Kanallar" -create: "Yaratish" -notificationSetting: "Bildirishnoma sozlamalari" -notificationSettingDesc: "Ko'rsatish uchun bildirishnoma turlarini tanlang." -useGlobalSetting: "Global sozlamalardan foydalanish" -other: "Qo‘shimcha" -regenerateLoginToken: "Kirish tokenini qayta yaratish" -setMultipleBySeparatingWithSpace: "Bo'sh joy qoldirib, bir necha ma'lumot kiritish mumkin" -fileIdOrUrl: "Fayl ID'si yoki URL havolasi" -behavior: "Hatti-harakatlar" -sample: "Namuna" -abuseReports: "Shikoyatlar" -reportAbuse: "Shikoyat qilish" -reportAbuseOf: "{name} ustidan shikoyat qilish" -abuseReported: "Shikoyatingiz yetkazildi. Ma'lumot uchun rahmat." -reporter: "Shikoyat qiluvchi" -reporteeOrigin: "Xabarning kelib chiqishi" -reporterOrigin: "Xabarchining joylashuvi" -send: "Yuborish" -openInNewTab: "Yangi tab da ochish" -openInSideView: "Yon panelda ochish" -defaultNavigationBehaviour: "Standart navigatsiya harakati" -editTheseSettingsMayBreakAccount: "Bu sozlamalarni o'zgartirish hisobingizga zarar yetkazishi mumkin." -waitingFor: "{x}ni kutayapman" -random: "Tasodifiy" -system: "Tizim" -switchUi: "Interfeysni almashtirish" -desktop: "Brauzer rejimi" -clip: "Klip" -createNew: "Yangi yaratish" -optional: "Ixtiyoriy" -createNewClip: "Yangi klip yaratish" -unclip: "qirqish\n" -confirmToUnclipAlreadyClippedNote: "Ushbu xat allaqachon \"{name}\" klipga tegishli. Uni ushbu klipdan olib tashlashni xohlaysizmi?" -public: "Ommaviy" -i18nInfo: "Misskey bir qancha volontyorlar yordamida bir qancha tillarga tarjima qilingan. Ushbu {link} orqali ularga yordam berishingiz mumkin." -manageAccessTokens: "Kirish tokenlarini boshqarish" -accountInfo: "Akkount haqida ma'lumot" -notesCount: "Xatlar soni" -repliesCount: "Yuborilgan javoblar soni" -renotesCount: "Qayta yuborilgan xatlar soni" -repliedCount: "Qabul qilingan javoblar soni" -renotedCount: "Qayta yuborilgan xatlar soni" -followingCount: "Obuna bo'lingan akkountlar soni" -followersCount: "Obunachilar soni" -sentReactionsCount: "Yuborilgan reaksiyalar soni" -receivedReactionsCount: "Qabul qilingan reaksiyalar soni" -pollVotesCount: "Berilgan ovozlar soni" -pollVotedCount: "Qabul qilingan ovozlar soni" -yes: "Ha" -no: "Yo'q" -driveFilesCount: "Diskdagi fayllar soni" -driveUsage: "Ishlatilgan disk joyi" -noCrawleDescription: "Qidiruv tizimlari sizning profilingiz, sahifalaringiz, xatlaringiz va hokazolarni belgilamasligi uchun so'rov yuborish" -lockedAccountInfo: "Xatlaringizni faqatgina obunachilaringizga ko'rsatishni xohlasangiz unda \"Faqat Obunachilar uchun\" xususiyatini yoqishingiz lozim. Bo'lmasa sizning yozgan xatlaringiz hammaga ko'rinadi." -alwaysMarkSensitive: "Avvaldan ta'sirchan kontent deb belgilash" -loadRawImages: "Thumbnaillarsiz Original rasmni yuklash" -disableShowingAnimatedImages: "Animatsiyali rasmlarni ko'rsatmaslik" -verificationEmailSent: "Emailingizga tasdiqlash xabari yuborildi. Iltimos linkda ko'rsatilgan amallarga rioya qiling" -notSet: "Sozlanmagan" -emailVerified: "Elektron pochta manzili tasdiqlandi" -pageLikesCount: "Sahifadagi like'lar soni" -contact: "aloqa uchun manzil" -useSystemFont: "Tizimdagi standart shriftidan foydalaning" -clips: "Klip" -experimentalFeatures: "eksperimental xususiyatlar" -experimental: "eksperimental" -developer: "Dasturchi" -makeExplorable: "Akkauntingizni topishni osonlashtiring" -duplicate: "Dublikat" -left: "Chap(dagi)" -center: "Markaz" -wide: "Keng" -narrow: "Tor" -reloadToApplySetting: "Bu sozlamalar sahifa yangilangandagina kuchga kiradi. Hozir yangilashni istaysizmi?" -needReloadToApply: "Sahifani yangilash talab etiladi." -clearCache: "Keshni tozalash" -onlineUsersCount: "Faol userlar" -nUsers: "{n} ta foydalanuvchi" -myTheme: "Mening rang sxemam" -backgroundColor: "Fon" -accentColor: "Urg'u" -textColor: "Matn" -saveAs: "Boshqacha saqlash" -advanced: "Murakkab" -advancedSettings: "Qo'shimcha sozlashlar" -value: "Qiymati" -createdAt: "Yaratilish vaqti" -updatedAt: "yangilangan sana" -saveConfirm: "O'zgartirishni saqlash?" -deleteConfirm: "o'chirishni tasdiqlash" -invalidValue: "noto'g'ri qiymat" -registry: "ro'yhatga olish" -closeAccount: "hisobni yopish / profilni yopish" -currentVersion: "joriy versiya" -latestVersion: "so'ngi versiya" -youAreRunningUpToDateClient: "siz so'ngi versiyali ilovani ishlatyapsiz" -newVersionOfClientAvailable: "Mijozning yangi versiyasi mavjud." -usageAmount: "foydalanish miqdori" -capacity: "sig'im" -inUse: "allaqachon band" -editCode: "kodni tahrirlash" -apply: "Ilova" -receiveAnnouncementFromInstance: "Serverdan bildirishnomalarni oling" -emailNotification: "E-mail xabarlari" -publish: "Chiqarish" -inChannelSearch: "Kanal qidirish" -useReactionPickerForContextMenu: "kontekst menyusi uchun reaktsiya tanlash vositasidan foydalaning" -typingUsers: "{users} yozmoqda" -jumpToSpecifiedDate: "Muayyan sanaga o'tish" -showingPastTimeline: "O'tgan vaqt jadvallarini ko'rsatish" -clear: "aniq" -markAllAsRead: "hammasini o'qilgan deb belgilang" -goBack: "qaytish" -unlikeConfirm: "Un like qilishni xohlaysizmi?" -fullView: "to'liq ko'rish" -quitFullView: "Toʻliq koʻrishdan chiqish" -addDescription: "Tavsif qo'shing" -info: "Haqida" -userInfo: "Foydalanuvchi ma'lumotlari" -unknown: "aniq emas" -onlineStatus: "onlayn holat" -hideOnlineStatus: "onlayn holatni yashirish" -hideOnlineStatusDescription: "Onlayn statusingizni yashirish, qidiruv kabi baʼzi funksiyalardan foydalanish imkoniyatini kamaytirishi mumkin." -online: "onlayn" -active: "Aktiv" -offline: "oflayn" -notRecommended: "tavsiya etilmaydi" -selectAccount: "Akkauntni tanlang" -switchAccount: "akkauntni almashtirish" -enabled: "yaroqli" -disabled: "yaroqsiz" -quickAction: "tezkor harakat" -user: "Foydalanuvchilar" -administration: "Administratsiya" -accounts: "akkaunt" -switch: "almashtirish" -noBotProtectionWarning: "Bot himoyasi sozlanmagan." -configure: "sozlamoq" -postToGallery: "Yangi galleriya posti" -gallery: "Galereya" -recentPosts: "So'nggi postlar" -popularPosts: "Mashhur postlar" -shareWithNote: "Eslatmani ulashish" -ads: "Reklama" -startingperiod: "Boshlanish davri" -memo: "Eslatma" -priority: "Ustuvorlik" -high: "Yuqori" -middle: "O'rta" -low: "Quyi" -ratio: "nisbat" -previewNoteText: "Razm solish" -customCss: "Maxsus CSS" -global: "Global" -squareAvatars: "Kvadrat avatarkalar" -sent: "Yuborish" -received: "Qabul qilingan" -searchResult: "Qidiruv natijalari" -hashtags: "Hashteglar" -troubleshooting: "Muammolarni bartaraf qilish" -useBlurEffect: "Interfeysda xiralashtiruvchi effektlardan foydalanish" -learnMore: "Batafsilroq" -misskeyUpdated: "Misskey yangilandi!" -whatIsNew: "O'zgarishlarni ko'rish" -translate: "Tarjima qilish" -translatedFrom: "{x} tilidan tarjima qilindi" -devMode: "Dasturchi rejimi" -lastCommunication: "Oxirgi muloqot" -resolved: "Hal qilingan" -unresolved: "Hal qilinmagan" -breakFollow: "Obunachini o'chirish" -breakFollowConfirm: "Obunachini o'chirmoqchimisiz?" -itsOn: "Yoqilgan" -itsOff: "O'chirilgan" -on: "Yoqish" -off: "O'chirish" -emailRequiredForSignup: "Ro'yxatdan o'tish uchun email talab qilish" -unread: "Oʻqilmagan xabarlar" -filter: "Filter" -controlPanel: "Boshqaruv paneli" -manageAccounts: "Hisobni boshqarish" -classic: "Klassik" -hide: "Yashirish" -searchByGoogle: "Izlash" -indefinitely: "Hech qachon" -file: "Fayllar" -recommended: "Tavsiya qilingan" -check: "Tekshirish" -requireAdminForView: "Ko'rish uchun adminstrator hisobi bilan tizimga kirgan bo'lishingiz kerak." -isSystemAccount: "Ushbu hisob tizim tomonidan avtomatik tarzda yaratilgan va boshqariladi." -typeToConfirm: "Ushbu amalni bajarish uchun {x}ni kiriting" -deleteAccount: "Hisobni o'chirish" -document: "hujjat" -numberOfPageCache: "Sahifa keshlar soni" -logoutConfirm: "Chiqishni xohlaysizmi?" -lastActiveDate: "oxirgi foydalanish sanasi" -statusbar: "Holat paneli" -pleaseSelect: "Iltimos tanlang" -reverse: "Teskari" -colored: "rangli" -refreshInterval: "yangilash oralig'i" -label: "Yorliq" -type: "turi" -speed: "tezlik" -slow: "Sekin" -fast: "Tez" -localOnly: "Faqat mahalliy" -remoteOnly: "faqat masofadan turib" -failedToUpload: "yuklanmadi" -cannotUploadBecauseInappropriate: "Faylni yuklab bo'lmaydi, chunki unda nomaqbul kontent borligi aniqlangan." -cannotUploadBecauseNoFreeSpace: "Yuklab bo'lmadi, chunki diskda bo'sh joy yo'q." -cannotUploadBecauseExceedsFileSizeLimit: "Faylni yuklash mumkin emas, chunki u fayl hajmi chegarasidan oshib ketgan." -beta: "beta" -account: "akkaunt" -show: "Displey" -color: "Rang" -disableFederationConfirm: "Federatsiyani o'chirmoqchimisiz?" -disableFederationOk: "O'chirish" -emailNotSupported: "Bu server E-pochtalar yuborishni qo'llab-quvvatlamaydi" -postToTheChannel: "Kanalga joylash" -cannotBeChangedLater: "Buni keyinchalik o'zgartirishni iloji yo'q" -likeOnly: "Faqat like'lar" -nonSensitiveOnly: "Xavfsiz rejim" -rolesAssignedToMe: "Mening rollarim" -resetPasswordConfirm: "Qayta parol o'rnatmoqchimisiz?" -sensitiveWords: "Ta'sirchan so'zlar" -icon: "Avatar" -replies: "Javob berish" -renotes: "Qayta qayd etish" -flip: "Teskari" -information: "Haqida" -_chat: - invitations: "Taklif qilish" - noHistory: "Tarix yo'q" - members: "A'zolar" - home: "Bosh sahifa" - send: "Yuborish" -_delivery: - stop: "To'xtatilgan" - _type: - none: "Yuborilmoqda" -_achievements: - _types: - _viewInstanceChart: - title: "Tahlilchi" -_role: - priority: "Ustuvorlik" - _priority: - low: "Quyi" - middle: "O'rta" - high: "Yuqori" -_ffVisibility: - public: "Chiqarish" -_ad: - back: "qaytish" - hide: "Boshqa ko'rsatilmasin" -_email: - _follow: - title: "sizga obuna bo'ldi" -_registry: - key: "Kalit" - keys: "Kalit" -_instanceTicker: - none: "Boshqa ko'rsatilmasin" - always: "Doimo ko'rsatilsin" -_theme: - install: "Rang sxemasini o'rnatish" - manage: "Rang sxemalarini boshqarish" - code: "Rang sxemasining kodi" - description: "Tavsif" - installed: "{name} o'rnatildi" - installedThemes: "O'rnatilgan rang sxemalari" - alreadyInstalled: "Ushbu rang sxemasi allaqachon o'rnatilgan" - invalid: "Ushbu rang sxemasining formati yaroqsiz" - make: "Rang sxemasini yasash" - base: "Asos" - addConstant: "O'zgarmas qo'shish" - constant: "O'zgarmas" - color: "Rang" - key: "Kalit" - func: "Funksiyalar" - funcKind: "Funksiya turi" - argument: "Argument" - darken: "Qoraytirish" - lighten: "Yoritish" - inputConstantName: "Ushbu o'zgarmas uchun nom kiriting" - deleteConstantConfirm: "Siz rostdan ham {const} o'zgarmasni o'chirmoqchimisiz?" - keys: - accent: "Urg'u" - bg: "Fon" - fg: "Matn" - focus: "Fokus" - panel: "Panel" - shadow: "Soya" - header: "Sarlavha" - navBg: "Yon panel foni" - navFg: "Yon panel matni" - mention: "Murojat" - renote: "Qayta qayd etish" - divider: "Ajratrmoq" - fgHighlighted: "Belgilangan matn" -_sfx: - note: "Qaydlar" - notification: "Xabarnomalar" -_ago: - minutesAgo: "{n} daqiqa oldin" - hoursAgo: "{n} soat oldin" - daysAgo: "{n} kun oldin" - invalid: "Hech narsa yo'q" -_2fa: - renewTOTPCancel: "Hozir emas" -_permissions: - "read:blocks": "Bloklangan foydalanuvchilar roʻyxatini koʻring" - "write:blocks": "Bloklangan foydalanuvchilar roʻyxatini tahrirlang" -_weekday: - saturday: "Shanba" -_widgets: - profile: "Profil" - instanceInfo: "Instans haqida ma'lumot" - notifications: "Xabarnomalar" - timeline: "Xronologiya" - clock: "Soat" - activity: "Faollik" - photos: "Rasmlar" - digitalClock: "Raqamli soat" - unixClock: "UNIX soat" - federation: "Federatsiya" - button: "Tugma" - jobQueue: "Vazifalar navbati" - _userList: - chooseList: "Ro'yxat tanlash" -_cw: - show: "Ko‘proq ko‘rish" - chars: "{count} ta belgi(lar)" - files: "{count} ta fayl(lar)" -_poll: - noOnlyOneChoice: "Kamida ikkita tanvol kerak" - infinite: "Hech qachon" - at: "...da tugatish" - after: "...dan keyin tugatish" - deadlineTime: "Vaqt" - duration: "Davomiylik" - votesCount: "{n} ovozlar" - totalVotes: "Umuman {n} ovozlar" - vote: "Ovoz berish" - showResult: "Natijalarni ko'rish" - voted: "Ovoz berildi" - closed: "Yakunladi" - remainingDays: "{d} kun {h} soat qoldi" - remainingHours: "{h} soat {m} daqiqa qoldi" - remainingMinutes: "{m} daqiqa {s} sekund qoldi" - remainingSeconds: "{s} sekund qoldi" -_visibility: - public: "Ommaviy" - publicDescription: "Sizning ovozingiz barcha foydalanuvchilarga ko'rinadi" - home: "Bosh sahifa" - followers: "Obunachilar" - specified: "Bevosita" -_profile: - name: "Ism" - username: "Foydalanuvchi nomi" - description: "Biografiya" - metadata: "Qo'shimcha ma'lumot" - metadataLabel: "Yorliq" - metadataContent: "Tarkib" - changeBanner: "Bannerni o'zgartirish" -_exportOrImport: - allNotes: "Barcha qaydlar" - clips: "Klip" - followingList: "Obuna bo‘lish" - muteList: "Ovozni o‘chirish" - blockingList: "Bloklangan foydalanuvchilar" - userLists: "Ro'yxatlar" -_charts: - federation: "Federatsiya" - apRequest: "So'rovlar" - usersTotal: "Foydalanuvchilarning umumiy soni" - activeUsers: "Faol foydalanuvchilar" - notesTotal: "Qaydlarning umumiy soni" - filesTotal: "Fayllarning umumiy soni" -_instanceCharts: - requests: "So'rovlar" - notes: "Qaydlar sonidagi farq" - cacheSize: "Kesh hajmidagi farq" - files: "Fayllar sonidagi farq" -_timelines: - home: "Bosh sahifa" - local: "Mahalliy" - social: "Ijtimoiy" - global: "Global" -_play: - featured: "Mashhur" - title: "Sarlavha" - script: "Skript" - summary: "Tavsif" -_pages: - newPage: "Yangi Sahifa yaratish" - editPage: "Ushbu Sahifani tahrirlash" - pageSetting: "Sahifa sozlamalari" - nameAlreadyExists: "Ko'rsatilgan Sahifa URL'i allaqachon mavjud" - invalidNameTitle: "Ko'rsatilgan Sahifa URL'i yaroqsiz" - editThisPage: "Ushbu Sahifani tahrirlash" - viewPage: "Sizning Sahifalaringizni ko'rish" - my: "Mening Sahifalarim" - featured: "Mashhur" - contents: "Tarkib" - title: "Sarlavha" - url: "Sahifa URL'i" - summary: "Sahifa bayoni" - font: "Shrift" - fontSerif: "Serif" - fontSansSerif: "Sans Serif" - selectType: "Turni tanlang" - contentBlocks: "Tarkib" - blocks: - text: "Matn" - textarea: "Matn maydoni" - section: "Bo'lim" - image: "Rasmlar" - button: "Tugma" - note: "Biriktirilgan qayd" - _note: - id: "Qayd ID" - detailed: "Batafsil ko'rinishi" -_relayStatus: - requesting: "Kutilmoqda" - accepted: "Tasdiqlandi" - rejected: "Rad etildi" -_notification: - fileUploaded: "Fayl muvaffaqiyatli yuklandi" - youGotMention: "{name} sizni eslab o'tdi" - youGotReply: "{name} sizga javob berdi" - youGotQuote: "{name} sizdan iqtibos keltirdi" - youRenoted: "{name} dan qayta qayd qilish" - youWereFollowed: "sizga obuna bo'ldi" - unreadAntennaNote: "Antenna {name}" - _types: - all: "Barchasi" - follow: "Obuna bo‘lish" - mention: "Murojat" - renote: "Qayta qayd etish" - quote: "Iqtibos keltirish" - reaction: "Reaktsiyalar" - receiveFollowRequest: "Qabul qilingan kuzatuv so'rovlari" - login: "Kirish" - _actions: - reply: "Javob berish" - renote: "Qayta qayd qilish" -_deck: - alwaysShowMainColumn: "Har doim asosiy ustunni ko'rsatish" - columnAlign: "Ustunlarni tekislash" - addColumn: "Ustun qo'shish" - configureColumn: "Ustun sozlamalari" - swapLeft: "Chapdagi ustun bilan joyni almashtirish" - swapRight: "O'ngdagi ustun bilan joyni almashtirish" - swapUp: "Yuqoridagi ustun bilan joyni almashtirish" - swapDown: "Quyidagi ustun bilan joyni almashtirish" - profile: "Profil" - newProfile: "Yangi profil" - deleteProfile: "Profilni o‘chirib tashlash" - _columns: - main: "Asosiy" - notifications: "Xabarnomalar" - tl: "Xronologiya" - antenna: "Antennalar" - list: "Ro‘yxat" - channel: "Kanal" - mentions: "Eslatib o'tish" - direct: "Bevosita qaydlar" - roleTimeline: "Rol xronologiyasi" -_webhookSettings: - name: "Ism" - active: "Yoqilgan" - _events: - renote: "Qayta qayd qilinganda" - mention: "Eslanganda" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" -_moderationLogTypes: - suspend: "To'xtatish" - resetPassword: "Parolni tiklash" -_reversi: - total: "Jami" -_remoteLookupErrors: - _noSuchObject: - title: "Topilmadi" -_search: - searchScopeAll: "Barcha" - searchScopeLocal: "Mahalliy" diff --git a/locales/verify.js b/locales/verify.js deleted file mode 100644 index a8e9875d6e..0000000000 --- a/locales/verify.js +++ /dev/null @@ -1,53 +0,0 @@ -import locales from './index.js'; - -let valid = true; - -function writeError(type, lang, tree, data) { - process.stderr.write(JSON.stringify({ type, lang, tree, data })); - process.stderr.write('\n'); - valid = false; -} - -function verify(expected, actual, lang, trace) { - for (let key in expected) { - if (!Object.prototype.hasOwnProperty.call(actual, key)) { - continue; - } - if (typeof expected[key] === 'object') { - if (typeof actual[key] !== 'object') { - writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'object', actual: typeof actual[key] }); - continue; - } - verify(expected[key], actual[key], lang, trace ? `${trace}.${key}` : key); - } else if (typeof expected[key] === 'string') { - switch (typeof actual[key]) { - case 'object': - writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'string', actual: 'object' }); - break; - case 'undefined': - continue; - case 'string': - const expectedParameters = new Set(expected[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1))); - const actualParameters = new Set(actual[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1))); - for (let parameter of expectedParameters) { - if (!actualParameters.has(parameter)) { - writeError('missing_parameter', lang, trace ? `${trace}.${key}` : key, { parameter }); - } - } - } - } - } -} - -const { ['ja-JP']: original, ...verifiees } = locales; - -for (let lang in verifiees) { - if (!Object.prototype.hasOwnProperty.call(locales, lang)) { - continue; - } - verify(original, verifiees[lang], lang); -} - -if (!valid) { - process.exit(1); -} diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 152a9dbd60..898c478bf5 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1,17 +1,13 @@ --- -_lang_: "Tiếng Việt " +_lang_: "Tiếng Việt" headlineMisskey: "Mạng xã hội liên hợp" introMisskey: "Xin chào! Misskey là một nền tảng tiểu blog phi tập trung mã nguồn mở.\nViết \"tút\" để chia sẻ những suy nghĩ của bạn 📡\nBằng \"biểu cảm\", bạn có thể bày tỏ nhanh chóng cảm xúc của bạn với các tút 👍\nHãy khám phá một thế giới mới! 🚀" poweredByMisskeyDescription: "{name} là một trong những chủ máy của Misskey là nền tảng mã nguồn mở" monthAndDay: "{day} tháng {month}" search: "Tìm kiếm" -reset: "cài lại" notifications: "Thông báo" username: "Tên người dùng" password: "Mật khẩu" -initialPasswordForSetup: "Mật khẩu ban đầu để thiết lập" -initialPasswordIsIncorrect: "Mật khẩu ban đầu đã nhập sai" -initialPasswordForSetupDescription: "Nếu bạn tự cài đặt Misskey, hãy sử dụng mật khẩu ban đầu của bạn đã nhập trong tệp cấu hình.\nNếu bạn đang sử dụng dịch vụ nào đó giống như dịch vụ lưu trữ của Misskey, hãy sử dụng mật khẩu ban đầu được cung cấp.\nNếu bạn chưa đặt mật khẩu ban đầu, vui lòng để trống và tiếp tục." forgotPassword: "Quên mật khẩu" fetchingAsApObject: "Đang nạp dữ liệu từ Fediverse..." ok: "Đồng ý" @@ -24,7 +20,6 @@ noNotes: "Chưa có bài viết nào." noNotifications: "Chưa có thông báo" instance: "Máy chủ" settings: "Cài đặt" -notificationSettings: "Cài đặt thông báo" basicSettings: "Thiết lập chung" otherSettings: "Thiết lập khác" openInWindow: "Mở trong cửa sổ mới" @@ -49,23 +44,14 @@ pin: "Ghim" unpin: "Bỏ ghim" copyContent: "Chép nội dung" copyLink: "Chép liên kết" -copyRemoteLink: "Sao chép liên kết từ xa" -copyLinkRenote: "Sao chép liên kết ghi chú" delete: "Xóa" -deleteAndEdit: "Xóa và soạn thảo lại" +deleteAndEdit: "Sửa" deleteAndEditConfirm: "Bạn có chắc muốn sửa tút này? Những biểu cảm, lượt trả lời và đăng lại sẽ bị mất." addToList: "Thêm vào danh sách" -addToAntenna: "Thêm vào Ăngten" sendMessage: "Gửi tin nhắn" copyRSS: "Sao chép RSS" copyUsername: "Chép tên người dùng" -copyUserId: "Sao chép ID người dùng" -copyNoteId: "Sao chép ID ghi chú" -copyFileId: "Sao chép ID tập tin" -copyFolderId: "Sao chép ID thư mục" -copyProfileUrl: "Sao chép URL hồ sơ" searchUser: "Tìm kiếm người dùng" -searchThisUsersNotes: "Tìm kiếm ghi chú của người dùng" reply: "Trả lời" loadMore: "Tải thêm" showMore: "Xem thêm" @@ -102,7 +88,7 @@ pageLoadErrorDescription: "Có thể là do bộ nhớ đệm của trình duy serverIsDead: "Máy chủ không phản hồi. Vui lòng thử lại sau giây lát." youShouldUpgradeClient: "Để xem trang này, hãy làm tươi để cập nhật ứng dụng." enterListName: "Đặt tên cho danh sách" -privacy: "Riêng tư" +privacy: "Bảo mật" makeFollowManuallyApprove: "Yêu cầu theo dõi cần được duyệt" defaultNoteVisibility: "Kiểu tút mặc định" follow: "Theo dõi" @@ -114,14 +100,11 @@ enterEmoji: "Chèn emoji" renote: "Đăng lại" unrenote: "Hủy đăng lại" renoted: "Đã đăng lại." -renotedToX: "Đã cho thuê lại {name}." cantRenote: "Không thể đăng lại tút này." cantReRenote: "Không thể đăng lại một tút đăng lại." quote: "Trích dẫn" inChannelRenote: "Chia sẻ trong kênh này" inChannelQuote: "Trích dẫn trong kênh này" -renoteToChannel: "Đăng lại tới kênh" -renoteToOtherChannel: "Đăng lại tới kênh khác" pinnedNote: "Bài viết đã ghim" pinned: "Ghim" you: "Bạn" @@ -130,23 +113,15 @@ sensitive: "Nhạy cảm" add: "Thêm" reaction: "Biểu cảm" reactions: "Biểu cảm" -emojiPicker: "Bộ chọn biểu tượng cảm xúc" -pinnedEmojisForReactionSettingDescription: "Ghim các biểu tượng cảm xúc sẽ hiển thị khi phản hồi" -pinnedEmojisSettingDescription: "Ghim các biểu tượng cảm xúc sẽ hiển thị trong bảng chọn emoji" -emojiPickerDisplay: "Hiển thị bộ chọn" -overwriteFromPinnedEmojisForReaction: "Ghi đè thiết lập phản hồi" -overwriteFromPinnedEmojis: "Ghi đè thiết lập chung" +reactionSetting: "Chọn những biểu cảm hiển thị" reactionSettingDescription2: "Kéo để sắp xếp, nhấn để xóa, nhấn \"+\" để thêm." rememberNoteVisibility: "Lưu kiểu tút mặc định" attachCancel: "Gỡ tập tin đính kèm" -deleteFile: "Xoá tệp tin" markAsSensitive: "Đánh dấu là nhạy cảm" unmarkAsSensitive: "Bỏ đánh dấu nhạy cảm" enterFileName: "Nhập tên tập tin" mute: "Ẩn" unmute: "Bỏ ẩn" -renoteMute: "Mute Renotes" -renoteUnmute: "Unmute Renotes" block: "Chặn" unblock: "Bỏ chặn" suspend: "Vô hiệu hóa" @@ -156,11 +131,8 @@ unblockConfirm: "Bạn có chắc muốn bỏ chặn người này?" suspendConfirm: "Bạn có chắc muốn vô hiệu hóa người này?" unsuspendConfirm: "Bạn có chắc muốn bỏ vô hiệu hóa người này?" selectList: "Chọn danh sách" -editList: "Chỉnh sửa danh sách" selectChannel: "Lựa chọn kênh" selectAntenna: "Chọn một antenna" -editAntenna: "Chỉnh sửa Ăngten" -createAntenna: "Tạo Ăngten " selectWidget: "Chọn tiện ích" editWidgets: "Sửa tiện ích" editWidgetsExit: "Xong" @@ -173,9 +145,6 @@ addEmoji: "Thêm emoji" settingGuide: "Cài đặt đề xuất" cacheRemoteFiles: "Tập tin cache từ xa" cacheRemoteFilesDescription: "Khi tùy chọn này bị tắt, các tập tin từ xa sẽ được tải trực tiếp từ máy chủ khác. Điều này sẽ giúp giảm dung lượng lưu trữ nhưng lại tăng lưu lượng truy cập, vì hình thu nhỏ sẽ không được tạo." -youCanCleanRemoteFilesCache: "Bạn có thể xoá bộ nhớ đệm bằng cách nhấn vào nút🗑️ở trong phần quản lý tệp." -cacheRemoteSensitiveFiles: "Lưu các tập tin nhạy cảm vào bộ nhớ tạm từ xa" -cacheRemoteSensitiveFilesDescription: "Khi bạn tắt tính năng này, các tệp nhạy cảm sẽ được tải trực tiếp từ máy chủ và không được lưu vào bộ nhớ tạm" flagAsBot: "Đánh dấu đây là tài khoản bot" flagAsBotDescription: "Bật tùy chọn này nếu tài khoản này được kiểm soát bởi một chương trình. Nếu được bật, nó sẽ được đánh dấu để các nhà phát triển khác ngăn chặn chuỗi tương tác vô tận với các bot khác và điều chỉnh hệ thống nội bộ của Misskey để coi tài khoản này như một bot." flagAsCat: "Chế độ Mèeeeeeeeeeo!!" @@ -184,13 +153,8 @@ flagShowTimelineReplies: "Hiện lượt trả lời trong bảng tin" flagShowTimelineRepliesDescription: "Hiện lượt trả lời của người bạn theo dõi trên tút của những người khác." autoAcceptFollowed: "Tự động phê duyệt theo dõi từ những người mà bạn đang theo dõi" addAccount: "Thêm tài khoản" -reloadAccountsList: "Cập nhật danh sách tài khoản" loginFailed: "Đăng nhập không thành công" showOnRemote: "Truy cập trang của người này" -continueOnRemote: "Tiếp tục trên phiên bản từ xa" -chooseServerOnMisskeyHub: "Chọn một máy chủ từ Misskey Hub" -specifyServerHost: "Thiết lập một máy chủ" -inputHostName: "Nhập địa chỉ máy chủ" general: "Tổng quan" wallpaper: "Ảnh bìa" setWallpaper: "Đặt ảnh bìa" @@ -201,7 +165,6 @@ followConfirm: "Bạn theo dõi {name}?" proxyAccount: "Tài khoản proxy" proxyAccountDescription: "Tài khoản proxy là tài khoản hoạt động như một người theo dõi từ xa cho người dùng trong những điều kiện nhất định. Ví dụ: khi người dùng thêm người dùng từ xa vào danh sách, hoạt động của người dùng từ xa sẽ không được chuyển đến phiên bản nếu không có người dùng cục bộ nào theo dõi người dùng đó, vì vậy tài khoản proxy sẽ theo dõi." host: "Host" -selectSelf: "Chọn chính bạn" selectUser: "Chọn người dùng" recipient: "Người nhận" annotation: "Bình luận" @@ -216,8 +179,6 @@ perHour: "Mỗi Giờ" perDay: "Mỗi Ngày" stopActivityDelivery: "Ngưng gửi hoạt động" blockThisInstance: "Chặn máy chủ này" -silenceThisInstance: "Máy chủ im lặng" -mediaSilenceThisInstance: "Tắt nội dung đa phương tiện từ máy chủ này" operations: "Vận hành" software: "Phần mềm" version: "Phiên bản" @@ -237,12 +198,6 @@ clearCachedFiles: "Xóa bộ nhớ đệm" clearCachedFilesConfirm: "Bạn có chắc muốn xóa sạch bộ nhớ đệm?" blockedInstances: "Máy chủ đã chặn" blockedInstancesDescription: "Danh sách những máy chủ bạn muốn chặn. Chúng sẽ không thể giao tiếp với máy chủy này nữa." -silencedInstances: "Máy chủ im lặng" -silencedInstancesDescription: "Đặt máy chủ mà bạn muốn tắt tiếng, phân tách bằng dấu xuống dòng. Tất cả tài khoản trên máy chủ bị tắt tiếng sẽ được coi là \"bị tắt tiếng\" và mọi hành động theo dõi sẽ được coi là yêu cầu. Không có tác dụng với những trường hợp bị chặn." -mediaSilencedInstances: "Các máy chủ đã tắt nội dung đa phương tiện " -mediaSilencedInstancesDescription: "Đặt máy chủ mà bạn muốn tắt nội dung đa phương tiện, phân tách bằng dấu xuống dòng. Tất cả tài khoản trên máy chủ bị tắt tiếng sẽ được coi là \"nhạy cảm\" và biểu tượng cảm xúc tùy chỉnh sẽ không thể được sử dụng. Không có tác dụng với những trường hợp bị chặn." -federationAllowedHosts: "Các máy chủ được phép liên kết" -federationAllowedHostsDescription: "Điền tên các máy chủ mà bạn muốn cho phép liên kết, cách nhau bởi dấu xuống dòng" muteAndBlock: "Ẩn và Chặn" mutedUsers: "Người đã ẩn" blockedUsers: "Người đã chặn" @@ -250,6 +205,7 @@ noUsers: "Chưa có ai" editProfile: "Sửa hồ sơ" noteDeleteConfirm: "Bạn có chắc muốn xóa tút này?" pinLimitExceeded: "Bạn không thể ghim bài viết nữa" +intro: "Đã cài đặt Misskey! Xin hãy tạo tài khoản admin." done: "Xong" processing: "Đang xử lý" preview: "Xem trước" @@ -278,16 +234,16 @@ more: "Thêm nữa!" featured: "Nổi bật" usernameOrUserId: "Tên người dùng hoặc ID" noSuchUser: "Không tìm thấy người dùng" -lookup: "Tra cứu" -announcements: "Thông báo máy chủ" +lookup: "Tìm kiếm" +announcements: "Thông báo" imageUrl: "URL ảnh" remove: "Xóa" removed: "Đã xóa" removeAreYouSure: "Bạn có chắc muốn gỡ \"{x}\"?" deleteAreYouSure: "Bạn có chắc muốn xóa \"{x}\"?" resetAreYouSure: "Bạn có chắc muốn đặt lại?" -areYouSure: "Bạn chắc chứ?" saved: "Đã lưu" +messaging: "Trò chuyện" upload: "Tải lên" keepOriginalUploading: "Giữ hình ảnh gốc" keepOriginalUploadingDescription: "Giữ nguyên như hình ảnh được tải lên ban đầu. Nếu tắt, một phiên bản để hiển thị trên web sẽ được tạo khi tải lên." @@ -297,17 +253,14 @@ uploadFromUrl: "Tải lên bằng một URL" uploadFromUrlDescription: "URL của tập tin bạn muốn tải lên" uploadFromUrlRequested: "Đã yêu cầu tải lên" uploadFromUrlMayTakeTime: "Sẽ mất một khoảng thời gian để tải lên xong." -uploadNFiles: "Tải lên {n} tập tin" explore: "Khám phá" messageRead: "Đã đọc" noMoreHistory: "Không còn gì để đọc" -startChat: "Bắt đầu trò chuyện" +startMessaging: "Bắt đầu trò chuyện" nUsersRead: "đọc bởi {n}" agreeTo: "Tôi đồng ý {0}" -agree: "Đồng ý" agreeBelow: "Đồng ý với nội dung dưới đây" basicNotesBeforeCreateAccount: "Những điều cơ bản cần chú ý " -termsOfService: "Điều khoản và Điều kiện" start: "Bắt đầu" home: "Trang chính" remoteUserCaution: "Vì người dùng này ở máy chủ khác, thông tin hiển thị có thể không đầy đủ." @@ -332,15 +285,12 @@ selectFile: "Chọn tập tin" selectFiles: "Chọn nhiều tập tin" selectFolder: "Chọn thư mục" selectFolders: "Chọn nhiều thư mục" -fileNotSelected: "Chưa chọn tệp nào" renameFile: "Đổi tên tập tin" folderName: "Tên thư mục" createFolder: "Tạo thư mục" renameFolder: "Đổi tên thư mục" deleteFolder: "Xóa thư mục" -folder: "Thư mục" addFile: "Thêm tập tin" -showFile: "Hiển thị tập tin" emptyDrive: "Ổ đĩa của bạn trống trơn" emptyFolder: "Thư mục trống" unableToDelete: "Không thể xóa" @@ -353,7 +303,6 @@ copyUrl: "Sao chép URL" rename: "Đổi tên" avatar: "Ảnh đại diện" banner: "Ảnh bìa" -displayOfSensitiveMedia: "Hiển thị nội dung nhạy cảm (NSFW)" whenServerDisconnected: "Khi mất kết nối tới máy chủ" disconnectedFromServer: "Mất kết nối tới máy chủ" reload: "Tải lại" @@ -383,10 +332,12 @@ enableLocalTimeline: "Bật bảng tin máy chủ" enableGlobalTimeline: "Bật bảng tin liên hợp" disablingTimelinesInfo: "Quản trị viên và Kiểm duyệt viên luôn có quyền truy cập mọi bảng tin, kể cả khi chúng không được bật." registration: "Đăng ký" +enableRegistration: "Cho phép đăng ký mới" invite: "Mời" driveCapacityPerLocalAccount: "Dung lượng ổ đĩa tối đa cho mỗi người dùng" driveCapacityPerRemoteAccount: "Dung lượng ổ đĩa tối đa cho mỗi người dùng từ xa" inMb: "Tính bằng MB" +iconUrl: "URL Icon" bannerUrl: "URL Ảnh bìa" backgroundImageUrl: "URL Ảnh nền" basicInfo: "Thông tin cơ bản" @@ -400,11 +351,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Bật hCaptcha" hcaptchaSiteKey: "Khóa của trang" hcaptchaSecretKey: "Khóa bí mật" -mcaptcha: "mCaptcha" -enableMcaptcha: "Bật mCaptcha" -mcaptchaSiteKey: "Khóa của trang" -mcaptchaSecretKey: "Khóa bí mật" -mcaptchaInstanceUrl: "URL mCaptcha máy chủ" recaptcha: "reCAPTCHA" enableRecaptcha: "Bật reCAPTCHA" recaptchaSiteKey: "Khóa của trang" @@ -420,11 +366,9 @@ name: "Tên" antennaSource: "Nguồn trạm phát sóng" antennaKeywords: "Từ khóa để nghe" antennaExcludeKeywords: "Từ khóa để lọc ra" -antennaExcludeBots: "Loại trừ các tài khoản bot" antennaKeywordsDescription: "Phân cách bằng dấu cách cho điều kiện AND hoặc bằng xuống dòng cho điều kiện OR." notifyAntenna: "Thông báo có tút mới" withFileAntenna: "Chỉ những tút có media" -excludeNotesInSensitiveChannel: "Không hiển thị trong kênh nhạy cảm" enableServiceworker: "Bật ServiceWorker" antennaUsersDescription: "Liệt kê mỗi hàng một tên người dùng" caseSensitive: "Trường hợp nhạy cảm" @@ -449,15 +393,10 @@ aboutMisskey: "Về Misskey" administrator: "Quản trị viên" token: "Token" 2fa: "Xác thực 2 yếu tố" -setupOf2fa: "Thiết lập xác thực 2 yếu tố" totp: "Ứng dụng xác thực" totpDescription: "Nhắn mã OTP bằng ứng dụng xác thực" moderator: "Kiểm duyệt viên" moderation: "Kiểm duyệt" -moderationNote: "Ghi chú kiểm duyệt" -moderationNoteDescription: "Bạn có thể điền vào những ghi chú chỉ được chia sẻ giữa những người kiểm duyệt." -addModerationNote: "Thêm ghi chú kiểm duyệt" -moderationLogs: "Nhật kí quản trị" nUsersMentioned: "Dùng bởi {n} người" securityKeyAndPasskey: "Mã bảo mật・Passkey" securityKey: "Khóa bảo mật" @@ -473,6 +412,7 @@ share: "Chia sẻ" notFound: "Không tìm thấy" notFoundDescription: "Không tìm thấy trang nào tương ứng với URL này." uploadFolder: "Thư mục tải lên mặc định" +cacheClear: "Xóa bộ nhớ đệm" markAsReadAllNotifications: "Đánh dấu tất cả các thông báo là đã đọc" markAsReadAllUnreadNotes: "Đánh dấu tất cả các tút là đã đọc" markAsReadAllTalkMessages: "Đánh dấu tất cả các tin nhắn là đã đọc" @@ -490,10 +430,10 @@ retype: "Nhập lại" noteOf: "Tút của {user}" quoteAttached: "Trích dẫn" quoteQuestion: "Trích dẫn lại?" -attachAsFileQuestion: "Văn bản ở trong bộ nhớ tạm rất dài. Bạn có muốn đăng nó dưới dạng một tệp văn bản không?" +noMessagesYet: "Chưa có tin nhắn" +newMessageExists: "Bạn có tin nhắn mới" onlyOneFileCanBeAttached: "Bạn chỉ có thể đính kèm một tập tin" signinRequired: "Vui lòng đăng nhập" -signinOrContinueOnRemote: "Để tiếp tục, bạn cần chuyển máy chủ hoặc đăng nhập/đăng ký ở máy chủ này." invitations: "Mời" invitationCode: "Mã mời" checking: "Đang kiểm tra..." @@ -515,12 +455,7 @@ uiLanguage: "Ngôn ngữ giao diện" aboutX: "Giới thiệu {x}" emojiStyle: "Kiểu cách Emoji" native: "Bản xứ" -menuStyle: "Kiểu Menu" -style: "Phong cách" -drawer: "Ngăn ứng dụng" -popup: "Cửa sổ bật lên" -showNoteActionsOnlyHover: "Chỉ hiển thị các hành động ghi chú khi di chuột" -showReactionsCount: "Hiển thị số reaction trong bài đăng" +disableDrawer: "Không dùng menu thanh bên" noHistory: "Không có dữ liệu" signinHistory: "Lịch sử đăng nhập" enableAdvancedMfm: "Xem bài MFM chất lượng cao." @@ -533,8 +468,6 @@ createAccount: "Tạo tài khoản" existingAccount: "Tài khoản hiện có" regenerate: "Tạo lại" fontSize: "Cỡ chữ" -mediaListWithOneImageAppearance: "Chiều cao của danh sách nội dung đã phương tiện mà chỉ có một hình ảnh" -limitTo: "Giới hạn tỷ lệ {x}" noFollowRequests: "Bạn không có yêu cầu theo dõi nào" openImageInNewTab: "Mở ảnh trong tab mới" dashboard: "Trang chính" @@ -568,26 +501,19 @@ objectStorageUseSSLDesc: "Tắt nếu bạn không dùng HTTPS để kết nối objectStorageUseProxy: "Kết nối thông qua Proxy" objectStorageUseProxyDesc: "Tắt nếu bạn không dùng Proxy để kết nối API" objectStorageSetPublicRead: "Đặt \"public-read\" khi tải lên" -s3ForcePathStyleDesc: "Nếu s3ForcePathStyle được bật, tên bucket phải được thêm vào địa chỉ URL thay vì chỉ có tên miền. Bạn có thể phải sử dụng thiết lập này nếu bạn sử dụng các dịch vụ như Minio mà bạn tự cung cấp." serverLogs: "Nhật ký máy chủ" deleteAll: "Xóa tất cả" showFixedPostForm: "Hiện khung soạn tút ở phía trên bảng tin" -showFixedPostFormInChannel: "Hiển thị mẫu bài đăng ở phía trên bản tin" -withRepliesByDefaultForNewlyFollowed: "Mặc định hiển thị trả lời từ những người dùng mới theo dõi trong dòng thời gian" newNoteRecived: "Đã nhận tút mới" sounds: "Âm thanh" sound: "Âm thanh" -notificationSoundSettings: "Cài đặt âm thanh thông báo" listen: "Nghe" none: "Không" showInPage: "Hiện trong trang" popout: "Pop-out" volume: "Âm lượng" masterVolume: "Âm thanh chung" -notUseSound: "Tắt tiếng" -useSoundOnlyWhenActive: "Chỉ phát âm thanh khi Misskey đang được hiển thị" details: "Chi tiết" -renoteDetails: "Tìm hiểu thêm về đăng lại " chooseEmoji: "Chọn emoji" unableToProcess: "Không thể hoàn tất hành động" recentUsed: "Sử dụng gần đây" @@ -603,15 +529,10 @@ ascendingOrder: "Tăng dần" descendingOrder: "Giảm dần" scratchpad: "Scratchpad" scratchpadDescription: "Scratchpad cung cấp môi trường cho các thử nghiệm AiScript. Bạn có thể viết, thực thi và kiểm tra kết quả tương tác với Misskey trong đó." -uiInspector: "Trình kiểm tra UI" output: "Nguồn ra" script: "Kịch bản" disablePagesScript: "Tắt AiScript trên Trang" updateRemoteUser: "Cập nhật thông tin người dùng ở máy chủ khác" -unsetUserAvatar: "Gỡ ảnh đại diện" -unsetUserAvatarConfirm: "Bạn có chắc muốn gỡ ảnh đại diện?" -unsetUserBanner: "Gỡ ảnh bìa" -unsetUserBannerConfirm: "Bạn có chắc muốn gỡ ảnh bìa?" deleteAllFiles: "Xóa toàn bộ tập tin" deleteAllFilesConfirm: "Bạn có chắc xóa toàn bộ tập tin?" removeAllFollowing: "Ngưng theo dõi tất cả mọi người" @@ -620,14 +541,9 @@ userSuspended: "Người này đã bị vô hiệu hóa." userSilenced: "Người này đã bị ẩn" yourAccountSuspendedTitle: "Tài khoản bị vô hiệu hóa" yourAccountSuspendedDescription: "Tài khoản này đã bị vô hiệu hóa do vi phạm quy tắc máy chủ hoặc điều tương tự. Liên hệ với quản trị viên nếu bạn muốn biết lý do chi tiết hơn. Vui lòng không tạo tài khoản mới." -tokenRevoked: "Token đã bị từ chối" -tokenRevokedDescription: "Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại." -accountDeleted: "Tài khoản đã bị xóa" -accountDeletedDescription: "Tài khoản này đã bị xóa." menu: "Menu" divider: "Phân chia" addItem: "Thêm mục" -rearrange: "Sắp xếp lại" relays: "Chuyển tiếp" addRelay: "Thêm chuyển tiếp" inboxUrl: "URL Hộp thư đến" @@ -662,7 +578,6 @@ medium: "Vừa" small: "Nhỏ" generateAccessToken: "Tạo mã truy cập" permission: "Cho phép " -adminPermission: "Quyền quản trị viên" enableAll: "Bật toàn bộ" disableAll: "Tắt toàn bộ" tokenRequested: "Cấp quyền truy cập vào tài khoản" @@ -684,19 +599,13 @@ smtpSecure: "Dùng SSL/TLS ngầm định cho các kết nối SMTP" smtpSecureInfo: "Tắt cái này nếu dùng STARTTLS" testEmail: "Kiểm tra vận chuyển email" wordMute: "Ẩn chữ" -wordMuteDescription: "Thu nhỏ các bài đăng chứa các từ hoặc cụm từ nhất định. Các bài đăng này có thể được hiển thị khi click vào." -hardWordMute: "Ẩn cụm từ hoàn toàn" -showMutedWord: "Hiển thị từ đã ẩn" -hardWordMuteDescription: "Ẩn hoàn toàn các bài đăng chứa từ hoặc cụm từ. Khác với mute, bài đăng sẽ bị ẩn hoàn toàn." regexpError: "Lỗi biểu thức" regexpErrorDescription: "Xảy ra lỗi biểu thức ở dòng {line} của {tab} chữ ẩn:" instanceMute: "Những máy chủ ẩn" userSaysSomething: "{name} nói gì đó" -userSaysSomethingAbout: "{name} đã nói gì đó về \"{word}\"" makeActive: "Kích hoạt" display: "Hiển thị" copy: "Sao chép" -copiedToClipboard: "Đã sao chép vào clipboard" metrics: "Số liệu" overview: "Tổng quan" logs: "Nhật ký" @@ -711,21 +620,22 @@ useGlobalSettingDesc: "Nếu được bật, cài đặt thông báo của bạn other: "Khác" regenerateLoginToken: "Tạo lại mã đăng nhập" regenerateLoginTokenDescription: "Tạo lại mã nội bộ có thể dùng để đăng nhập. Thông thường hành động này là không cần thiết. Nếu được tạo lại, tất cả các thiết bị sẽ bị đăng xuất." -theKeywordWhenSearchingForCustomEmoji: "Đây là từ khoá được sử dụng để tìm kiếm emoji" setMultipleBySeparatingWithSpace: "Tách nhiều mục nhập bằng dấu cách." fileIdOrUrl: "ID tập tin hoặc URL" behavior: "Thao tác" sample: "Ví dụ" abuseReports: "Lượt báo cáo" reportAbuse: "Báo cáo" -reportAbuseRenote: "Báo cáo bài đăng lại" reportAbuseOf: "Báo cáo {name}" fillAbuseReportDescription: "Vui lòng điền thông tin chi tiết về báo cáo này. Nếu đó là về một tút cụ thể, hãy kèm theo URL của tút." abuseReported: "Báo cáo đã được gửi. Cảm ơn bạn nhiều." reporter: "Người báo cáo" reporteeOrigin: "Bị báo cáo" reporterOrigin: "Máy chủ người báo cáo" +forwardReport: "Chuyển tiếp báo cáo cho máy chủ từ xa" +forwardReportIsAnonymous: "Thay vì tài khoản của bạn, một tài khoản hệ thống ẩn danh sẽ được hiển thị dưới dạng người báo cáo ở máy chủ từ xa." send: "Gửi" +abuseMarkAsResolved: "Đánh dấu đã xử lý" openInNewTab: "Mở trong tab mới" openInSideView: "Mở trong thanh bên" defaultNavigationBehaviour: "Thao tác điều hướng mặc định" @@ -743,7 +653,6 @@ createNewClip: "Tạo một ghim mới" unclip: "Bỏ ghim" confirmToUnclipAlreadyClippedNote: "Bài đăng này là một phần của \"{name}\" ghim. Bạn có muốn bỏ khỏi ghim?" public: "Công khai" -private: "Riêng tư" i18nInfo: "Misskey đang được các tình nguyện viên dịch sang nhiều thứ tiếng khác nhau. Bạn có thể hỗ trợ tại {link}." manageAccessTokens: "Tạo mã truy cập" accountInfo: "Thông tin tài khoản" @@ -768,7 +677,6 @@ lockedAccountInfo: "Ghi chú của bạn sẽ hiển thị với bất kỳ ai, alwaysMarkSensitive: "Luôn đánh dấu NSFW" loadRawImages: "Tải ảnh gốc thay vì ảnh thu nhỏ" disableShowingAnimatedImages: "Không phát ảnh động" -highlightSensitiveMedia: "Đánh dấu nội dung nhạy cảm" verificationEmailSent: "Một email xác minh đã được gửi. Vui lòng nhấn vào liên kết đính kèm để hoàn tất xác minh." notSet: "Chưa đặt" emailVerified: "Email đã được xác minh" @@ -779,11 +687,10 @@ contact: "Liên hệ" useSystemFont: "Dùng phông chữ mặc định của hệ thống" clips: "Lưu bài viết" experimentalFeatures: "Tính năng thử nghiệm" -experimental: "Thử nghiệm" -thisIsExperimentalFeature: "Tính năng này đang trong quá trình thử nghiệm. Tính năng có thể không hoạt động, hoặc đặc tính kỹ thuật có thể bị thay đổi sau này." developer: "Nhà phát triển" makeExplorable: "Không hiện tôi trong \"Khám phá\"" makeExplorableDescription: "Nếu bạn tắt, tài khoản của bạn sẽ không hiện trong mục \"Khám phá\"." +showGapBetweenNotesInTimeline: "Hiện dải phân cách giữa các tút trên bảng tin" duplicate: "Tạo bản sao" left: "Bên trái" center: "Giữa" @@ -861,11 +768,9 @@ administration: "Quản lý" accounts: "Tài khoản của bạn" switch: "Chuyển đổi" noMaintainerInformationWarning: "Chưa thiết lập thông tin vận hành." -noInquiryUrlWarning: "Địa chỉ hỏi đáp chưa được đặt" noBotProtectionWarning: "Bảo vệ Bot chưa thiết lập." configure: "Thiết lập" postToGallery: "Tạo tút có ảnh" -postToHashtag: "Đăng bài với hashtag này" gallery: "Thư viện ảnh" recentPosts: "Tút gần đây" popularPosts: "Tút được xem nhiều nhất" @@ -899,7 +804,6 @@ translatedFrom: "Dịch từ {x}" accountDeletionInProgress: "Đang xử lý việc xóa tài khoản" usernameInfo: "Bạn có thể sử dụng chữ cái (a ~ z, A ~ Z), chữ số (0 ~ 9) hoặc dấu gạch dưới (_). Tên người dùng không thể thay đổi sau này." aiChanMode: "Chế độ Ai" -devMode: "Chế độ dành cho nhà phát triển" keepCw: "Giữ cảnh báo nội dung" pubSub: "Tài khoản Chính/Phụ" lastCommunication: "Lần giao tiếp cuối" @@ -909,8 +813,6 @@ breakFollow: "Xóa người theo dõi" breakFollowConfirm: "Bạn bỏ theo dõi tài khoản này không?" itsOn: "Đã bật" itsOff: "Đã tắt" -on: "Bật" -off: "Tắt" emailRequiredForSignup: "Yêu cầu địa chỉ email khi đăng ký" unread: "Chưa đọc" filter: "Bộ lọc" @@ -921,12 +823,11 @@ makeReactionsPublicDescription: "Điều này sẽ hiển thị công khai danh classic: "Cổ điển" muteThread: "Không quan tâm nữa" unmuteThread: "Quan tâm tút này" -followingVisibility: "Hiển thị lượt theo dõi" -followersVisibility: "Hiển thị người theo dõi" +ffVisibility: "Hiển thị Theo dõi/Người theo dõi" +ffVisibilityDescription: "Quyết định ai có thể xem những người bạn theo dõi và những người theo dõi bạn." continueThread: "Tiếp tục xem chuỗi tút" deleteAccountConfirm: "Điều này sẽ khiến tài khoản bị xóa vĩnh viễn. Vẫn tiếp tục?" incorrectPassword: "Sai mật khẩu." -incorrectTotp: "Mã OTP không đúng hoặc đã quá hạn" voteConfirm: "Xác nhận bình chọn \"{choice}\"?" hide: "Ẩn" useDrawerReactionPickerForMobile: "Hiện bộ chọn biểu cảm dạng xổ ra trên điện thoại" @@ -951,15 +852,11 @@ oneHour: "1 giờ" oneDay: "1 ngày" oneWeek: "1 tuần" oneMonth: "1 tháng" -threeMonths: "3 tháng" -oneYear: "1 năm" -threeDays: "3 ngày " reflectMayTakeTime: "Có thể mất một thời gian để điều này được áp dụng." failedToFetchAccountInformation: "Không thể lấy thông tin tài khoản" rateLimitExceeded: "Giới hạn quá mức" cropImage: "Cắt hình ảnh" cropImageAsk: "Bạn có muốn cắt ảnh này?" -cropYes: "Cắt" cropNo: "Để nguyên" file: "Tập tin" recentNHours: "{n}h trước" @@ -978,7 +875,6 @@ document: "Tài liệu" numberOfPageCache: "Số lượng trang bộ nhớ đệm" numberOfPageCacheDescription: "Việc tăng con số này sẽ cải thiện sự thuận tiện cho người dùng nhưng gây ra nhiều áp lực hơn cho máy chủ cũng như sử dụng nhiều bộ nhớ hơn." logoutConfirm: "Bạn có chắc muốn đăng xuất?" -logoutWillClearClientData: "Đăng xuất sẽ xoá các thiết lập của bạn khỏi trình duyệt. Để có thể khôi phục thiết lập khi đăng nhập lại, bạn phải bật tự động sao lưu cài đặt." lastActiveDate: "Lần cuối vào" statusbar: "Thanh trạng thái" pleaseSelect: "Chọn một lựa chọn" @@ -996,7 +892,6 @@ remoteOnly: "Chỉ máy chủ từ xa" failedToUpload: "Tải lên thất bại" cannotUploadBecauseInappropriate: "Không thể tải lên tập tin này vì các phần của tập tin đã được phát hiện có khả năng là NSFW." cannotUploadBecauseNoFreeSpace: "Tải lên không thành công do thiếu dung lượng Drive." -cannotUploadBecauseExceedsFileSizeLimit: "Không thể tải lên tập tin vì kích thước quá lớn." beta: "Beta" enableAutoSensitive: "Tự động đánh dấu NSFW" enableAutoSensitiveDescription: "Cho phép tự động phát hiện và đánh dấu media NSFW thông qua học máy, nếu có thể. Ngay cả khi tùy chọn này bị tắt, nó vẫn có thể được bật trên toàn máy chủ." @@ -1009,11 +904,9 @@ pushNotification: "Thông báo đẩy" subscribePushNotification: "Bật thông báo đẩy" unsubscribePushNotification: "Tắt thông báo đẩy" pushNotificationAlreadySubscribed: "Đang bật thông báo đẩy" -pushNotificationNotSupported: "Trình duyệt của bạn không hỗ trợ thông báo đẩy." sendPushNotificationReadMessage: "Xóa thông báo đẩy sau khi đọc thông báo hay tin nhắn" sendPushNotificationReadMessageCaption: "Thông báo như {emptyPushNotificationMessage} sẽ hiển thị trong giây phút. Tiêu tốn pin của máy bạn có thể tăng lên hơn nữa." windowMaximize: "Phóng to" -windowMinimize: "Thu nhỏ tối đa" windowRestore: "Khôi phục" caption: "Mô tả" loggedInAsBot: "Đang đăng nhập bằng tài khoản Bot" @@ -1028,26 +921,14 @@ neverShow: "Không hiển thị nữa" remindMeLater: "Để sau" didYouLikeMisskey: "Bạn có ưa thích Mískey không?" pleaseDonate: "Misskey là phần mềm miễn phí mà {host} đang sử dụng. Xin mong bạn quyên góp cho chúng tôi để chúng tôi có thể tiếp tục phát triển dịch vụ này. Xin cảm ơn!!" -correspondingSourceIsAvailable: "Mã nguồn có thể được xem tại {anchor}" roles: "Vai trò" role: "Vai trò" -noRole: "Bạn chưa được cấp quyền." normalUser: "Người dùng bình thường" undefined: "Chưa xác định" -assign: "Phân công" -unassign: "Hủy phân công" color: "Màu sắc" manageCustomEmojis: "Quản lý CustomEmoji" -manageAvatarDecorations: "Quản lý trang trí ảnh đại diện" -youCannotCreateAnymore: "Bạn đã tới giới hạn tạo." cannotPerformTemporary: "Tạm thời không sử dụng được" cannotPerformTemporaryDescription: "Tạm thời không sử dụng được vì lần số điều kiện quá giới hạn. Thử lại sau mọt lát nữa." -invalidParamError: "Lỗi tham số" -invalidParamErrorDescription: "Có vấn đề với các tham số được request. Thông thường, đây là do bug, nhưng cũng có thể do bạn đã nhập vào quá nhiều ký tự." -permissionDeniedError: "Thao tác bị từ chối" -permissionDeniedErrorDescription: "Tài khoản này không có đủ quyền hạn để thực hiện thao tác này." -preset: "Mẫu thiết lập" -selectFromPresets: "Chọn từ mẫu" achievements: "Thành tích" gotInvalidResponseError: "Không nhận được trả lời chủ máy" gotInvalidResponseErrorDescription: "Chủ máy có lẻ ngừng hoạt động hoặc bảo trí. Thử lại sau một lát nữa. " @@ -1056,239 +937,14 @@ thisPostMayBeAnnoyingHome: "Đăng trên trang chính" thisPostMayBeAnnoyingCancel: "Từ chối" thisPostMayBeAnnoyingIgnore: "Đăng bài để nguyên" collapseRenotes: "Không hiển thị bài viết đã từng xem" -collapseRenotesDescription: "Các bài đăng bị thu gọn mà bạn đã phản hồi hoặc đăng lại trước đây." internalServerError: "Lỗi trong chủ máy" internalServerErrorDescription: "Trong chủ máy lỗi bất ngờ xảy ra" copyErrorInfo: "Sao chép thông tin lỗi" joinThisServer: "Đăng ký trên chủ máy này" exploreOtherServers: "Tìm chủ máy khác" letsLookAtTimeline: "Thử xem Timeline" -disableFederationConfirm: "Bạn có muốn làm điều đó mà không cần liên minh không?" -disableFederationConfirmWarn: "Ngay cả khi bị trì hoãn, bài đăng vẫn sẽ tiếp tục là công khai trừ khi được thiết lập khác. Bạn thường không cần phải làm điều này." -disableFederationOk: "Vô hiệu hoá" -invitationRequiredToRegister: "Phiên bản này chỉ dành cho người được mời. Bạn phải nhập mã mời hợp lệ để đăng ký." -emailNotSupported: "Máy chủ này không hỗ trợ gửi email" -postToTheChannel: "Đăng lên kênh" -cannotBeChangedLater: "Không thể thay đổi sau này." -reactionAcceptance: "Phản ứng chấp nhận" -likeOnly: "Chỉ lượt thích" -likeOnlyForRemote: "Tất cả (chỉ bao gồm lượt thích trên các máy chủ khác)" -nonSensitiveOnly: "Chỉ nội dung không nhạy cảm" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Chỉ nội dung không nhạy cảm (chỉ bao gồm lượt thích từ máy chủ khác)" -rolesAssignedToMe: "Vai trò được giao cho tôi" -resetPasswordConfirm: "Bạn thực sự muốn đặt lại mật khẩu?" -sensitiveWords: "Các từ nhạy cảm" -sensitiveWordsDescription: "Phạm vi của tất cả bài đăng chứa các từ được cấu hình sẽ tự động được đặt về \"Home\". Ban có thể thêm nhiều từ trên mỗi dòng." -sensitiveWordsDescription2: "Sử dụng dấu cách sẽ tạo cấu trúc AND và thêm dấu gạch xuôi để sử dụng như một regex." -prohibitedWords: "Các từ bị cấm" -prohibitedWordsDescription: "Hiển thị lỗi khi đăng một bài đăng chứa các từ sau. Nhiều từ có thể được thêm bằng cách viết một từ trên mỗi dòng." -prohibitedWordsDescription2: "Sử dụng dấu cách sẽ tạo cấu trúc AND và thêm dấu gạch xuôi để sử dụng như một regex." -hiddenTags: "Hashtag ẩn" -hiddenTagsDescription: "Các hashtag này sẽ không được hiển thị trên danh sách Trending. Nhiều tag có thể được thêm bằng cách viết một tag trên mỗi dòng." -notesSearchNotAvailable: "Tìm kiếm bài đăng hiện không khả dụng." -license: "Giấy phép" -unfavoriteConfirm: "Bạn thực sự muốn xoá khỏi mục yêu thích?" -myClips: "Các clip của tôi" -drivecleaner: "Trình dọn đĩa" -retryAllQueuesNow: "Thử lại cho tất cả hàng chờ" -retryAllQueuesConfirmTitle: "Bạn có muốn thử lại?" -retryAllQueuesConfirmText: "Điều này sẽ tạm thời làm tăng mức độ tải của máy chủ." -enableChartsForRemoteUser: "Tạo biểu đồ người dùng từ xa" -video: "Video" -videos: "Các video" -audio: "Âm thanh" -audioFiles: "Âm thanh" -dataSaver: "Tiết kiệm dung lượng" -accountMigration: "Chuyển tài khoản" -accountMoved: "Người dùng này đã chuyển sang một tài khoản mới:" -accountMovedShort: "Tài khoản này đã được chuyển" -operationForbidden: "Thao tác này không thể thực hiện" -forceShowAds: "Luôn hiện quảng cáo" -notificationDisplay: "Thông báo" -leftTop: "Phía trên bên tráí" -rightTop: "Phía trên bên phải" -leftBottom: "Phía dưới bên trái" -rightBottom: "Phía dưới bên phải" -stackAxis: "Hướng chồng" -vertical: "Dọc" horizontal: "Thanh bên" -position: "Vị trí" -serverRules: "Luật của máy chủ" -pleaseConfirmBelowBeforeSignup: "Để đăng ký trên máy chủ này, bạn phải xem xét và đồng ý với những điều sau." -pleaseAgreeAllToContinue: "Bạn phải đồng ý tất cả điều trên để tiếp tục." -continue: "Tiếp tục" -archive: "Lưu trữ" -thisChannelArchived: "Kênh này đã được lưu trữ." -initialAccountSetting: "Thiết lập hồ sơ" youFollowing: "Đang theo dõi" -preventAiLearning: "Từ chối sử dụng công nghệ Máy Học (AI Sáng Tạo)" -options: "Tùy chọn" -specifyUser: "Người dùng chỉ định" -failedToPreviewUrl: "Không thể xem trước" -update: "Cập nhật" -cancelReactionConfirm: "Bạn có muốn hủy phản ứng của mình không?" -changeReactionConfirm: "Bạn có muốn thay đổi phản ứng của mình không?" -later: "Để sau" -goToMisskey: "Tới Misskey" -installed: "Đã tải xuống" -branding: "Thương hiệu" -turnOffToImprovePerformance: "Tắt mục này có thể cải thiện hiệu năng." -createInviteCode: "Tạo lời mời" -createWithOptions: "Tạo cùng tùy chọn" -createCount: "Số lượng mời" -inviteCodeCreated: "Lời mời đã được tạo" -inviteLimitExceeded: "Bạn đã vượt quá số lượng mời mà bạn có thể tạo." -createLimitRemaining: "Giới hạn lượt mời: Còn lại {limit}" -inviteLimitResetCycle: "Giới hạn này sẽ được đặt lại về {limit} lúc {time}." -expirationDate: "Ngày hết hạn" -noExpirationDate: "Vô thời hạn" -inviteCodeUsedAt: "Mã mời đã được sử dụng lúc" -registeredUserUsingInviteCode: "Lời mời đã được sử dụng bởi" -waitingForMailAuth: "Đang chờ xác nhận email" -inviteCodeCreator: "Lời mời đã được tạo bởi" -usedAt: "Sử dụng vào lúc" -unused: "Chưa được sử dụng" -used: "Đã được sử dụng" -expired: "Đã hết hạn" -doYouAgree: "Đồng ý?" -beSureToReadThisAsItIsImportant: "Hãy đọc kỹ vì nó rất quan trọng." -iHaveReadXCarefullyAndAgree: "Tôi đã đọc và đồng ý với \"{x}\"." -dialog: "Hộp thoại" -icon: "Ảnh đại diện" -forYou: "Dành cho bạn" -currentAnnouncements: "Thông báo hiện tại" -pastAnnouncements: "Thông báo trước đó" -youHaveUnreadAnnouncements: "Có thông báo chưa đọc." -useSecurityKey: "Làm theo hướng dẫn trên trình duyệt hoặc thiết bị của bạn để sử dụng khóa bảo mật hoặc mật mã." -replies: "Trả lời" -renotes: "Đăng lại" -loadReplies: "Hiển thị các trả lời" -loadConversation: "Xem cuộc trò chuyện" -pinnedList: "Các mục đã được ghim" -keepScreenOn: "Giữ màn hình luôn bật" -verifiedLink: "Chúng tôi đã xác nhận bạn là chủ sở hữu của đường dẫn này" -authentication: "Xác thực" -authenticationRequiredToContinue: "Vui lòng xác thực để tiếp tục" -dateAndTime: "Ngày và giờ" -edited: "Đã chỉnh sửa" -notificationRecieveConfig: "Cài đặt thông báo" -mutualFollow: "Theo dõi lẫn nhau" -followingOrFollower: "Đang theo dõi hoặc người theo dõi" -externalServices: "Các dịch vụ bên ngoài" -sourceCode: "Mã nguồn" -sourceCodeIsNotYetProvided: "Mã nguồn hiện chưa có sẵn, vui lòng liên hệ với quản trị viên để khắc phục sự cố này." -repositoryUrlDescription: "Nếu bạn có kho lưu trữ mã nguồn có thể truy cập công khai, hãy nhập URL. Nếu bạn đang sử dụng Misskey theo mặc định (không thực hiện bất kỳ thay đổi nào đối với mã nguồn), hãy nhập https://github.com/misskey-dev/misskey." -feedback: "Phản hồi" -feedbackUrl: "URL phản hồi" -impressum: "Thông tin nhà điều hành" -impressumUrl: "URL thông tin nhà điều hành" -privacyPolicy: "Chính sách bảo mật" -privacyPolicyUrl: "URL Chính sách bảo mật" -tosAndPrivacyPolicy: "Điều khoản sử dụng và Chính sách bảo mật" -avatarDecorations: "Trang trí ảnh đại diện" -attach: "Mặc" -detach: "Bỏ" -detachAll: "Bỏ tất cả" -angle: "Góc" -flip: "Lật" -showAvatarDecorations: "Hiển thị trang trí ảnh đại diện" -releaseToRefresh: "Thả để làm mới" -refreshing: "Đang làm mới" -pullDownToRefresh: "Kéo xuống để làm mới" -signupPendingError: "Đã xảy ra sự cố khi xác minh địa chỉ email của bạn. Liên kết có thể đã hết hạn." -cwNotationRequired: "Nếu \"Ẩn nội dung\" được bật thì cần phải có chú thích." -decorate: "Trang trí" -lastNDays: "{n} ngày trước" -userSaysSomethingSensitive: "Bài đăng có chứa các tập tin nhạy cảm từ {name}" -surrender: "Từ chối" -signinWithPasskey: "Đăng nhập bằng mật khẩu của bạn" -passkeyVerificationFailed: "Xác minh mật khẩu không thành công." -messageToFollower: "Tin nhắn cho người theo dõi" -yourNameContainsProhibitedWords: "Tên bạn đang cố gắng đổi có chứa chuỗi ký tự bị cấm." -yourNameContainsProhibitedWordsDescription: "Tên có chứa chuỗi ký tự bị cấm. Nếu bạn muốn sử dụng tên này, hãy liên hệ với quản trị viên máy chủ của bạn." -pleaseSelectAccount: "Chọn tài khoản của bạn" -federationDisabled: "Liên kết bị vô hiệu hóa trên máy chủ này. Bạn không thể tương tác với người dùng trên các máy chủ khác." -reactAreYouSure: "Bạn có muốn phản hồi với \" {emoji} \" không?" -preferences: "Thiết lập môi trường" -accessibility: "Khả năng tiếp cận" -paste: "dán" -postForm: "Mẫu đăng" -information: "Giới thiệu" -chat: "Trò chuyện" -migrateOldSettings: "Di chuyển cài đặt cũ" -migrateOldSettings_description: "Thông thường, quá trình này diễn ra tự động, nhưng nếu vì lý do nào đó mà quá trình di chuyển không thành công, bạn có thể kích hoạt thủ công quy trình di chuyển, quá trình này sẽ ghi đè lên thông tin cấu hình hiện tại của bạn." -_chat: - invitations: "Mời" - noHistory: "Không có dữ liệu" - members: "Thành viên" - home: "Trang chính" - send: "Gửi" -_accountSettings: - requireSigninToViewContents: "Yêu cầu đăng nhập để xem nội dung" - requireSigninToViewContentsDescription1: "Yêu cầu đăng nhập để xem tất cả ghi chú và nội dung khác mà bạn tạo. Điều này được kỳ vọng sẽ có hiệu quả trong việc ngăn chặn thông tin bị thu thập bởi các trình thu thập thông tin." -_delivery: - stop: "Đã vô hiệu hóa" - _type: - none: "Đang đăng" -_announcement: - forExistingUsers: "Chỉ những người dùng đã tồn tại" - forExistingUsersDescription: "Nếu được bật, thông báo này sẽ chỉ hiển thị với những người dùng đã tồn tại vào lúc thông báo được tạo. Nếu tắt đi, những tài khoản mới đăng ký sau khi thông báo được đăng lên cũng sẽ thấy nó." - end: "Lưu trữ thông báo" - tooManyActiveAnnouncementDescription: "Có quá nhiều thông báo sẽ làm trải nghiệm của người dùng tệ đi. Vui lòng lưu trữ những thông báo đã hết hiệu lực." - readConfirmTitle: "Đánh dấu là đã đọc?" - readConfirmText: "Điều này sẽ đánh dấu nội dung của \"{title}\" là đã đọc." -_initialAccountSetting: - accountCreated: "Tài khoản của bạn đã được tạo thành công!" - letsStartAccountSetup: "Để bắt đầu, hãy cùng thiết lập tài khoản nhé." - letsFillYourProfile: "Đầu tiên, hãy thiết lập hồ sơ của bạn." - profileSetting: "Thiết lập hồ sơ" - privacySetting: "Cài đặt quyền riêng tư" - theseSettingsCanEditLater: "Bạn vẫn có thể thay đổi những cài đặt này." - youCanEditMoreSettingsInSettingsPageLater: "Còn rất nhiều những cài đặt khác bạn có thể thay đổi ở trang \"Cài đặt\". Hãy nhớ ghé thăm trong lần sau nhé." - followUsers: "Thử theo dõi một vài người mà bạn có thể thích để xây dựng dòng thời gian của mình." - pushNotificationDescription: "Bật thông báo đẩy sẽ cho phép bạn nhận thông báo từ {name} trực tiếp từ thiết bị của bạn." - initialAccountSettingCompleted: "Thiết lập tài khoản thành công!" - haveFun: "Hãy tận hưởng {name} nhé!" - youCanContinueTutorial: "Bạn có thể tiếp tục xem hướng dẫn về cách sử dụng {name} (Misskey) hoặc bạn có thể thoát khỏi phần thiết lập tại đây và bắt đầu sử dụng ngay lập tức." - startTutorial: "Bắt đầu hướng dẫn" - skipAreYouSure: "Bạn thực sự muốn bỏ qua mục thiết lập tài khoản?" - laterAreYouSure: "Bạn thực sự muốn thiết lập tài khoản vào lúc khác?" -_initialTutorial: - launchTutorial: "Bắt đầu hướng dẫn" - title: "Hướng dẫn" - wellDone: "Làm tốt!" - skipAreYouSure: "Thoát khỏi hướng dẫn?" - _landing: - title: "Chào mừng đến với Hướng dẫn" - description: "Tại đây, bạn có thể tìm hiểu những điều cơ bản về cách sử dụng Misskey và các tính năng của nó." - _note: - title: "Bài Viết là gì?" - description: "Các bài đăng trên Misskey được gọi là 'Bài Viết'. Ghi chú được sắp xếp theo thứ tự thời gian trên dòng thời gian và được cập nhật theo thời gian thực." - _timeline: - home: "Bạn có thể xem ghi chú từ những tài khoản bạn theo dõi." - local: "Bạn có thể xem ghi chú từ tất cả người dùng trên máy chủ này." - social: "Ghi chú từ dòng thời gian Trang chủ và Địa phương sẽ được hiển thị." - global: "Bạn có thể xem ghi chú từ tất cả các máy chủ được kết nối." - _postNote: - _visibility: - home: "Chỉ công khai trên dòng thời gian Trang chủ. Những người truy cập trang cá nhân của bạn, thông qua người theo dõi và thông qua ghi chú lại có thể thấy thông tin đó." -_timelineDescription: - home: "Trong dòng thời gian Trang chính, bạn có thể xem ghi chú từ các tài khoản bạn theo dõi." - local: "Trong dòng thời gian cục bộ, bạn có thể xem ghi chú từ tất cả người dùng trên máy chủ này." - social: "Dòng thời gian Xã hội hiển thị các ghi chú từ cả dòng thời gian Trang chủ và Địa phương." -_serverSettings: - iconUrl: "Biểu tượng URL" - appIconResolutionMustBe: "Độ phân giải tối thiểu là {resolution}." - manifestJsonOverride: "Ghi đè manifest.json" -_accountMigration: - moveFrom: "Chuyển một tài khoản khác vào tài khoản này" - moveFromLabel: "Tài khoản gốc #{n}" - moveTo: "Chuyển tài khoản này vào một tài khoản khác" - moveCannotBeUndone: "Việc chuyển tài khoản không thể huỷ." - moveAccountDescription: "Điều này sẽ chuyển tài khoản này sang một tài khoản khác.\n ・Những người theo dõi sẽ tự động được chuyển sang tài khoản mới\n ・Tài khoản này sẽ tự bỏ theo dõi những người mà bạn đã theo dõi trước đây\n ・Bạn sẽ không thể đăng tút mới, v.v trên tài khoản này\n\nDù việc chuyển người theo dõi được diễn ra tự động, bạn vẫn phải tự chuẩn bị một vài bước để chuyển danh sách những người dùng bạn đang theo dõi. Để làm vậy, vui lòng thực hiện việc xuất dữ liệu những người dùng đã theo dõi mà sau này bạn sẽ dùng để nhập vào tài khoản mới ở menu Cài đặt. Hành động tương tự áp dụng với danh sách những người dùng bị chặn hoặc tắt tiếng.\n\n(Điều này áp dụng cho phiên bản Misskey v13.12.0 và sau này. Các phần mềm ActivityPub khác , ví dụ như Mastodon, sẽ có thể hoạt động khác đi.)" - startMigration: "Chuyển" - movedAndCannotBeUndone: "\nTài khoản này đã được chuyển đi.\nViệc di chuyển tài khoản không thể bị huỷ bỏ." - movedTo: "Tài khoản mới:" _achievements: earnedAt: "Ngày thu nhận" _types: @@ -1327,8 +983,6 @@ _achievements: title: "Hàng tinh đăng bài" description: "Đã đăng bài 50,000 lần rồi" _notes100000: - title: "ALL YOUR NOTE ARE BELONG TO US" - description: "Đăng 100,000 tút" flavor: "Liệu viết bài gì tầm này vậy? " _login3: title: "Sơ cấp I" @@ -1360,15 +1014,6 @@ _achievements: _login400: title: "Khách hàng thường xuyên cấp III" description: "Tổng số ngày đăng nhập đạt 400 ngày" - _login1000: - flavor: "Cảm ơn bạn đã sử dụng Misskey!" - _noteFavorited1: - title: "Nhà thiên văn học" - _myNoteFavorited1: - title: "Đi tìm những ngôi sao" - _profileFilled: - title: "Luôn sẵn sàng" - description: "Thiết lập tài khoản của bạn" _markedAsCat: title: "Tôi là một con mèo" description: "Bật chế độ mèo" @@ -1394,18 +1039,8 @@ _achievements: _followers10: title: "FOLLOW ME!!" description: "Người theo dõi bạn vượt lên 10 người" - _followers50: - title: "Từng chút một" - description: "Đạt được 50 lượt theo dõi" - _followers100: - title: "Người nổi tiếng" - description: "Đạt được 100 lượt theo dõi" - _followers300: - title: "Vui lòng xếp thành hàng nào" - description: "Đạt được 300 lượt theo dõi" _followers500: title: "Trạm phát sóng" - description: "Đạt được 500 lượt theo dõi" _followers1000: title: "Người có tầm ảnh hưởng" description: "Người theo dõi bạn vượt lên 1000 người" @@ -1424,19 +1059,15 @@ _achievements: description: "Tìm thấy được những kho báu cất giấu" _client30min: title: "Giải lao xỉu" - description: "Giữ Misskey mở trong ít nhất 30 phút" - _client60min: - description: "Giữ Misskey mở trong ít nhất 60 phút" _noteDeletedWithin1min: title: "Xem như không có gì đâu nha" _postedAtLateNight: title: "Loài ăn đêm" description: "Đăng bài trong đêm khuya " - flavor: "Đến giờ đi ngủ rồi." _postedAt0min0sec: title: "Tín hiệu báo giờ" description: "Đăng bài vào 0 phút 0 giây" - flavor: "Pin pop pop pop" + flavor: "Piiiiiii ĐÂY LÀ TIẾNG NÓI VIỆT NAM" _selfQuote: title: "Nói đến bản thân" description: "Trích dẫn bài viết của mình" @@ -1463,8 +1094,6 @@ _achievements: _setNameToSyuilo: title: "Ngưỡng mộ với vị thần" description: "Đạt tên là syuilo" - _passedSinceAccountCreated1: - title: "Kỷ niệm một năm" _loggedInOnBirthday: title: "Sinh nhật vủi vẻ" description: "Đăng nhập vào ngày sinh" @@ -1475,7 +1104,6 @@ _achievements: _cookieClicked: flavor: "Bạn nhầm phầm mềm chứ?" _role: - assignTarget: "Phân công" priority: "Ưu tiên" _priority: low: "Thấp" @@ -1486,7 +1114,6 @@ _role: ltlAvailable: "Xem Timeline trong máy chủ này" canPublicNote: "Cho phép đăng bài công khai" canManageCustomEmojis: "Quản lý CustomEmoji" - canManageAvatarDecorations: "Quản lý trang trí ảnh đại diện" driveCapacity: "Dữ liệu Drive" pinMax: "Giới hạn ghim bài viết" antennaMax: "Giới hạn tạo ăng ten" @@ -1551,7 +1178,6 @@ _plugin: install: "Cài đặt tiện ích" installWarn: "Vui lòng không cài đặt những tiện ích đáng ngờ." manage: "Quản lý plugin" - viewSource: "Xem mã nguồn" _preferencesBackups: list: "Tạo sao lưu" saveNew: "Lưu bản sao lưu" @@ -1612,6 +1238,11 @@ _wordMute: muteWords: "Ẩn từ ngữ" muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription2: "Bao quanh các từ khóa bằng dấu gạch chéo để sử dụng cụm từ thông dụng." + softDescription: "Ẩn các tút phù hợp điều kiện đã đặt khỏi bảng tin." + hardDescription: "Ngăn các tút đáp ứng các điều kiện đã đặt xuất hiện trên bảng tin. Lưu ý, những tút này sẽ không được thêm vào bảng tin ngay cả khi các điều kiện được thay đổi." + soft: "Yếu" + hard: "Mạnh" + mutedNotes: "Những tút đã ẩn" _instanceMute: instanceMuteDescription: "Thao tác này sẽ ẩn mọi tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lời từ máy chủ bị ẩn." instanceMuteDescription2: "Tách bằng cách xuống dòng" @@ -1658,6 +1289,7 @@ _theme: header: "Ảnh bìa" navBg: "Nền thanh bên" navFg: "Chữ thanh bên" + navHoverFg: "Chữ thanh bên (Khi chạm)" navActive: "Chữ thanh bên (Khi chọn)" navIndicator: "Chỉ báo thanh bên" link: "Đường dẫn" @@ -1674,18 +1306,30 @@ _theme: infoFg: "Chữ thông tin" infoWarnBg: "Nền cảnh báo" infoWarnFg: "Chữ cảnh báo" + cwBg: "Nền nút nội dung ẩn" + cwFg: "Chữ nút nội dung ẩn" + cwHoverBg: "Nền nút nội dung ẩn (Chạm)" toastBg: "Nền thông báo" toastFg: "Chữ thông báo" buttonBg: "Nền nút" buttonHoverBg: "Nền nút (Chạm)" inputBorder: "Đường viền khung soạn thảo" + listItemHoverBg: "Nền mục liệt kê (Chạm)" + driveFolderBg: "Nền thư mục Ổ đĩa" + wallpaperOverlay: "Lớp phủ hình nền" badge: "Huy hiệu" messageBg: "Nền chat" + accentDarken: "Màu phụ (Tối)" + accentLighten: "Màu phụ (Sáng)" fgHighlighted: "Chữ nổi bật" _sfx: note: "Tút" noteMy: "Tút của tôi" notification: "Thông báo" + chat: "Trò chuyện" + chatBg: "Chat (Nền)" + antenna: "Trạm phát sóng" + channel: "Kênh" _ago: future: "Tương lai" justNow: "Vừa xong" @@ -1704,18 +1348,13 @@ _time: day: "ngày" _2fa: alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước." - registerTOTP: "Đăng ký ứng dụng xác thực" + passwordToTOTP: "Nhắn mật mã" step1: "Trước tiên, hãy cài đặt một ứng dụng xác minh (chẳng hạn như {a} hoặc {b}) trên thiết bị của bạn." step2: "Sau đó, quét mã QR hiển thị trên màn hình này." - step3Title: "Nhập mã xác thực" + step2Url: "Bạn cũng có thể nhập URL này nếu sử dụng một chương trình máy tính:" step3: "Nhập mã token do ứng dụng của bạn cung cấp để hoàn tất thiết lập." step4: "Kể từ bây giờ, những lần đăng nhập trong tương lai sẽ yêu cầu mã token đăng nhập đó." - securityKeyNotSupported: "Trình duyệt của bạn không hỗ trợ khóa bảo mật" - registerTOTPBeforeKey: "Vui lòng thiết lập một ứng dụng xác thực để đăng ký khóa bảo mật hoặc mật khẩu." securityKeyInfo: "Bên cạnh xác minh bằng vân tay hoặc mã PIN, bạn cũng có thể thiết lập xác minh thông qua khóa bảo mật phần cứng hỗ trợ FIDO2 để bảo mật hơn nữa cho tài khoản của mình." - registerSecurityKey: "Tạo khóa bảo mật hoặc mã bảo mật" - securityKeyName: "Nhập tên khóa bảo mật" - tapSecurityKey: "Vui lòng làm theo hướng dẫn của trình duyệt để đăng ký mã bảo mật hoặc mã khóa" removeKey: "Xóa mã bảo mật" removeKeyConfirm: "Xóa bản sao lưu {name}?" renewTOTP: "Cài đặt lại ứng dụng xác thực" @@ -1755,7 +1394,6 @@ _permissions: "write:gallery": "Sửa kho ảnh của tôi" "read:gallery-likes": "Xem danh sách các tút đã thích trong thư viện của tôi" "write:gallery-likes": "Sửa danh sách các tút đã thích trong thư viện của tôi" - "write:chat": "Soạn hoặc xóa tin nhắn" _auth: shareAccessTitle: "Cho phép truy cập app" shareAccess: "Bạn có muốn cho phép \"{name}\" truy cập vào tài khoản này không?" @@ -1809,7 +1447,6 @@ _widgets: _userList: chooseList: "Chọn danh sách" clicker: "clicker" - chat: "Trò chuyện" _cw: hide: "Ẩn" show: "Tải thêm" @@ -1874,7 +1511,6 @@ _profile: _exportOrImport: allNotes: "Toàn bộ tút" favoritedNotes: "Bài viết đã thích" - clips: "Lưu bài viết" followingList: "Đang theo dõi" muteList: "Ẩn" blockingList: "Chặn" @@ -1931,6 +1567,9 @@ _pages: newPage: "Tạo Trang mới" editPage: "Sửa Trang này" readPage: "Xem mã nguồn Trang này" + created: "Trang đã được tạo thành công" + updated: "Trang đã được cập nhật thành công" + deleted: "Trang đã được xóa thành công" pageSetting: "Cài đặt trang" nameAlreadyExists: "URL Trang đã tồn tại" invalidNameTitle: "URL Trang không hợp lệ" @@ -1987,7 +1626,7 @@ _notification: youReceivedFollowRequest: "Bạn vừa có một yêu cầu theo dõi" yourFollowRequestAccepted: "Yêu cầu theo dõi của bạn đã được chấp nhận" pollEnded: "Cuộc bình chọn đã kết thúc" - unreadAntennaNote: "Ăng ten {name}" + unreadAntennaNote: "Ăng ten" emptyPushNotificationMessage: "Đã cập nhật thông báo đẩy" achievementEarned: "Hoàn thành Achievement" _types: @@ -2002,7 +1641,6 @@ _notification: receiveFollowRequest: "Yêu cầu theo dõi" followRequestAccepted: "Yêu cầu theo dõi được chấp nhận" achievementEarned: "Hoàn thành Achievement" - login: "Đăng nhập" app: "Từ app liên kết" _actions: followBack: "đã theo dõi lại bạn" @@ -2035,42 +1673,9 @@ _deck: channel: "Kênh" mentions: "Lượt nhắc" direct: "Nhắn riêng" - chat: "Trò chuyện" _dialog: charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}" charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}" _webhookSettings: - createWebhook: "Tạo Webhook" name: "Tên" - secret: "Mã bí mật" active: "Đã bật" - _events: - reaction: "Khi nhận được sự kiện" - mention: "Khi có người nhắc tới bạn" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" -_moderationLogTypes: - createRole: "Tạo một vai trò" - deleteRole: "Xóa vai trò" - updateRole: "Cập nhật vai trò" - assignRole: "Chỉ định cho vai trò" - unassignRole: "Bỏ gán vai trò" - suspend: "Vô hiệu hóa" - unsuspend: "Rã đông" - resetPassword: "Đặt lại mật khẩu" - createInvitation: "Tạo lời mời" -_reversi: - total: "Tổng cộng" -_customEmojisManager: - _local: - _list: - confirmDeleteEmojisDescription: "Xóa các biểu tượng cảm xúc {count} đã chọn. Bạn có muốn chạy nó không?" -_remoteLookupErrors: - _noSuchObject: - title: "Không tìm thấy" -_search: - searchScopeAll: "Tất cả" - searchScopeLocal: "Máy chủ này" - searchScopeUser: "Người dùng chỉ định" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index f9e910c070..7cf450213b 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -5,21 +5,17 @@ introMisskey: "欢迎!Misskey是一个开源的、去中心化的“微博客 poweredByMisskeyDescription: "{name} 是开源平台 Misskey 的服务器之一。" monthAndDay: "{month}月 {day}日" search: "搜索" -reset: "重置" notifications: "通知" username: "用户名" password: "密码" -initialPasswordForSetup: "初始化密码" -initialPasswordIsIncorrect: "初始化密码不正确" -initialPasswordForSetupDescription: "如果是自己安装的 Misskey,请输入配置文件里设好的密码。\n如果使用的是 Misskey 的托管服务等,请输入服务商提供的密码。\n如果没有设置密码,请留空并继续。" forgotPassword: "忘记密码" fetchingAsApObject: "在联邦宇宙查询中..." ok: "OK" -gotIt: "好" +gotIt: "我明白了" cancel: "取消" noThankYou: "不用,谢谢" enterUsername: "输入用户名" -renotedBy: "{user} 转发了" +renotedBy: "由 {user} 转贴" noNotes: "没有帖文" noNotifications: "无通知" instance: "服务器" @@ -49,23 +45,16 @@ pin: "置顶" unpin: "取消置顶" copyContent: "复制内容" copyLink: "复制链接" -copyRemoteLink: "复制远程链接" -copyLinkRenote: "复制转帖链接" delete: "删除" deleteAndEdit: "删除并编辑" deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。" addToList: "添加至列表" -addToAntenna: "添加到天线" sendMessage: "发送" copyRSS: "复制RSS" copyUsername: "复制用户名" copyUserId: "复制用户 ID" copyNoteId: "复制帖子 ID" -copyFileId: "复制文件ID" -copyFolderId: "复制文件夹ID" -copyProfileUrl: "复制个人资料URL" searchUser: "搜索用户" -searchThisUsersNotes: "搜索用户帖子" reply: "回复" loadMore: "查看更多" showMore: "查看更多" @@ -95,7 +84,7 @@ followsYou: "正在关注你" createList: "创建列表" manageLists: "管理列表" error: "错误" -somethingHappened: "出错了" +somethingHappened: "出现了一些问题!" retry: "重试" pageLoadError: "页面加载失败。" pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。" @@ -109,19 +98,16 @@ follow: "关注" followRequest: "关注申请" followRequests: "关注申请" unfollow: "取消关注" -followRequestPending: "关注请求待批准" +followRequestPending: "关注请求批准中" enterEmoji: "输入表情符号" renote: "转发" unrenote: "取消转发" renoted: "已转发。" -renotedToX: "转帖给 {name}" cantRenote: "该帖无法转发。" cantReRenote: "转发无法被再次转发。" quote: "引用" inChannelRenote: "在频道内转发" inChannelQuote: "在频道内引用" -renoteToChannel: "转帖至频道" -renoteToOtherChannel: "转帖至其它频道" pinnedNote: "已置顶的帖子" pinned: "置顶" you: "您" @@ -130,29 +116,23 @@ sensitive: "敏感内容" add: "添加" reaction: "回应" reactions: "回应" -emojiPicker: "表情符号选择器" -pinnedEmojisForReactionSettingDescription: "可以设置发表回应时置顶显示的表情符号" -pinnedEmojisSettingDescription: "可以设置输入表情符号时置顶显示的表情符号" -emojiPickerDisplay: "选择器显示设置" -overwriteFromPinnedEmojisForReaction: "从「置顶(回应)」设置覆盖" -overwriteFromPinnedEmojis: "从全局设置覆盖" +reactionSetting: "在选择器中显示回应" reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。" rememberNoteVisibility: "保存上次设置的可见性" -attachCancel: "取消添加附件" -deleteFile: "删除文件" +attachCancel: "删除附件" markAsSensitive: "标记为敏感内容" unmarkAsSensitive: "取消标记为敏感内容" enterFileName: "输入文件名" mute: "屏蔽" -unmute: "取消隐藏" -renoteMute: "隐藏转帖" -renoteUnmute: "解除隐藏转帖" -block: "屏蔽" -unblock: "取消屏蔽" +unmute: "解除屏蔽" +renoteMute: "屏蔽转帖" +renoteUnmute: "解除屏蔽转帖" +block: "拉黑" +unblock: "取消拉黑" suspend: "冻结" unsuspend: "解除冻结" -blockConfirm: "确定要屏蔽吗?" -unblockConfirm: "确定要取消屏蔽吗?" +blockConfirm: "确定要拉黑吗?" +unblockConfirm: "确定要解除拉黑吗?" suspendConfirm: "要冻结吗?" unsuspendConfirm: "要解除冻结吗?" selectList: "选择列表" @@ -160,7 +140,6 @@ editList: "编辑列表" selectChannel: "选择频道" selectAntenna: "选择天线" editAntenna: "编辑天线" -createAntenna: "创建天线" selectWidget: "选择小工具" editWidgets: "编辑部件" editWidgetsExit: "完成编辑" @@ -172,14 +151,11 @@ emojiUrl: "emoji 地址" addEmoji: "添加表情符号" settingGuide: "推荐配置" cacheRemoteFiles: "缓存远程文件" -cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。" -youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。" -cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件" -cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。" +cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" flagAsBot: "这是一个机器人账号" flagAsBotDescription: "如果此账户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让 Misskey 的内部系统将此账户识别为机器人。" -flagAsCat: "喵!!!!!!!!!!!!" -flagAsCatDescription: "喵喵喵??" +flagAsCat: "将这个账户设定为一只猫" +flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后,会在您的头像上出现猫耳朵,并将你的帖子中的「na」替换为「nya」,日文同理。" flagShowTimelineReplies: "在时间线上显示帖子的回复" flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。" autoAcceptFollowed: "自动允许来自我关注的用户对我的关注请求" @@ -187,21 +163,16 @@ addAccount: "添加账户" reloadAccountsList: "更新账户列表" loginFailed: "登录失败" showOnRemote: "转到所在服务器显示" -continueOnRemote: "转到所在服务器继续" -chooseServerOnMisskeyHub: "从 Misskey Hub 选择服务器" -specifyServerHost: "直接输入服务器域名" -inputHostName: "请输入域名" general: "常规设置" wallpaper: "壁纸" setWallpaper: "设置壁纸" removeWallpaper: "移除壁纸" searchWith: "搜索:{q}" youHaveNoLists: "列表为空" -followConfirm: "确定要关注 {name} 吗?" +followConfirm: "你确定要关注 {name} 吗?" proxyAccount: "代理账户" -proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。" +proxyAccountDescription: "代理账户是在某些情况下充当用户的远程关注者的账户。 例如,当一个用户列出一个远程用户时,如果没有人跟随该列出的用户,则该活动将不会传递到该服务器,因此将代之以代理账户。" host: "主机名" -selectSelf: "选择自己" selectUser: "选择用户" recipient: "收件人" annotation: "注解" @@ -215,12 +186,9 @@ charts: "图表" perHour: "每小时" perDay: "每天" stopActivityDelivery: "停止发送活动" -blockThisInstance: "屏蔽此服务器" -silenceThisInstance: "静音此服务器" -mediaSilenceThisInstance: "隐藏此服务器的媒体文件" +blockThisInstance: "阻止此服务器向本服务器推流" operations: "操作" software: "软件" -softwareName: "软件名" version: "版本" metadata: "元数据" withNFiles: "{n} 个文件" @@ -232,25 +200,20 @@ disk: "存储" instanceInfo: "服务器信息" statistics: "统计" clearQueue: "清除队列" -clearQueueConfirmTitle: "确定要清除队列吗?" -clearQueueConfirmText: "未送达的帖子将不会被投递。 通常无需执行此操作。" +clearQueueConfirmTitle: "确定清除队列?" +clearQueueConfirmText: "未送达的帖子将不会投递。 通常,您不需要这样做。" clearCachedFiles: "清除缓存" -clearCachedFilesConfirm: "确定要清除所有缓存的远程文件吗?" -blockedInstances: "被屏蔽的服务器" -blockedInstancesDescription: "设定要屏蔽的服务器,以换行分隔。被屏蔽的服务器将无法与本服务器进行交换通讯。子域名也同样会被屏蔽。" -silencedInstances: "被静音的服务器" -silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户都被视为「静音」状态,且关注操作均需要被批准。被阻止的实例不受影响。" -mediaSilencedInstances: "已隐藏媒体文件的服务器" -mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" -federationAllowedHosts: "允许联合的服务器" -federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。" -muteAndBlock: "隐藏和屏蔽" -mutedUsers: "已隐藏用户" -blockedUsers: "已屏蔽的用户" +clearCachedFilesConfirm: "确定要清除缓存文件?" +blockedInstances: "被封锁的服务器" +blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。" +muteAndBlock: "屏蔽/拉黑" +mutedUsers: "已屏蔽用户" +blockedUsers: "已拉黑的用户" noUsers: "无用户" editProfile: "编辑资料" -noteDeleteConfirm: "确定要删除该帖子吗?" +noteDeleteConfirm: "要删除该帖子吗?" pinLimitExceeded: "无法置顶更多了" +intro: "Misskey 的部署结束啦!创建管理员账号吧!" done: "完成" processing: "正在处理" preview: "预览" @@ -259,8 +222,8 @@ defaultValueIs: "默认值: {value}" noCustomEmojis: "没有自定义表情符号" noJobs: "没有任务" federating: "联合中" -blocked: "已屏蔽" -suspended: "停止投递" +blocked: "已拉黑" +suspended: "停止推流" all: "全部" subscribing: "已订阅" publishing: "投递中" @@ -287,8 +250,8 @@ removed: "已删除" removeAreYouSure: "要删掉「{x}」吗?" deleteAreYouSure: "要删掉「{x}」吗?" resetAreYouSure: "恢复默认设置?" -areYouSure: "你确定吗?" saved: "已保存" +messaging: "聊天" upload: "本地上传" keepOriginalUploading: "保留原图" keepOriginalUploadingDescription: "上传图片时保留原始图片。关闭时,浏览器会在上传时生成一张用于web发布的图片。" @@ -298,11 +261,10 @@ uploadFromUrl: "从网址上传" uploadFromUrlDescription: "输入文件的 URL" uploadFromUrlRequested: "请求上传" uploadFromUrlMayTakeTime: "上传可能需要一些时间完成。" -uploadNFiles: "上传 {n} 个文件" explore: "发现" messageRead: "已读" noMoreHistory: "没有更多的历史记录" -startChat: "开始聊天" +startMessaging: "添加聊天" nUsersRead: "{n} 人已读" agreeTo: "勾选则表示已阅读并同意 {0}" agree: "同意" @@ -327,22 +289,18 @@ dark: "深色" lightThemes: "浅色主题" darkThemes: "深色主题" syncDeviceDarkMode: "将深色模式与设备设置同步" -switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」已开启。要关闭同步并手动切换模式吗?" drive: "网盘" fileName: "文件名称" selectFile: "选择文件" selectFiles: "选择文件" selectFolder: "选择文件夹" selectFolders: "选择多个文件夹" -fileNotSelected: "未选择文件" renameFile: "重命名文件" folderName: "文件夹名称" createFolder: "创建文件夹" renameFolder: "重命名文件夹" deleteFolder: "删除文件夹" -folder: "文件夹" addFile: "添加文件" -showFile: "显示文件" emptyDrive: "网盘中无文件" emptyFolder: "此文件夹中无文件" unableToDelete: "无法删除" @@ -355,11 +313,10 @@ copyUrl: "复制链接" rename: "重命名" avatar: "头像" banner: "横幅" -displayOfSensitiveMedia: "显示敏感媒体" whenServerDisconnected: "与服务器连接中断时" disconnectedFromServer: "已和服务器断开连接" reload: "重新加载" -doNothing: "关闭" +doNothing: "关闭弹窗" reloadConfirm: "确定要重新加载吗?" watch: "关注" unwatch: "取消关注" @@ -370,7 +327,7 @@ instanceName: "服务器名称" instanceDescription: "服务器简介" maintainerName: "管理员名称" maintainerEmail: "管理员电子邮箱" -tosUrl: "服务条款地址" +tosUrl: "服务条款 URL" thisYear: "今年" thisMonth: "本月" today: "今天" @@ -383,12 +340,14 @@ connectService: "连接" disconnectService: "断开连接" enableLocalTimeline: "启用本地时间线" enableGlobalTimeline: "启用全局时间线" -disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和监察员也可以继续使用。" +disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和协作者也可以继续使用。" registration: "注册" +enableRegistration: "允许任何人注册" invite: "邀请" driveCapacityPerLocalAccount: "每个用户的网盘容量" driveCapacityPerRemoteAccount: "每个远程用户的网盘容量" inMb: "以兆字节(MegaByte)为单位" +iconUrl: "图标 URL" bannerUrl: "横幅 URL" backgroundImageUrl: "背景图 URL" basicInfo: "基本信息" @@ -402,31 +361,24 @@ hcaptcha: "hCaptcha" enableHcaptcha: "启用 hCaptcha" hcaptchaSiteKey: "网站密钥" hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)" -mcaptcha: "mCaptcha" -enableMcaptcha: "启用 mCaptcha" -mcaptchaSiteKey: "网站密钥" -mcaptchaSecretKey: "mCaptcha 密钥(SecretKey)" -mcaptchaInstanceUrl: "mCaptcha 实例地址" recaptcha: "reCAPTCHA" enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" recaptchaSiteKey: "网站密钥" -recaptchaSecretKey: "mCaptcha 密钥(SecretKey)" +recaptchaSecretKey: "reCAPTCHA 密钥(SecretKey)" turnstile: "Turnstile" enableTurnstile: "启用 Turnstile" turnstileSiteKey: "网站密钥" turnstileSecretKey: "Turnstile 密钥(SecretKey)" -avoidMultiCaptchaConfirm: "使用多个 Captcha 可能会互相干扰,您要禁用其它 Captcha 吗?您可以按“取消”按钮,继续保持启用多种验证方式。" +avoidMultiCaptchaConfirm: "使用多种验证方式可能会造成干扰,您要禁用其他验证方式吗?您可以按“取消”按钮,继续保持启用多种验证方式。" antennas: "天线" manageAntennas: "天线管理" name: "名称" antennaSource: "接收来源" antennaKeywords: "包含关键字" antennaExcludeKeywords: "排除关键字" -antennaExcludeBots: "排除机器人账户" antennaKeywordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" notifyAntenna: "开启通知" withFileAntenna: "仅带有附件的帖子" -excludeNotesInSensitiveChannel: "排除敏感频道内的帖子" enableServiceworker: "启用 ServiceWorker" antennaUsersDescription: "指定用户名,一行一个" caseSensitive: "区分大小写" @@ -451,15 +403,10 @@ aboutMisskey: "关于 Misskey" administrator: "管理员" token: "Token (令牌)" 2fa: "双因素认证" -setupOf2fa: "设置双因素认证" -totp: "验证器" -totpDescription: "使用验证器输入一次性密码" +totp: "身份验证应用" +totpDescription: "使用认证应用输入一次性密码。" moderator: "监察员" moderation: "管理" -moderationNote: "管理笔记" -moderationNoteDescription: "可以用来记录仅在管理员之间共享的笔记。" -addModerationNote: "添加管理笔记" -moderationLogs: "管理日志" nUsersMentioned: "{n} 被提到" securityKeyAndPasskey: "安全密钥或 Passkey" securityKey: "安全密钥" @@ -475,6 +422,7 @@ share: "分享" notFound: "未找到" notFoundDescription: "没有与指定 URL 对应的页面。" uploadFolder: "默认上传文件夹" +cacheClear: "清空缓存" markAsReadAllNotifications: "将所有通知标为已读" markAsReadAllUnreadNotes: "将所有帖子标记为已读" markAsReadAllTalkMessages: "将所有聊天标记为已读" @@ -492,10 +440,10 @@ retype: "重新输入" noteOf: "{user} 的帖子" quoteAttached: "已引用" quoteQuestion: "是否引用此链接内容?" -attachAsFileQuestion: "剪贴板内的文字过长。要转换为文本文件并添加吗?" +noMessagesYet: "现在没有新的聊天" +newMessageExists: "新信息" onlyOneFileCanBeAttached: "只能添加一个附件" signinRequired: "请先登录" -signinOrContinueOnRemote: "若要继续,需要转到您所使用的实例,或者在此服务器上注册或登录。" invitations: "邀请" invitationCode: "邀请码" checking: "正在确认" @@ -515,14 +463,10 @@ or: "或者" language: "语言" uiLanguage: "显示语言" aboutX: "关于 {x}" -emojiStyle: "表情符号的样式" +emojiStyle: "emoji 的样式" native: "原生" -menuStyle: "菜单样式" -style: "样式" -drawer: "抽屉" -popup: "弹窗" +disableDrawer: "不显示抽屉菜单" showNoteActionsOnlyHover: "仅在悬停时显示帖子操作" -showReactionsCount: "显示帖子的回应数" noHistory: "没有历史记录" signinHistory: "登录历史" enableAdvancedMfm: "启用扩展 MFM" @@ -556,41 +500,35 @@ showFeaturedNotesInTimeline: "在时间线上显示热门推荐" objectStorage: "对象存储" useObjectStorage: "使用对象存储" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "用于参考的 URL,如果您正在使用 CDN 或 Proxy,请填入服务商提供的 URL;S3:“https://.s3.amazonaws.com”;GCS:“https://storage.googleapis.com/”" +objectStorageBaseUrlDesc: "这里是用于引用的 URL,如果您正在使用 CDN 或反向代理,请指定其 URL,例如S3:“https://.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/”" objectStorageBucket: "存储桶" objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。" objectStoragePrefix: "前缀" objectStoragePrefixDesc: "文件将存储在此前缀的目录下。" -objectStorageEndpoint: "端点" -objectStorageEndpointDesc: "如果你使用 AWS S3 请留空。否则请根据你使用的服务商的说明来进行设置,指定端点形式为“”或“:”。" +objectStorageEndpoint: "Endpoint" +objectStorageEndpointDesc: "如果你使用 AWS S3 请留空。否则请根据你使用的服务商的说明来进行设置,指定 Endpoint 形式为“”或“:”。" objectStorageRegion: "可用区" objectStorageRegionDesc: "指定一个可用区,例如“xx-east-1”。 如果您的对象存储服务没有可用区概念,请将其留空或填写“us-east-1”。如果引用 AWS 的配置文件或环境变量,则留空。" objectStorageUseSSL: "使用 SSL" objectStorageUseSSLDesc: "如果不使用 https 进行 API 连接,请关闭。" objectStorageUseProxy: "使用代理" -objectStorageUseProxyDesc: "如果不使用代理进行 API 连接,请关闭。" +objectStorageUseProxyDesc: "如果您不使用代理进行 API 连接,请将其关闭。" objectStorageSetPublicRead: "上传时设置为 public-read" s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定为 URL 中路径的一部分,而不是主机名。使用自托管 Minio 等时可能需要启用。" serverLogs: "服务器日志" deleteAll: "全部删除" showFixedPostForm: "在时间线顶部显示发帖框" showFixedPostFormInChannel: "在时间线顶部显示发帖对话框(频道)" -withRepliesByDefaultForNewlyFollowed: "在时间线中默认包含新关注用户的回复" newNoteRecived: "有新的帖子" -newNote: "新帖子" sounds: "提示音" sound: "提示音" -notificationSoundSettings: "设置通知声音" listen: "试听" none: "无" showInPage: "在页面中显示" popout: "弹窗" volume: "音量" masterVolume: "主音量" -notUseSound: "静音" -useSoundOnlyWhenActive: "仅在 Misskey 活跃时输出声音" details: "详情" -renoteDetails: "转帖详情" chooseEmoji: "选择表情符号" unableToProcess: "操作无法完成" recentUsed: "最近使用" @@ -606,20 +544,14 @@ ascendingOrder: "升序" descendingOrder: "降序" scratchpad: "AiScript 控制台" scratchpadDescription: "AiScript 控制台为 AiScript 提供了实验环境。您可以编写代码与 Misskey 交互,运行并查看结果。" -uiInspector: "UI 检查器" -uiInspectorDescription: "查看内存中所有由 UI 组件生成出的实例。UI 组件由 UI:C 系列函数所生成。" output: "输出" script: "脚本" disablePagesScript: "禁用页面脚本" updateRemoteUser: "更新远程用户信息" -unsetUserAvatar: "清除头像" -unsetUserAvatarConfirm: "要清除头像吗?" -unsetUserBanner: "清除横幅" -unsetUserBannerConfirm: "要清除横幅吗?" deleteAllFiles: "删除所有文件" deleteAllFilesConfirm: "要删除所有文件吗?" removeAllFollowing: "取消所有关注" -removeAllFollowingDescription: "取消来自 {host} 的所有关注者。当服务器不再存在时执行。" +removeAllFollowingDescription: "取消 {host} 的所有关注者。当服务器不再存在时执行。" userSuspended: "该用户已被冻结。" userSilenced: "该用户已被禁言。" yourAccountSuspendedTitle: "账户已被冻结" @@ -648,7 +580,7 @@ disablePlayer: "关闭播放器" expandTweet: "展开帖子" themeEditor: "主题编辑器" description: "描述" -describeFile: "添加描述" +describeFile: "添加标题" enterFileDescription: "输入标题" author: "作者" leaveConfirm: "存在未保存的更改。要放弃更改吗?" @@ -666,7 +598,6 @@ medium: "中" small: "小" generateAccessToken: "生成访问令牌" permission: "权限" -adminPermission: "管理员权限" enableAll: "启用全部" disableAll: "禁用全部" tokenRequested: "允许访问账户" @@ -687,20 +618,14 @@ emptyToDisableSmtpAuth: "用户名和密码留空可以禁用 SMTP 验证" smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS" smtpSecureInfo: "使用 STARTTLS 时关闭。" testEmail: "邮件发送测试" -wordMute: "隐藏关键词" -wordMuteDescription: "折叠包含指定关键词的帖子。被折叠的帖子可单击展开。" -hardWordMute: "隐藏硬关键词" -showMutedWord: "显示已隐藏的关键词" -hardWordMuteDescription: "隐藏包含指定关键词的帖子。与隐藏关键词不同,帖子将完全不会显示。" +wordMute: "文字屏蔽" regexpError: "正则表达式错误" -regexpErrorDescription: "{tab} 隐藏文字的第 {line} 行的正则表达式有错误:" -instanceMute: "已隐藏的服务器" +regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:" +instanceMute: "被屏蔽的服务器" userSaysSomething: "{name} 说了什么,但是被屏蔽词过滤了" -userSaysSomethingAbout: "{name} 说了关于「{word}」的什么" makeActive: "启用" display: "显示" copy: "复制" -copiedToClipboard: "已复制到剪贴板" metrics: "指标" overview: "概览" logs: "日志" @@ -715,21 +640,22 @@ useGlobalSettingDesc: "启用时,将使用账户通知设置。关闭时,则 other: "其他" regenerateLoginToken: "重新生成登录令牌" regenerateLoginTokenDescription: "重新生成用于登录的内部令牌。通常您不需要这样做。重新生成后,您将在所有设备上登出。" -theKeywordWhenSearchingForCustomEmoji: "这将是搜索自定义表情符号时的关键词。" setMultipleBySeparatingWithSpace: "您可以使用空格分隔多个项目。" fileIdOrUrl: "文件 ID 或者 URL" behavior: "行为" sample: "示例" abuseReports: "举报" reportAbuse: "举报" -reportAbuseRenote: "举报转帖" reportAbuseOf: "举报 {name}" fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写 URL 地址。" abuseReported: "内容已发送。感谢您提交信息。" reporter: "举报者" reporteeOrigin: "举报来源" reporterOrigin: "举报者来源" +forwardReport: "将该举报信息转发给远程服务器" +forwardReportIsAnonymous: "在远程实例上显示的报告者是匿名的系统账号,而不是您的账号。" send: "发送" +abuseMarkAsResolved: "处理完毕" openInNewTab: "在新标签页中打开" openInSideView: "在侧边栏中打开" defaultNavigationBehaviour: "默认导航" @@ -747,9 +673,8 @@ createNewClip: "新建便签" unclip: "移除便签" confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。您想要将本帖从该便签中移除吗?" public: "公开" -private: "私密" i18nInfo: "Misskey 已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过 {link} 帮助翻译。" -manageAccessTokens: "管理访问令牌" +manageAccessTokens: "管理 Access Tokens" accountInfo: "账户信息" notesCount: "帖子数量" repliesCount: "回复数量" @@ -768,11 +693,10 @@ driveFilesCount: "网盘的文件数" driveUsage: "网盘的空间用量" noCrawle: "要求搜索引擎不索引该用户" noCrawleDescription: "要求搜索引擎不要收录(索引)您的用户页面,帖子,页面等。" -lockedAccountInfo: "即使启用该功能,只要帖子可见范围不是「仅关注者」,任何人都可以看到您的帖子。" +lockedAccountInfo: "即使启用该功能,只要您不将帖子可见范围设置为“仅关注者”,任何人都还是可以看到您的帖子。" alwaysMarkSensitive: "默认将媒体文件标记为敏感内容" loadRawImages: "添加附件图像的缩略图时使用原始图像质量" disableShowingAnimatedImages: "不播放动画" -highlightSensitiveMedia: "高亮显示敏感媒体" verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。" notSet: "未设置" emailVerified: "电子邮件地址已验证" @@ -788,6 +712,7 @@ thisIsExperimentalFeature: "这是一项实验性功能。规范可能会变更 developer: "开发者" makeExplorable: "使账号可见。" makeExplorableDescription: "关闭时,账号不会显示在\"发现\"中。" +showGapBetweenNotesInTimeline: "时间线上的帖子分开显示。" duplicate: "复制" left: "左" center: "中央" @@ -795,7 +720,6 @@ wide: "宽" narrow: "窄" reloadToApplySetting: "页面刷新后设置才会生效。是否现在刷新页面?" needReloadToApply: "重新载入后应用才会生效。" -needToRestartServerToApply: "需要重启服务才能应用更改。" showTitlebar: "显示标题栏" clearCache: "清除缓存" onlineUsersCount: "{n} 人在线" @@ -855,7 +779,7 @@ active: "活动" offline: "离线" notRecommended: "不推荐" botProtection: "Bot防御" -instanceBlocking: "屏蔽/静音的服务器" +instanceBlocking: "被阻拦的服务器" selectAccount: "选择账户" switchAccount: "切换账户" enabled: "已启用" @@ -865,9 +789,8 @@ user: "用户" administration: "管理" accounts: "账户" switch: "切换" -noMaintainerInformationWarning: "尚未设置管理员信息。" -noInquiryUrlWarning: "尚未设置联络地址。" -noBotProtectionWarning: "尚未设置 Bot 防御。" +noMaintainerInformationWarning: "管理人员信息未设置。" +noBotProtectionWarning: "Bot 防御未设置。" configure: "设置" postToGallery: "发送到图库" postToHashtag: "投稿到这个标签" @@ -878,16 +801,16 @@ shareWithNote: "在帖子中分享" ads: "广告" expiration: "截止时间" startingperiod: "开始时间" -memo: "备注" +memo: "便笺" priority: "优先级" high: "高" middle: "中" low: "低" -emailNotConfiguredWarning: "尚未设置电子邮件地址。" +emailNotConfiguredWarning: "电子邮件地址未设置。" ratio: "比率" previewNoteText: "预览文本" customCss: "自定义 CSS" -customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用。" +customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用!" global: "全局" squareAvatars: "显示方形头像图标" sent: "发送" @@ -924,14 +847,13 @@ manageAccounts: "管理账户" makeReactionsPublic: "将回应设置为公开" makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。" classic: "经典" -muteThread: "隐藏帖子列表" -unmuteThread: "取消隐藏帖子列表" -followingVisibility: "关注的人的公开范围" -followersVisibility: "关注者的公开范围" +muteThread: "屏蔽帖子列表" +unmuteThread: "取消屏蔽帖子列表" +ffVisibility: "关注关系的可见范围" +ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围" continueThread: "查看更多帖子" deleteAccountConfirm: "将要删除账户。是否确认?" incorrectPassword: "密码错误" -incorrectTotp: "一次性密码不正确或已过期" voteConfirm: "确定投给 “{choice}” ?" hide: "隐藏" useDrawerReactionPickerForMobile: "在移动设备上使用抽屉显示" @@ -948,7 +870,7 @@ searchByGoogle: "Google" instanceDefaultLightTheme: "服务器默认浅色主题" instanceDefaultDarkTheme: "服务器默认深色主题" instanceDefaultThemeDescription: "以对象格式输入主题代码" -mutePeriod: "隐藏期限" +mutePeriod: "屏蔽期限" period: "截止时间" indefinitely: "永久" tenMinutes: "10 分钟" @@ -956,9 +878,6 @@ oneHour: "1 小时" oneDay: "1 天" oneWeek: "1 周" oneMonth: "1 个月" -threeMonths: "3 个月" -oneYear: "1 年" -threeDays: "3 天" reflectMayTakeTime: "可能需要一些时间才能体现出效果。" failedToFetchAccountInformation: "获取账户信息失败" rateLimitExceeded: "已超过速率限制" @@ -983,7 +902,6 @@ document: "文档" numberOfPageCache: "缓存页数" numberOfPageCacheDescription: "设置较高的值会更方便用户,但设备的负载和内存使用量会增加。" logoutConfirm: "是否确认登出?" -logoutWillClearClientData: "登出时将会从浏览器中删除客户端的设置信息。如果想要在再次登入时恢复设置信息,请在设置里打开自动备份。" lastActiveDate: "最后活跃时间" statusbar: "状态栏" pleaseSelect: "请选择" @@ -1002,7 +920,6 @@ failedToUpload: "上传失败" cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法上传。" cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。" cannotUploadBecauseExceedsFileSizeLimit: "无法上传文件,超过文件大小限制。" -cannotUploadBecauseUnallowedFileType: "因文件类型被禁止而无法上传。" beta: "测试" enableAutoSensitive: "自动 NSFW 识别" enableAutoSensitiveDescription: "使用机器学习在可用时自动使用 NSFW 标记来标记媒体。即使您关闭此功能,根据服务器的不同,它仍然可能会自动设置。" @@ -1017,7 +934,7 @@ unsubscribePushNotification: "停用推送通知消息" pushNotificationAlreadySubscribed: "推送通知消息已启用" pushNotificationNotSupported: "浏览器或服务器不支持推送通知消息" sendPushNotificationReadMessage: "删除已读推送通知消息" -sendPushNotificationReadMessageCaption: "您终端设备的电池消耗可能会增加。" +sendPushNotificationReadMessageCaption: "“{emptyPushNotificationMessage}”的通知消息将会显示。您终端设备的电池消耗可能会增加。" windowMaximize: "最大化" windowMinimize: "最小化" windowRestore: "还原" @@ -1034,7 +951,6 @@ neverShow: "不再显示" remindMeLater: "稍后提醒我" didYouLikeMisskey: "您喜欢 Misskey 吗?" pleaseDonate: "Misskey 是 {host} 所使用的免费软件。为了今后也能够维持 Misskey 的开发,请在有余力的情况下进行捐助!" -correspondingSourceIsAvailable: "对应的源代码可在{anchor}找到" roles: "角色" role: "角色" noRole: "角色不存在" @@ -1044,7 +960,6 @@ assign: "分配" unassign: "取消分配" color: "颜色" manageCustomEmojis: "管理自定义表情符号" -manageAvatarDecorations: "管理头像挂件" youCannotCreateAnymore: "抱歉,您无法再创建更多了。" cannotPerformTemporary: "暂时不可用" cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用,请稍后再试。" @@ -1062,13 +977,12 @@ thisPostMayBeAnnoyingHome: "发到首页" thisPostMayBeAnnoyingCancel: "取消" thisPostMayBeAnnoyingIgnore: "就这样发布" collapseRenotes: "省略显示已经看过的转发内容" -collapseRenotesDescription: "将回应过或转贴过的贴子折叠表示。" internalServerError: "内部服务器错误" internalServerErrorDescription: "内部服务器发生了预期外的错误" copyErrorInfo: "复制错误信息" joinThisServer: "在本服务器上注册" exploreOtherServers: "探索其他服务器" -letsLookAtTimeline: "看看时间线" +letsLookAtTimeline: "时间线" disableFederationConfirm: "确定要禁用联合?" disableFederationConfirmWarn: "禁用联合不会将帖子设为私有。在大多数情况下,不需要禁用联合。" disableFederationOk: "联合禁用" @@ -1084,13 +998,8 @@ nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点 rolesAssignedToMe: "指派给自己的角色" resetPasswordConfirm: "确定重置密码?" sensitiveWords: "敏感词" -sensitiveWordsDescription: "包含这些词的帖子将只在首页可见。可用换行来设定多个词。" +sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。" sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" -prohibitedWords: "禁用词" -prohibitedWordsDescription: "发布包含设定词汇的帖子时将出错。可用换行设定多个关键字。" -prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" -hiddenTags: "隐藏标签" -hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。" notesSearchNotAvailable: "帖子检索不可用" license: "许可信息" unfavoriteConfirm: "确定要取消收藏吗?" @@ -1101,15 +1010,11 @@ retryAllQueuesConfirmTitle: "要再尝试一次吗?" retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加" enableChartsForRemoteUser: "生成远程用户的图表" enableChartsForFederatedInstances: "生成远程服务器的图表" -enableStatsForFederatedInstances: "获取远程服务器的信息" showClipButtonInNoteFooter: "在贴文下方显示便签按钮" -reactionsDisplaySize: "回应显示大小" -limitWidthOfReaction: "限制回应的最大宽度,并将其缩小显示" +largeNoteReactions: "使用大图标来显示回应" noteIdOrUrl: "帖子 ID 或 URL" video: "视频" videos: "视频" -audio: "音频" -audioFiles: "音频" dataSaver: "省流量模式" accountMigration: "账户迁移" accountMoved: "此用户已迁移账户" @@ -1130,15 +1035,13 @@ vertical: "纵向" horizontal: "横向" position: "位置" serverRules: "服务器规则" -pleaseConfirmBelowBeforeSignup: "如果要在此服务器上注册,需要确认并同意以下内容。" +pleaseConfirmBelowBeforeSignup: "在这个服务器上注册账号前,请确认以下信息。" pleaseAgreeAllToContinue: "必须全部勾选「同意」才能够继续。" continue: "继续" preservedUsernames: "保留的用户名" preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。" createNoteFromTheFile: "从文件创建帖子" archive: "归档" -archived: "已归档" -unarchive: "取消归档" channelArchiveConfirmTitle: "要将 {name} 归档吗?" channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。" thisChannelArchived: "该频道已被归档。" @@ -1149,9 +1052,6 @@ preventAiLearning: "拒绝接受生成式 AI 的学习" preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。" options: "选项" specifyUser: "用户指定" -lookupConfirm: "确定查询?" -openTagPageConfirm: "确定打开话题标签页面?" -specifyHost: "指定主机名" failedToPreviewUrl: "无法预览" update: "更新" rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色" @@ -1166,367 +1066,6 @@ installed: "已安装" branding: "品牌" enableServerMachineStats: "公开服务器硬件统计信息" enableIdenticonGeneration: "启用生成用户 Identicon" -turnOffToImprovePerformance: "关闭该选项可以提高性能。" -createInviteCode: "生成邀请码" -createWithOptions: "使用选项来创建" -createCount: "发行数" -inviteCodeCreated: "已生成邀请码" -inviteLimitExceeded: "可供生成的邀请码已达上限。" -createLimitRemaining: "可供生成的邀请码:剩余 {limit} 个" -inviteLimitResetCycle: "可以在 {time} 内生成最多 {limit} 个邀请码。" -expirationDate: "有效日期" -noExpirationDate: "不设置有效日期" -inviteCodeUsedAt: "邀请码被使用的日期和时间" -registeredUserUsingInviteCode: "使用了邀请码的用户" -waitingForMailAuth: "等待验证电子邮件" -inviteCodeCreator: "生成邀请码的用户" -usedAt: "使用时间" -unused: "未使用" -used: "已使用" -expired: "已过期" -doYouAgree: "你同意吗?" -beSureToReadThisAsItIsImportant: "请好好阅读,这真的很重要。" -iHaveReadXCarefullyAndAgree: "我已经仔细阅读并同意了「{x}」的内容。" -dialog: "对话框" -icon: "头像" -forYou: "您的" -currentAnnouncements: "现在的公告" -pastAnnouncements: "过去的公告" -youHaveUnreadAnnouncements: "您有未读的公告" -useSecurityKey: "请根据浏览器或设备的提示,使用安全密钥或通行密钥。" -replies: "回复" -renotes: "转发" -loadReplies: "查看回复" -loadConversation: "查看对话" -pinnedList: "已置顶的列表" -keepScreenOn: "保持设备屏幕开启" -verifiedLink: "已验证的链接" -notifyNotes: "打开发帖通知" -unnotifyNotes: "关闭发帖通知" -authentication: "验证" -authenticationRequiredToContinue: "要继续,请先进行验证" -dateAndTime: "日期和时间" -showRenotes: "显示转帖" -edited: "已编辑" -notificationRecieveConfig: "通知接收设置" -mutualFollow: "互相关注" -followingOrFollower: "关注中或关注者" -fileAttachedOnly: "仅限媒体" -showRepliesToOthersInTimeline: "在时间线中包含给别人的回复" -hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复" -showRepliesToOthersInTimelineAll: "在时间线中显示所有现在关注的人的回复" -hideRepliesToOthersInTimelineAll: "在时间线中隐藏所有现在关注的人的回复" -confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中显示所有现在关注的人的回复吗?" -confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏所有现在关注的人的回复吗?" -externalServices: "外部服务" -sourceCode: "源代码" -sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。" -repositoryUrl: "仓库地址" -repositoryUrlDescription: "若源代码所在的仓库是公开的,请填入对应的 URL。若并未追加或者修改 Misskey 的代码,请填入 https://github.com/misskey-dev/misskey。" -repositoryUrlOrTarballRequired: "若仓库并未公开,则需要提供 tarball 作为替代。详情请看 .config/example.yml。" -feedback: "反馈" -feedbackUrl: "反馈地址" -impressum: "运营商信息" -impressumUrl: "运营商信息地址" -impressumDescription: "德国等国家和地区有义务展示此类信息(Impressum)。" -privacyPolicy: "隐私政策" -privacyPolicyUrl: "隐私政策地址" -tosAndPrivacyPolicy: "服务条款及隐私政策" -avatarDecorations: "头像挂件" -attach: "佩戴" -detach: "卸下" -detachAll: "全部卸下" -angle: "角度" -flip: "翻转" -showAvatarDecorations: "显示头像挂件" -releaseToRefresh: "松开以刷新" -refreshing: "刷新中" -pullDownToRefresh: "下拉以刷新" -useGroupedNotifications: "分组显示通知" -signupPendingError: "确认电子邮件时出现错误。链接可能已过期。" -cwNotationRequired: "在启用「隐藏内容」时必须输入注释" -doReaction: "回应" -code: "代码" -reloadRequiredToApplySettings: "需要重新载入来使设置生效" -remainingN: "剩余:{n}" -overwriteContentConfirm: "将覆盖现有内容。确定吗?" -seasonalScreenEffect: "符合当前季节的画面效果" -decorate: "装饰" -addMfmFunction: "添加装饰" -enableQuickAddMfmFunction: "显示高级 MFM 选择器" -bubbleGame: "泡泡游戏" -sfx: "音效" -soundWillBePlayed: "声音将会播放" -showReplay: "观看回放" -replay: "重播" -replaying: "重播中" -endReplay: "结束回放" -copyReplayData: "复制回放数据" -ranking: "排行榜" -lastNDays: "最近 {n} 天" -backToTitle: "返回标题" -hemisphere: "居住地区" -withSensitive: "显示包含敏感媒体的帖子" -userSaysSomethingSensitive: "含 {name} 敏感文件的帖子" -enableHorizontalSwipe: "滑动切换标签页" -loading: "读取中" -surrender: "取消" -gameRetry: "重试" -notUsePleaseLeaveBlank: "如不使用请留空" -useTotp: "使用一次性代码" -useBackupCode: "使用备用代码" -launchApp: "启动应用" -useNativeUIForVideoAudioPlayer: "使用浏览器的 UI 播放动画及音频" -keepOriginalFilename: "保持原文件名" -keepOriginalFilenameDescription: "若关闭此设置,上传文件时文件名将被替换为随机字符。" -noDescription: "没有描述" -alwaysConfirmFollow: "总是确认关注" -inquiry: "联系我们" -tryAgain: "请再试一次" -confirmWhenRevealingSensitiveMedia: "显示敏感内容前需要确认" -sensitiveMediaRevealConfirm: "这是敏感内容。是否显示?" -createdLists: "已创建的列表" -createdAntennas: "已创建的天线" -fromX: "从 {x}" -genEmbedCode: "生成嵌入代码" -noteOfThisUser: "此用户的帖子" -clipNoteLimitExceeded: "无法再往此便签内添加更多帖子" -performance: "性能" -modified: "有变更" -discard: "取消" -thereAreNChanges: "有 {n} 处更改" -signinWithPasskey: "使用通行密钥登录" -unknownWebAuthnKey: "此通行密钥未注册。" -passkeyVerificationFailed: "验证通行密钥失败。" -passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。" -messageToFollower: "给关注者的消息" -target: "对象" -testCaptchaWarning: "此功能为测试 CAPTCHA 用。请勿在正式环境中使用。" -prohibitedWordsForNameOfUser: "用户名中禁止的词" -prohibitedWordsForNameOfUserDescription: "更改用户名时,如果用户名中包含此列表里的词汇,用户的改名请求将被拒绝。持有管理员权限的用户不受此限制。" -yourNameContainsProhibitedWords: "目标用户名包含违禁词" -yourNameContainsProhibitedWordsDescription: "用户名内含有违禁词。若想使用此用户名,请联系服务器管理员。" -thisContentsAreMarkedAsSigninRequiredByAuthor: "根据发帖者的设定,需要登录才能显示" -lockdown: "锁定" -pleaseSelectAccount: "请选择帐户" -availableRoles: "可用角色" -acknowledgeNotesAndEnable: "理解注意事项后再开启。" -federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。" -federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。" -confirmOnReact: "发送回应前需要确认" -reactAreYouSure: "要用「{emoji}」进行回应吗?" -markAsSensitiveConfirm: "要将此媒体标记为敏感吗?" -unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?" -preferences: "设置" -accessibility: "辅助功能" -preferencesProfile: "设置的配置" -copyPreferenceId: "复制设置 ID" -resetToDefaultValue: "重置为默认值" -overrideByAccount: "用账户覆盖" -untitled: "未命名" -noName: "没有名字" -skip: "跳过" -restore: "恢复" -syncBetweenDevices: "设备间同步" -preferenceSyncConflictTitle: "服务器上已存在设定值" -preferenceSyncConflictText: "服务器上已有此设置的设定值。要覆盖哪个设定值?" -preferenceSyncConflictChoiceMerge: "合并" -preferenceSyncConflictChoiceServer: "服务器上的设定值" -preferenceSyncConflictChoiceDevice: "设备上的设定值" -preferenceSyncConflictChoiceCancel: "取消同步" -paste: "粘贴" -emojiPalette: "表情符号调色板" -postForm: "投稿窗口" -textCount: "字数" -information: "关于" -chat: "聊天" -migrateOldSettings: "迁移旧设置信息" -migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。" -compress: "压缩" -right: "右" -bottom: "下" -top: "上" -embed: "嵌入" -settingsMigrating: "正在迁移设置,请稍候。(之后也可以在设置 → 其它 → 迁移旧设置来手动迁移)" -readonly: "只读" -goToDeck: "返回至 Deck" -federationJobs: "联合作业" -driveAboutTip: "网盘可以显示以前上传的文件。
\n也可以在发布帖子时重复使用文件,或在发布帖子前预先上传文件。
\n删除文件时,其将从至今为止所有用到该文件的地方(如帖子、页面、头像、横幅)消失。
\n也可以新建文件夹来整理文件。" -scrollToClose: "滑动并关闭" -advice: "建议" -realtimeMode: "实时模式" -turnItOn: "开启" -turnItOff: "关闭" -emojiMute: "隐藏表情符号" -emojiUnmute: "解除隐藏表情符号" -muteX: "隐藏{x}" -unmuteX: "解除隐藏{x}" -abort: "中止" -tip: "提示和技巧" -redisplayAllTips: "重新显示所有的提示和技巧" -hideAllTips: "隐藏所有的提示和技巧" -_chat: - noMessagesYet: "还没有消息" - newMessage: "新消息" - individualChat: "私聊" - individualChat_description: "可以与特定用户进行一对一聊天。" - roomChat: "群聊" - roomChat_description: "可以进行多人聊天。\n就算用户未允许私聊,只要接受了邀请,仍可以聊天。" - createRoom: "创建房间" - inviteUserToChat: "邀请用户来开始聊天" - yourRooms: "已创建的房间" - joiningRooms: "已加入的房间" - invitations: "邀请" - noInvitations: "没有邀请" - history: "历史" - noHistory: "没有历史记录" - noRooms: "没有房间" - inviteUser: "邀请用户" - sentInvitations: "已发送的邀请" - join: "加入" - ignore: "忽略" - leave: "退出房间" - members: "成员" - searchMessages: "搜索消息" - home: "首页" - send: "发送" - newline: "换行" - muteThisRoom: "静音此房间" - deleteRoom: "删除房间" - chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。" - chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。" - chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。" - cannotChatWithTheUser: "无法与此用户聊天" - cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。" - youAreNotAMemberOfThisRoomButInvited: "您还未加入此房间,但已收到邀请。如要加入,请接受邀请。" - doYouAcceptInvitation: "要接受邀请吗?" - chatWithThisUser: "聊天" - thisUserAllowsChatOnlyFromFollowers: "此用户仅接受关注者发起的聊天。" - thisUserAllowsChatOnlyFromFollowing: "此用户仅接受关注的人发起的聊天。" - thisUserAllowsChatOnlyFromMutualFollowing: "此用户仅接受互相关注的人发起的聊天。" - thisUserNotAllowedChatAnyone: "此用户不接受任何人发起的聊天。" - chatAllowedUsers: "谁可以发起聊天" - chatAllowedUsers_note: "主动发起聊天时,对方将不受此设置限制。" - _chatAllowedUsers: - everyone: "任何人" - followers: "仅关注者" - following: "仅关注的人" - mutual: "仅相互关注" - none: "没有人" -_emojiPalette: - palettes: "调色板" - enableSyncBetweenDevicesForPalettes: "启用调色板的设备间同步" - paletteForMain: "主调色板" - paletteForReaction: "回应用调色板" -_settings: - driveBanner: "可在此管理和设置网盘、确认使用量及配置上传文件的设置。" - pluginBanner: "使用插件可以扩展客户端的功能。可以在此安装、单独管理插件。" - notificationsBanner: "可在此设置从服务器接收的通知的种类和范围,以及推送通知的设置。" - api: "API" - webhook: "Webhook" - serviceConnection: "连接服务" - serviceConnectionBanner: "可在此管理用于连接外部应用或服务的访问令牌及 Webhook。" - accountData: "账户数据" - accountDataBanner: "可在此导入或导出帐户数据的存档。" - muteAndBlockBanner: "可在此设置隐藏内容,或限制指定用户能进行的操作。" - accessibilityBanner: "可在此设置客户端的显示及动态效果等辅助设置。" - privacyBanner: "可在此设置如内容可见性、可发现性、批准关注请求等账户隐私设置。" - securityBanner: "可在此设置如密码、登入方式、验证器、Passkey 等账户安全性设置。" - preferencesBanner: "可在此设置客户端的整体运作行为。" - appearanceBanner: "可在此设置客户端的外观及显示方式。" - soundsBanner: "可在此设置客户端播放的声音。" - timelineAndNote: "时间线和帖子" - makeEveryTextElementsSelectable: "使所有的文字均可选择" - makeEveryTextElementsSelectable_description: "若开启,在某些情况下可能降低用户体验。" - useStickyIcons: "使图标跟随滚动" - enableHighQualityImagePlaceholders: "显示高质量图像的占位符" - uiAnimations: "UI 动画" - showNavbarSubButtons: "在导航栏中显示副按钮" - ifOn: "启用时" - ifOff: "关闭时" - enableSyncThemesBetweenDevices: "在设备间同步已安装的主题" - enablePullToRefresh: "开启下拉刷新" - enablePullToRefresh_description: "使用鼠标时按下滚轮来拖动" - realtimeMode_description: "与服务器建立连接并实时更新内容。将会增加流量和电池消耗。" - contentsUpdateFrequency: "内容获取频率" - contentsUpdateFrequency_description: "设置越高,内容更新越实时,但性能会降低,并且会消耗更多的流量和电池。" - contentsUpdateFrequency_description2: "当实时模式开启时,无论此设置如何,内容都会实时更新。" - showUrlPreview: "显示 URL 预览" - _chat: - showSenderName: "显示发送者的名字" - sendOnEnter: "回车键发送" -_preferencesProfile: - profileName: "配置名" - profileNameDescription: "请指定用于识别此设备的名称" - profileNameDescription2: "如「PC」、「手机」等" - manageProfiles: "管理配置文件" -_preferencesBackup: - autoBackup: "自动备份" - restoreFromBackup: "从备份恢复" - noBackupsFoundTitle: "没有找到备份" - noBackupsFoundDescription: "没有找到自动备份。若有手动保存备份文件,可将其导入来恢复。" - selectBackupToRestore: "请选择要恢复的备份" - youNeedToNameYourProfileToEnableAutoBackup: "需指定配置名以开启自动备份。" - autoPreferencesBackupIsNotEnabledForThisDevice: "此设备未开启自动备份" - backupFound: "已找到备份" -_accountSettings: - requireSigninToViewContents: "需要登录才能显示内容" - requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。" - requireSigninToViewContentsDescription2: "没有 URL 预览(OGP)、内嵌网页、引用帖子的功能的服务器也将无法显示。" - requireSigninToViewContentsDescription3: "这些限制可能不适用于联合到远程服务器的内容。" - makeNotesFollowersOnlyBefore: "可将过去的帖子设为仅关注者可见" - makeNotesFollowersOnlyBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅关注者可见。关闭后帖子的公开状态将恢复成原本的设定。" - makeNotesHiddenBefore: "将过去的帖子设为私密" - makeNotesHiddenBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅自己可见。关闭后帖子的公开状态将恢复成原本的设定。" - mayNotEffectForFederatedNotes: "与远程服务器联合的帖子在远端可能会没有效果。" - mayNotEffectSomeSituations: "此限制功能非常简单,在与远程服务器联合等情形时可能不适用。" - notesHavePassedSpecifiedPeriod: "超过指定时间的帖子" - notesOlderThanSpecifiedDateAndTime: "指定日期前的帖子" -_abuseUserReport: - forward: "转发" - forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。" - resolve: "解决" - accept: "确认" - reject: "拒绝" - resolveTutorial: "如果认可举报并已解决,选择「确认」将案件以肯定的态度标记为已解决。\n如果不认可举报,选择「拒绝」将案件以否定的态度标记为已解决。" -_delivery: - status: "投递状态" - stop: "停止投递" - resume: "继续投递" - _type: - none: "投递中" - manuallySuspended: "手动停止中" - goneSuspended: "因服务器被删除而停止" - autoSuspendedForNotResponding: "因服务器无应答而停止" - softwareSuspended: "因有停止投递的软件而停止" -_bubbleGame: - howToPlay: "游戏说明" - hold: "抓住" - _score: - score: "得分" - scoreYen: "赚到的钱" - highScore: "最高分" - maxChain: "最高连击数" - yen: "{yen} 日元" - estimatedQty: "约 {qty} 个" - scoreSweets: "相当于 {onigiriQtyWithUnit} 饭团" - _howToPlay: - section1: "对准位置将Emoji投入盒子。" - section2: "相同的Emoji相互接触合成后会得到新的Emoji,以此获得分数。" - section3: "如果Emoji从箱子中溢出游戏将会结束。在防止Emoji溢出的同时,不断合成新的Emoji,来获取更高的分数吧!" -_announcement: - forExistingUsers: "仅限现有用户" - forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" - needConfirmationToRead: "需要确认才能标记为已读" - needConfirmationToReadDescription: "若启用,则会在标记已读时会显示确认对话框。此外,它也会不受批量已读操作的影响。" - end: "结束公告" - tooManyActiveAnnouncementDescription: "若有大量活动公告,可能会造成用户体验下降。请考虑归档已完成的公告。" - readConfirmTitle: "标记为已读?" - readConfirmText: "阅读“{title}”的内容并将其标记为已读。" - shouldNotBeUsedToPresentPermanentInfo: "因可能损坏新用户的 UX 体验,建议将通知用于发布具有时效性的信息,而不是用于长期展示的信息。" - dialogAnnouncementUxWarn: "同时存在 2 个或以上的对话框公告极有可能对用户体验产生负面的影响,建议谨慎使用。" - silence: "不发送通知" - silenceDescription: "开启后,此条公告将不会发送通知,也不强制用户阅读。" _initialAccountSetting: accountCreated: "账户创建完成了!" letsStartAccountSetup: "来进行帐户的初始设置吧。" @@ -1539,123 +1078,20 @@ _initialAccountSetting: pushNotificationDescription: "启用推送通知的话,就可以在设备上接收来自 {name} 的通知了。" initialAccountSettingCompleted: "初始设定已经完成了!" haveFun: "希望 {name} 在这里玩得开心!" - youCanContinueTutorial: "您可以继续了解 {name}(Misskey) 的使用教程,也可以在此停止教程并立即开始使用它。\n" - startTutorial: "开始教学" + ifYouNeedLearnMore: "关于 {name}(Misskey) 的使用方法,详见 {link}。" skipAreYouSure: "要跳过初始设置吗?" laterAreYouSure: "要稍后再进行初始设定吗?" -_initialTutorial: - launchTutorial: "观看教学" - title: "教学" - wellDone: "做得好" - skipAreYouSure: "是否退出教学?" - _landing: - title: "欢迎来到教学" - description: "在这里,您可以查看 Misskey 的基本使用方法和功能。" - _note: - title: "什么是帖子?" - description: "在 Misskey 上发表的文章称为「帖子」。帖子在时间线上按照时间顺序排列,并实时更新。" - reply: "用来回复帖子。可以对回复进行回复,从而形成一串对话。" - renote: "用来将帖子共享到自己的时间线上。也可以加上自己的文字然后引用它。" - reaction: "用来添加回应。详细信息将在下一页进行说明。" - menu: "用来进行例如显示帖子详情、复制链接等各种各样的操作。" - _reaction: - title: "什么是回应?" - description: "您可以在帖子中添加“回应”。 您可以使用反应轻松地表达点“赞”所无法传达的细微差别。" - letsTryReacting: "回应可以通过点击帖子中的「+」按钮来添加。试着给这个示例帖子添加一个回应!" - reactToContinue: "添加一个回应来继续" - reactNotification: "当您的帖子被某人添加了回应时,将实时收到通知。" - reactDone: "通过按下「ー」按钮,可以取消已经添加的回应" - _timeline: - title: "时间线的运作方式" - description1: "Misskey 根据使用方式提供了多个时间线(根据服务器的设定,可能有一些被禁用)。" - home: "可以查看您关注的账户的帖子。" - local: "可以查看这个服务器上所有用户发表的帖子。" - social: "将同时显示首页时间线和本地时间线的内容。" - global: "可以查看所有已联合的服务器上的帖子。" - description2: "可以随时在屏幕顶部在每个时间线之间切换。" - description3: "另外,还有列表时间线和频道时间线。请参阅{link}了解更多详细信息。" - _postNote: - title: "帖子发布设置" - description1: "在 Misskey 发布帖子时,您可以设置各种选项。发帖窗口看起来是这样的。\n" - _visibility: - description: "您可以限制谁可以看到您的帖子。" - public: "向所有用户公开。\n" - home: "仅在首页时间线上发布。 关注者、从个人资料页查看过来的用户、以及通过转帖也能被别的用户看见。" - followers: "仅对关注者可见。 除了您自己之外,没有人可以转贴,并且只有您的关注者可以查看它。\n" - direct: "它将仅向指定用户公开,并且他们也会收到通知。 您可以使用它来代替私信。\n" - doNotSendConfidencialOnDirect1: "发送敏感信息时请注意。\n" - doNotSendConfidencialOnDirect2: "目标服务器的管理员可以看到发布的内容,因此如果您向不受信任的服务器上的用户发送私信,则在处理敏感信息时需要小心。" - localOnly: "不将帖子推送到其它服务器。 无论上述公开范围如何,其它服务器的用户将无法看到附加了此设定的帖子。\n" - _cw: - title: "隐藏内容 (CW)\n" - description: "显示「注解」里的内容而不是正文。点击「查看更多」将会把正文显示出来。" - _exampleNote: - cw: "深夜报复社会" - note: "茨了带巧克力的甜甜圈🍩😋" - useCases: "用于服务器条款所规定的帖子,或对剧透内容和敏感内容进行自主规制。" - _howToMakeAttachmentsSensitive: - title: "如何将附件标注为敏感内容?" - description: "对于服务器方针所要求要求的,又或者不适合直接展示的附件,请添加「敏感」标记。\n" - tryThisFile: "试试看,将附加到此窗口的图像标注为敏感!" - _exampleNote: - note: "拆纳豆包装时失手了…" - method: "要标注附件为敏感内容,请单击该文件以打开菜单,然后单击「标记为敏感内容」。" - sensitiveSucceeded: "附加文件时,请遵循服务器的条款来设置正确敏感设定。\n" - doItToContinue: "将图像标记为敏感后才能够继续" - _done: - title: "恭喜您,已经完成了教程🎉\n" - description: "这里介绍的只是其中一小部分的功能。 要了解更多有关如何使用 Misskey 的更多信息,请访问 {link}。" -_timelineDescription: - home: "首页时间线可以查看您关注的账户的帖子。" - local: "本地时间线可以查看这个服务器上所有用户发表的帖子。" - social: "社交时间线将同时显示首页时间线和本地时间线的内容。" - global: "全局时间线可以查看所有已联合的服务器上的帖子。" _serverRules: description: "在新用户注册前显示服务器的简单规则。推荐显示服务条款的主要内容。" -_serverSettings: - iconUrl: "图标 URL" - appIconDescription: "指定当 {host} 显示为 app 时的图标。" - appIconUsageExample: "如作为书签添加到 PWA 或手机主屏幕时" - appIconStyleRecommendation: "因为有可能会被裁切为圆形或者圆角矩形,建议使用边缘带有留白背景的图标。" - appIconResolutionMustBe: "分辨率必须为 {resolution}。" - manifestJsonOverride: "覆盖 manifest.json" - shortName: "简称" - shortNameDescription: "如果服务器的正式名称很长,可以用简称或者別名来替代。" - fanoutTimelineDescription: "当启用时,可显著提高获取各种时间线时的性能,并减轻数据库的负荷。但是相对的 Redis 的内存使用量将会增加。如果服务器的内存不是很大,又或者运行不稳定的话可以把它关掉。" - fanoutTimelineDbFallback: "回退到数据库" - fanoutTimelineDbFallbackDescription: "当启用时,若时间线未被缓存,则将额外查询数据库。禁用该功能可通过不执行回退处理进一步减少服务器负载,但会限制可检索的时间线范围。" - reactionsBufferingDescription: "开启时可显著提高发送回应时的性能,及减轻数据库负荷。但 Redis 的内存用量会相应增加。" - inquiryUrl: "联络地址" - inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。" - openRegistration: "开放注册" - openRegistrationWarning: "开放注册有风险。建议仅当能够持续监控服务器并在出现问题时能够立即响应时才打开它。" - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。" - deliverSuspendedSoftware: "停止投递的软件" - deliverSuspendedSoftwareDescription: "可因安全漏洞之类的原因,停止向指定的服务器及服务器版本送信。版本信息由服务器提供,不保证可靠性。可使用 semver 范围来指定版本,但指定 >= 2024.3.1 将不包括如 2024.3.1-custom.0 等自定义版本,因此建议像 >= 2024.3.1-0 这样指定 prerelease 版本。" - singleUserMode: "单用户模式" - singleUserMode_description: "若此服务器只有自己使用,开启此模式将最佳化性能。" - signToActivityPubGet: "对 GET 请求签名" - signToActivityPubGet_description: "通常情况下请保持启用。若遇到联合通信方面的问题,将其关闭可能会有所改善,但另一方面有可能会造成无法通信。" - proxyRemoteFiles: "代理远程文件" - proxyRemoteFiles_description: "如果启用,远程服务器的文件将由代理提供。可有效保护图像预览缩略图的生成与用户隐私。" - allowExternalApRedirect: "允许通过 ActivityPub 重定向查询" - allowExternalApRedirect_description: "启用时,将允许其它服务器通过此服务器查询第三方内容,但有可能导致内容欺骗。" - userGeneratedContentsVisibilityForVisitor: "用户生成内容对非用户的可见性" - userGeneratedContentsVisibilityForVisitor_description: "这对于防止诸如难以审核的不适当的远程内容通过您自己的服务器无意中在互联网上公开等问题很有用。" - userGeneratedContentsVisibilityForVisitor_description2: "包含服务器接收到的远程内容在内,无条件将服务器上的所有内容公开在互联网上存在风险。特别是对去中心化的特性不是很了解的访问者有可能将远程服务器上的内容误认为是在此服务器内生成的,需要特别留意。" - _userGeneratedContentsVisibilityForVisitor: - all: "全部公开" - localOnly: "仅公开本地内容,隐藏远程内容" - none: "全部隐藏" _accountMigration: moveFrom: "从别的账号迁移到此账户" moveFromSub: "为另一个账户建立别名" - moveFromLabel: "迁移前的账户 #{n}" + moveFromLabel: "迁移前的账户" moveFromDescription: "如果迁移时需要继承其他账户的关注者,你需要创建一个别名。此操作需要在迁移前完成!\n请像这样输入要迁移的账户:@username@server.example.com\n如果要删除,请将输入字段留空,并保存(不推荐)。" moveTo: "把这个账户迁移到新的账户" moveToLabel: "迁移后的账户" moveCannotBeUndone: "一旦迁移账户,就无法撤销。" - moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的,但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表,并在迁移后立即在目标帐户上执行导入。\n列表、隐藏、屏蔽也是如此,因此您必须手动迁移它。\n(此描述适用于该服务器(Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为可能有所不同。)" + moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的,但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表,并在迁移后立即在目标帐户上执行导入。\n屏蔽列表也是如此,因此您必须手动迁移它。\n(此描述适用于该服务器(Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为可能有所不同。)" moveAccountHowTo: "要进行账户迁移,请现在目标账户中为此账户建立一个别名。\n建立别名后,请像这样输入目标账户:@username@server.example.com" startMigration: "迁移" migrationConfirm: "确定要把此账户迁移到 {account} 吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时,请确认迁移后的账户,已创造别名。" @@ -1717,53 +1153,53 @@ _achievements: flavor: "真的有那么多可以写的东西吗?" _login3: title: "初学者 I" - description: "累计登录 3 天" + description: "连续登录 3 天" flavor: "今天开始我就是 Misskist!" _login7: title: "初学者 II" - description: "累计登录 7 天" + description: "连续登录 7 天" flavor: "您开始习惯了吗?" _login15: title: "初学者 III" - description: "累计登录 15 天" + description: "连续登录 15 天" _login30: title: "Misskist Ⅰ" - description: "累计登录 30 天" + description: "连续登录 30 天" _login60: title: "Misskist Ⅱ" - description: "累计登录 60 天" + description: "连续登录 60 天" _login100: title: "Misskist Ⅲ" - description: "累计登入 100 天" + description: "总登入 100 天" flavor: "那个用户,是 Misskist 喔" _login200: title: "定期联系Ⅰ" - description: "累计登录 200 天" + description: "总登录天数 200 天" _login300: title: "定期联系Ⅱ" - description: "累计登录 300 天" + description: "总登录天数 300 天" _login400: title: "定期联系Ⅲ" - description: "累计登录 400 天" + description: "总登录天数 400 天" _login500: title: "老熟人Ⅰ" - description: "累计登录 500 天" + description: "总登录天数 500 天" flavor: "诸君,我喜欢贴文" _login600: title: "老熟人Ⅱ" - description: "累计登录 600 天" + description: "总登录天数 600 天" _login700: title: "老熟人Ⅲ" - description: "累计登录 700 天" + description: "总登录天数 700 天" _login800: title: "帖子大师 Ⅰ" - description: "累计登录 800 天" + description: "总登录天数 800 天" _login900: title: "帖子大师 Ⅱ" - description: "累计登录 900 天" + description: "总登录天数 900 天" _login1000: title: "帖子大师 Ⅲ" - description: "累计登录 1000 天" + description: "总登录天数 1000 天" flavor: "感谢您使用 Misskey!" _noteClipped1: title: "忍不住要收藏到便签" @@ -1846,7 +1282,7 @@ _achievements: _postedAt0min0sec: title: "报时" description: "在 0 点发布一篇帖子" - flavor: "嘟 · 嘟 · 嘟 · 哔——" + flavor: "嘣 嘣 嘣 Biu——!" _selfQuote: title: "自我引用" description: "引用了自己的帖子" @@ -1873,9 +1309,9 @@ _achievements: description: "点了这里" _justPlainLucky: title: "超高校级的幸运" - description: "每 10 秒有 0.005% 的概率自动获得" + description: "每 10 秒有 0.01 的概率自动获得" _setNameToSyuilo: - title: "上帝情结" + title: "像神一样呐" description: "将名称设定为 syuilo" _passedSinceAccountCreated1: title: "一周年" @@ -1894,26 +1330,13 @@ _achievements: description: "在元旦登入" flavor: "今年也请对本服务器多多指教!" _cookieClicked: - title: "饼干点点乐" - description: "点击了饼干" - flavor: "穿越了?" + title: "点击饼干小游戏" + description: "点击了可疑的饼干" + flavor: "是不是软件有问题?" _brainDiver: title: "Brain Diver" description: "发布了包含 Brain Diver 链接的帖子" flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "过度测试" - description: "短时间内连续测试通知" - _tutorialCompleted: - title: "Misskey 初学者课程 结业证书" - description: "完成了教学" - _bubbleGameExplodingHead: - title: "🤯" - description: "你合成出了游戏里最大的Emoji" - _bubbleGameDoubleExplodingHead: - title: "两个🤯" - description: "你合成出了2个游戏里最大的Emoji" - flavor: "大约能 装满 这些便当盒 🤯 🤯 (比划)" _role: new: "创建角色" edit: "编辑角色" @@ -1924,9 +1347,7 @@ _role: assignTarget: "授权对象" descriptionOfAssignTarget: "手动指手动选择谁被包括在这个角色中。\n符合条件指设置条件以自动包括符合条件的用户。" manual: "手动" - manualRoles: "手动角色" conditional: "符合条件" - conditionalRoles: "条件角色" condition: "条件" isConditionalRole: "这是一个条件控制的角色。" isPublic: "角色公开" @@ -1943,10 +1364,8 @@ _role: descriptionOfIsExplorable: "打开后将公开角色时间线。如果角色不是公开的,就无法公开时间线。" displayOrder: "显示顺序" descriptionOfDisplayOrder: "数字越大,显示位置越靠前。" - preserveAssignmentOnMoveAccount: "将分配状态继承到目标账户" - preserveAssignmentOnMoveAccount_description: "启用后,当迁移具有该角色的账户时,目标账户也会继承该角色。" - canEditMembersByModerator: "允许监察员编辑成员" - descriptionOfCanEditMembersByModerator: "如果选中,监察员和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" + canEditMembersByModerator: "允许监察者编辑成员" + descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" priority: "优先级" _priority: low: "低" @@ -1956,20 +1375,13 @@ _role: gtlAvailable: "查看全局时间线" ltlAvailable: "查看本地时间线" canPublicNote: "允许公开发帖" - mentionMax: "帖子内最多提及数" canInvite: "发放服务器邀请码" - inviteLimit: "可生成邀请码的数量" - inviteLimitCycle: "邀请码的发行间隔" - inviteExpirationTime: "邀请码的有效日期" canManageCustomEmojis: "管理自定义表情符号" - canManageAvatarDecorations: "管理头像挂件" driveCapacity: "网盘容量" - maxFileSize: "可上传的最大文件大小" alwaysMarkNsfw: "总是将文件标记为 NSFW" - canUpdateBioMedia: "可以更新头像和横幅" pinMax: "帖子置顶数量限制" antennaMax: "可创建的最大天线数量" - wordMuteMax: "隐藏词的字数限制" + wordMuteMax: "屏蔽词的字数限制" webhookMax: "Webhook 创建数量限制" clipMax: "便签创建数量限制" noteEachClipsMax: "单个便签内的贴文数量限制" @@ -1979,26 +1391,9 @@ _role: descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" canHideAds: "可以隐藏广告" canSearchNotes: "是否可以搜索帖子" - canUseTranslator: "使用翻译功能" - avatarDecorationLimit: "可添加头像挂件的最大个数" - canImportAntennas: "允许导入天线" - canImportBlocking: "允许导入屏蔽列表" - canImportFollowing: "允许导入关注列表" - canImportMuting: "允许导入隐藏列表" - canImportUserLists: "允许导入用户列表" - chatAvailability: "允许聊天" - uploadableFileTypes: "可上传的文件类型" - uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*)" - uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。" _condition: - roleAssignedTo: "已分配给手动角色" isLocal: "是本地用户" isRemote: "是远程用户" - isCat: "猫猫用户" - isBot: "机器人用户" - isSuspended: "停用的用户" - isLocked: "锁推用户" - isExplorable: "启用“使账号可见”的用户" createdLessThan: "账户创建时间少于" createdMoreThan: "账户创建时间超过" followersLessThanOrEq: "关注者不多于" @@ -2024,7 +1419,6 @@ _emailUnavailable: disposable: "不是永久可用的地址" mx: "邮件服务器不正确" smtp: "邮件服务器没有响应" - banned: "无法使用此邮件地址注册" _ffVisibility: public: "公开" followers: "只有关注你的用户能看到" @@ -2044,11 +1438,6 @@ _ad: back: "返回" reduceFrequencyOfThisAd: "减少此广告的频率" hide: "不显示" - timezoneinfo: "星期几是由服务器的时区所指定的。" - adsSettings: "广告设置" - notesPerOneAd: "在实时更新时间线中插入广告的间隔(帖子个数)" - setZeroToDisable: "设为 0 将不在实时更新时间线中投放广告" - adsTooClose: "广告投放时间间隔过短将可能显著损害用户体验。" _forgotPassword: enterEmail: "请输入您设置的电子邮箱地址,密码重置链接将发送至该邮箱上。" ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。" @@ -2067,8 +1456,6 @@ _plugin: install: "安装插件" installWarn: "请不要安装不可信的插件。" manage: "管理插件..." - viewSource: "查看源代码" - viewLog: "显示日志" _preferencesBackups: list: "已创建的备份" saveNew: "另存为" @@ -2089,8 +1476,8 @@ _preferencesBackups: invalidFile: "无效的的文件格式。" _registry: scope: "范围" - key: "键" - keys: "键" + key: "主要" + keys: "主要" domain: "域" createKey: "创建键" _aboutMisskey: @@ -2098,17 +1485,10 @@ _aboutMisskey: contributors: "主要贡献者" allContributors: "全体贡献者" source: "源代码" - original: "原版" - thisIsModifiedVersion: "{name}正在使用修改后的 Misskey。" translation: "翻译 Misskey" donate: "赞助 Misskey" morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰" patrons: "支持者" - projectMembers: "项目成员" -_displayOfSensitiveMedia: - respect: "隐藏敏感媒体" - ignore: "显示敏感媒体" - force: "隐藏所有内容" _instanceTicker: none: "不显示" remote: "仅远程用户" @@ -2129,21 +1509,25 @@ _channel: notesCount: "有 {n} 个帖子" nameAndDescription: "名称与描述" nameOnly: "仅名称" - allowRenoteToExternal: "允许在频道外转帖及引用" _menuDisplay: sideFull: "横向" sideIcon: "横向(图标)" top: "顶部" hide: "隐藏" _wordMute: - muteWords: "要隐藏的词" + muteWords: "禁用词" muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" muteWordsDescription2: "正则表达式用斜线包裹" + softDescription: "隐藏时间线中指定条件的帖子。" + hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。" + soft: "软屏蔽" + hard: "硬屏蔽" + mutedNotes: "被屏蔽的帖子" _instanceMute: - instanceMuteDescription: "隐藏服务器中的所有帖子和转帖,包括这些服务器上的用户回复。" + instanceMuteDescription: "屏蔽服务器中的所有帖子和转帖,包括这些服务器上的用户回复。" instanceMuteDescription2: "一行一个" - title: "下面实例中的帖子将被隐藏。" - heading: "已隐藏的服务器" + title: "隐藏服务器已设置的帖子。" + heading: "屏蔽服务器" _theme: explore: "寻找主题" install: "安装主题" @@ -2153,7 +1537,6 @@ _theme: installed: "{name} 已安装" installedThemes: "已安装的主题" builtinThemes: "标准主题" - instanceTheme: "服务器主题" alreadyInstalled: "此主题已经安装" invalid: "主题格式错误" make: "制作主题" @@ -2186,6 +1569,7 @@ _theme: header: "顶栏" navBg: "侧边栏背景" navFg: "侧栏文本" + navHoverFg: "侧栏文本(悬停)" navActive: "侧栏文本(活动)" navIndicator: "侧栏标记" link: "链接" @@ -2202,28 +1586,30 @@ _theme: infoFg: "信息文本" infoWarnBg: "警告背景" infoWarnFg: "警告文本" + cwBg: "隐藏内容按钮背景" + cwFg: "隐藏内容按钮文本" + cwHoverBg: "隐藏内容按钮背景(悬停)" toastBg: "Toast 通知背景" toastFg: "Toast 通知文本" buttonBg: "按钮背景" buttonHoverBg: "按钮背景(悬停)" inputBorder: "输入框边框" + listItemHoverBg: "下拉列表项目背景(悬停)" + driveFolderBg: "网盘的文件夹背景" + wallpaperOverlay: "壁纸叠加层" badge: "徽章" messageBg: "聊天背景" + accentDarken: "强调色(深)" + accentLighten: "强调色(浅)" fgHighlighted: "高亮显示文本" _sfx: note: "帖子" noteMy: "我的帖子" notification: "通知" - reaction: "选择回应时" - chatMessage: "聊天信息" -_soundSettings: - driveFile: "使用网盘内的音频" - driveFileWarn: "选择网盘上的文件" - driveFileTypeWarn: "不支持此文件" - driveFileTypeWarnDescription: "请选择音频文件" - driveFileDurationWarn: "音频过长" - driveFileDurationWarnDescription: "使用长音频可能会影响 Misskey 的使用。即使这样也要继续吗?" - driveFileError: "无法读取声音。请更改设置。" + chat: "聊天" + chatBg: "聊天背景" + antenna: "天线接收" + channel: "频道通知" _ago: future: "未来" justNow: "最近" @@ -2235,53 +1621,51 @@ _ago: monthsAgo: "{n} 月前" yearsAgo: "{n} 年前" invalid: "没有" -_timeIn: - seconds: "{n}秒后" - minutes: "{n} 分后" - hours: "{n} 小时后" - days: "{n}天后" - weeks: "{n} 周后" - months: "{n} 月后" - years: "{n} 年后" _time: second: "秒" minute: "分" hour: "小时" day: "日" +_timelineTutorial: + title: "Misskey 的使用方法" + step1_1: "这个画面是「时间线」。{name}的投稿会按照帖子的发布时间顺序来显示。" + step1_2: "时间线有许多种类,比如在「首页时间线」中展现的是你关注的人的贴文;而在「本地时间线」中展现的是{name}里全部用户的贴文。" + step2_1: "那么接下来,试着写一些什么东西来发布吧!你可以通过点击屏幕上的铅笔图标来打开投稿页面。" + step2_2: "第一次发布的帖子内容,建议包含自我介绍,以及「开始使用{name}了」。" + step3_1: "将想说的话发出去了吗?" + step3_2: "太棒了!现在你可以在你的时间线中看到刚刚发布的帖子了。" + step4_1: "试着对帖子使用「回应」吧!" + step4_2: "在他人的帖子上按下「+」图标,即可选择想要的表情来进行「回应」。" _2fa: alreadyRegistered: "此设备已被注册" - registerTOTP: "开始设置验证器" + registerTOTP: "开始设置认证应用" + passwordToTOTP: "请输入您的密码" step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。" step2: "然后,扫描屏幕上显示的二维码。" - step2Uri: "如果使用桌面应用程序的话,请输入下面的 URI" + step2Click: "通过点击二维码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册" + step2Url: "在桌面应用程序中输入以下 URL:" step3Title: "输入验证码" step3: "输入您的应用提供的动态口令以完成设置。" - setupCompleted: "设置完成" step4: "从现在开始,任何登录操作都将要求您提供动态口令。" securityKeyNotSupported: "您的浏览器不支持安全密钥。" - registerTOTPBeforeKey: "要注册安全密钥或 Passkey,请先设置验证器。" - securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 以及 Passkey 等。" + registerTOTPBeforeKey: "要注册安全密钥或 Passkey,请先设置验证器应用程序。" + securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 码以及 Passkey 等。" + chromePasskeyNotSupported: "目前不支持 Chrome 的 Passkey。" registerSecurityKey: "注册安全密钥或 Passkey" securityKeyName: "输入密钥名称" tapSecurityKey: "请按照浏览器说明操作来注册安全密钥或 Passkey。" removeKey: "删除安全密钥" - removeKeyConfirm: "确定要删除 {name} 吗?" - whyTOTPOnlyRenew: "当注册了安全密钥时,无法取消使用验证器。" - renewTOTP: "重置验证器" - renewTOTPConfirm: "当前验证器的验证码及备用代码已失效" + removeKeyConfirm: "您确定要删除 {name} 吗?" + whyTOTPOnlyRenew: "如果注册了安全密钥,则无法取消验证器应用程序上的设置。" + renewTOTP: "重置验证器应用程序" + renewTOTPConfirm: "当前验证器应用程序的验证码将不再有效" renewTOTPOk: "重新配置" renewTOTPCancel: "不用,谢谢" - checkBackupCodesBeforeCloseThisWizard: "在关闭此窗口前,请确认下面的备用代码" - backupCodes: "备用代码" - backupCodesDescription: "如果无法使用验证器,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。" - backupCodeUsedWarning: "已使用备用代码。若验证器无法使用,请尽快重置验证器。" - backupCodesExhaustedWarning: "已使用完所有的备用代码。若验证器无法使用,则无法再访问您的账户。请重置验证器。" - moreDetailedGuideHere: "此处为详细指南" _permissions: "read:account": "查看账户信息" "write:account": "更改帐户信息" - "read:blocks": "查看屏蔽列表" - "write:blocks": "编辑屏蔽列表" + "read:blocks": "查看黑名单" + "write:blocks": "编辑黑名单" "read:drive": "查看网盘" "write:drive": "管理网盘文件" "read:favorites": "查看收藏夹" @@ -2290,8 +1674,8 @@ _permissions: "write:following": "关注/取消关注" "read:messaging": "查看消息" "write:messaging": "撰写或删除消息" - "read:mutes": "查看隐藏列表" - "write:mutes": "编辑隐藏列表" + "read:mutes": "查看屏蔽列表" + "write:mutes": "编辑屏蔽列表" "write:notes": "撰写或删除帖子" "read:notifications": "查看通知" "write:notifications": "管理通知" @@ -2310,60 +1694,6 @@ _permissions: "write:gallery": "操作图库" "read:gallery-likes": "读取喜欢的图片" "write:gallery-likes": "操作喜欢的图片" - "read:flash": "查看 Play" - "write:flash": "编辑 Play" - "read:flash-likes": "查看 Play 的点赞" - "write:flash-likes": "编辑 Play 的点赞列表" - "read:admin:abuse-user-reports": "查看来自用户的举报" - "write:admin:delete-account": "删除用户账户" - "write:admin:delete-all-files-of-a-user": "删除用户所有的文件" - "read:admin:index-stats": "查看数据库索引相关的信息" - "read:admin:table-stats": "查看数据库表相关的信息" - "read:admin:user-ips": "查看用户 IP 地址" - "read:admin:meta": "查看实例的元数据" - "write:admin:reset-password": "重置用户密码" - "write:admin:resolve-abuse-user-report": "将来自用户的报告标记为「已解决」" - "write:admin:send-email": "发送邮件" - "read:admin:server-info": "查看服务器信息" - "read:admin:show-moderation-log": "查看管理日志" - "read:admin:show-user": "查看用户的非公开信息" - "write:admin:suspend-user": "冻结用户" - "write:admin:unset-user-avatar": "删除用户头像" - "write:admin:unset-user-banner": "删除用户横幅" - "write:admin:unsuspend-user": "解除用户冻结" - "write:admin:meta": "编辑实例元数据" - "write:admin:user-note": "编辑管理笔记" - "write:admin:roles": "编辑角色" - "read:admin:roles": "查看角色" - "write:admin:relays": "编辑中继" - "read:admin:relays": "查看中继" - "write:admin:invite-codes": "编辑邀请码" - "read:admin:invite-codes": "查看邀请码" - "write:admin:announcements": "编辑公告" - "read:admin:announcements": "查看公告" - "write:admin:avatar-decorations": "编辑头像挂件" - "read:admin:avatar-decorations": "查看头像挂件" - "write:admin:federation": "编辑联合相关信息" - "write:admin:account": "编辑用户账户" - "read:admin:account": "查看用户相关情报" - "write:admin:emoji": "编辑表情文字" - "read:admin:emoji": "查看表情文字" - "write:admin:queue": "编辑作业队列" - "read:admin:queue": "查看作业队列相关情报" - "write:admin:promo": "运营推广说明" - "write:admin:drive": "编辑用户网盘" - "read:admin:drive": "查看用户网盘相关情报" - "read:admin:stream": "使用管理员用的 Websocket API" - "write:admin:ad": "编辑广告" - "read:admin:ad": "查看广告" - "write:invite-codes": "生成邀请码" - "read:invite-codes": "获取已发行的邀请码" - "write:clip-favorite": "编辑便签的点赞" - "read:clip-favorite": "查看便签的点赞" - "read:federation": "查看联合相关信息" - "write:report-abuse": "举报用户" - "write:chat": "撰写或删除消息" - "read:chat": "查看聊天" _auth: shareAccessTitle: "应用程序授权许可" shareAccess: "您要授权允许 “{name}” 访问您的帐户吗?" @@ -2372,17 +1702,13 @@ _auth: permissionAsk: "这个应用程序需要以下权限" pleaseGoBack: "请返回到应用程序" callback: "回到应用程序" - accepted: "已允许访问" denied: "拒绝访问" - scopeUser: "以下面的用户进行操作" pleaseLogin: "在对应用进行授权许可之前,请先登录" - byClickingYouWillBeRedirectedToThisUrl: "允许访问后将会自动重定向到以下 URL" _antennaSources: all: "所有帖子" homeTimeline: "已关注用户的帖子" users: "来自指定用户的帖子" userList: "来自指定列表中的帖子" - userBlacklist: "除掉已选择用户后所有的帖子" _weekday: sunday: "星期日" monday: "星期一" @@ -2421,8 +1747,6 @@ _widgets: _userList: chooseList: "选择列表" clicker: "点击器" - birthdayFollowings: "今天是他们的生日" - chat: "聊天" _cw: hide: "隐藏" show: "查看更多" @@ -2476,7 +1800,7 @@ _profile: name: "昵称" username: "用户名" description: "个人简介" - youCanIncludeHashtags: "可以在个人简介中包含 #标签。" + youCanIncludeHashtags: "你可以在个人简介中包含一些#标签。" metadata: "附加信息" metadataEdit: "附加信息编辑" metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。" @@ -2484,22 +1808,15 @@ _profile: metadataContent: "内容" changeAvatar: "修改头像" changeBanner: "修改横幅" - verifiedLinkDescription: "如果将内容设置为 URL,当链接所指向的网页内包含自己的个人资料链接时,可以显示一个已验证图标。" - avatarDecorationMax: "最多可添加 {max} 个挂件" - followedMessage: "被关注时显示的消息" - followedMessageDescription: "可以设置被关注时向对方显示的短消息。" - followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在请求被批准后显示。" _exportOrImport: allNotes: "所有帖子" favoritedNotes: "收藏的帖子" - clips: "便签" followingList: "关注中" - muteList: "隐藏" - blockingList: "屏蔽" + muteList: "屏蔽" + blockingList: "拉黑" userLists: "列表" excludeMutingUsers: "排除屏蔽用户" excludeInactiveUsers: "排除不活跃用户" - withReplies: "在时间线中包含导入用户的回复" _charts: federation: "联合" apRequest: "请求" @@ -2546,11 +1863,13 @@ _play: title: "标题" script: "脚本" summary: "描述" - visibilityDescription: "设置为不公开后资料将不再显示,但知道 URL 的人仍可继续访问。" _pages: newPage: "创建页面" editPage: "编辑页面" readPage: "查看页面" + created: "页面已创建" + updated: "页面已更新" + deleted: "该页面已被删除" pageSetting: "页面设置" nameAlreadyExists: "该页面 URL 已存在" invalidNameTitle: "无效的页面 URL" @@ -2577,8 +1896,7 @@ _pages: fontSansSerif: "无衬线字体" eyeCatchingImageSet: "设置封面图片" eyeCatchingImageRemove: "删除封面图片" - chooseBlock: "添加内容块" - enterSectionTitle: "输入会话标题" + chooseBlock: "添加块" selectType: "选择类型" contentBlocks: "内容" inputBlocks: "输入" @@ -2589,8 +1907,6 @@ _pages: section: "章节" image: "图片" button: "按钮" - dynamic: "动态内容块" - dynamicDescription: "这个内容块已经废弃。以后请使用{play}。" note: "嵌入的帖子" _note: id: "帖子 ID" @@ -2610,28 +1926,11 @@ _notification: youReceivedFollowRequest: "您有新的关注请求" yourFollowRequestAccepted: "您的关注请求已通过" pollEnded: "问卷调查结果已生成。" - newNote: "新的帖子" unreadAntennaNote: "天线 {name}" - roleAssigned: "授予的角色" - chatRoomInvitationReceived: "受邀加入聊天室" emptyPushNotificationMessage: "推送通知已更新" achievementEarned: "获得成就" - testNotification: "测试通知" - checkNotificationBehavior: "检查通知显示" - sendTestNotification: "发送测试通知" - notificationWillBeDisplayedLikeThis: "通知将会这样表示" - reactedBySomeUsers: "{n} 人回应了" - likedBySomeUsers: "{n}人赞了你的帖子" - renotedBySomeUsers: "{n} 人转发了" - followedBySomeUsers: "被 {n} 人关注" - flushNotification: "重置通知历史" - exportOfXCompleted: "已完成 {x} 的导出" - login: "有新的登录" - createToken: "访问令牌已创建" - createTokenDescription: "如果不明白其用途,请遵循「{text}」的指示删除访问令牌。" _types: all: "全部" - note: "用户的新帖子" follow: "关注中" mention: "提及" reply: "回复" @@ -2641,13 +1940,7 @@ _notification: pollEnded: "问卷调查结束" receiveFollowRequest: "收到关注请求" followRequestAccepted: "关注请求已通过" - roleAssigned: "授予的角色" - chatRoomInvitationReceived: "受邀加入聊天室" achievementEarned: "取得的成就" - exportCompleted: "已完成导出" - login: "登录" - createToken: "创建访问令牌" - test: "测试通知" app: "关联应用的通知" _actions: followBack: "回关" @@ -2656,11 +1949,7 @@ _notification: _deck: alwaysShowMainColumn: "总是显示主列" columnAlign: "列对齐" - columnGap: "列间距" - deckMenuPosition: "Deck 菜单位置" - navbarPosition: "导航栏位置" addColumn: "添加列" - newNoteNotificationSettings: "新帖子通知设定" configureColumn: "列设置" swapLeft: "向左移动" swapRight: "向右移动" @@ -2672,12 +1961,8 @@ _deck: newProfile: "新建配置文件" deleteProfile: "删除配置文件" introduction: "将各列进行组合以创建您自己的界面!" - introduction2: "可以随时通过屏幕右侧的 + 来添加列" + introduction2: "您可以随时通过屏幕右侧的 + 来添加列" widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具" - useSimpleUiForNonRootPages: "用简易UI表示非根页面" - usedAsMinWidthWhenFlexible: "「自适应宽度」被启用的时候,这就是最小的宽度" - flexible: "自适应宽度" - enableSyncBetweenDevicesForProfiles: "启用个人资料信息跨设备同步" _columns: main: "主列" widgets: "小工具" @@ -2689,7 +1974,6 @@ _deck: mentions: "提及" direct: "指定用户" roleTimeline: "角色时间线" - chat: "聊天" _dialog: charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}" charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}" @@ -2701,10 +1985,9 @@ _drivecleaner: orderByCreatedAtAsc: "按添加日期降序排列" _webhookSettings: createWebhook: "创建 Webhook" - modifyWebhook: "编辑 webhook" name: "名称" secret: "密钥" - trigger: "触发器" + events: "何时运行 Webhook" active: "已启用" _events: follow: "关注时" @@ -2714,406 +1997,3 @@ _webhookSettings: renote: "被转发时" reaction: "被回应时" mention: "被提及时" - _systemEvents: - abuseReport: "当收到举报时" - abuseReportResolved: "当举报被处理时" - userCreated: "当用户被创建时" - inactiveModeratorsWarning: "当管理员在一段时间内不活跃时" - inactiveModeratorsInvitationOnlyChanged: "当因为管理员在一段时间内不活跃,导致服务器变为邀请制时" - deleteConfirm: "要删除 webhook 吗?" - testRemarks: "点击开关右侧的按钮,可以发送使用假数据的测试 Webhook。" -_abuseReport: - _notificationRecipient: - createRecipient: "新建举报通知" - modifyRecipient: "编辑举报通知" - recipientType: "通知类型" - _recipientType: - mail: "邮箱" - webhook: "Webhook" - _captions: - mail: "当收到新举报时,向持有监察员权限的用户发送通知邮件" - webhook: "当收到新举报及举报被处理时,使用指定的 SystemWebhook 发送通知" - keywords: "关键字" - notifiedUser: "通知的用户" - notifiedWebhook: "使用的 webhook" - deleteConfirm: "要删除通知吗?" -_moderationLogTypes: - createRole: "创建角色" - deleteRole: "删除角色" - updateRole: "更新角色" - assignRole: "分配角色" - unassignRole: "取消分配角色" - suspend: "冻结" - unsuspend: "解除冻结" - addCustomEmoji: "添加自定义表情符号" - updateCustomEmoji: "更新自定义表情符号" - deleteCustomEmoji: "删除自定义表情符号" - updateServerSettings: "更新服务器设置" - updateUserNote: "更新管理笔记" - deleteDriveFile: "删除文件" - deleteNote: "删除帖子" - createGlobalAnnouncement: "创建全体通知" - createUserAnnouncement: "创建用户通知" - updateGlobalAnnouncement: "更新全体通知" - updateUserAnnouncement: "更新用户通知" - deleteGlobalAnnouncement: "删除全体通知" - deleteUserAnnouncement: "删除用户通知" - resetPassword: "重置密码" - suspendRemoteInstance: "停止远程服务器" - unsuspendRemoteInstance: "恢复远程服务器" - updateRemoteInstanceNote: "更新远程服务器的管理笔记" - markSensitiveDriveFile: "标记网盘文件为敏感媒体" - unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体" - resolveAbuseReport: "处理举报" - forwardAbuseReport: "转发举报" - updateAbuseReportNote: "更新举报用管理笔记" - createInvitation: "生成邀请码" - createAd: "创建了广告" - deleteAd: "删除了广告" - updateAd: "更新了广告" - createAvatarDecoration: "新建头像挂件" - updateAvatarDecoration: "更新头像挂件" - deleteAvatarDecoration: "删除头像挂件" - unsetUserAvatar: "清除用户头像" - unsetUserBanner: "清除用户横幅" - createSystemWebhook: "新建了 SystemWebhook" - updateSystemWebhook: "更新了 SystemWebhook" - deleteSystemWebhook: "删除了 SystemWebhook" - createAbuseReportNotificationRecipient: "新建了举报通知" - updateAbuseReportNotificationRecipient: "更新了举报通知" - deleteAbuseReportNotificationRecipient: "删除了举报通知" - deleteAccount: "删除了账户" - deletePage: "删除了页面" - deleteFlash: "删除了 Play" - deleteGalleryPost: "删除了图库稿件" - deleteChatRoom: "删除聊天室" - updateProxyAccountDescription: "更新代理账户的简介" -_fileViewer: - title: "文件信息" - type: "文件类型" - size: "文件大小" - url: "URL" - uploadedAt: "添加日期" - attachedNotes: "附加到的帖子" - thisPageCanBeSeenFromTheAuthor: "此页只能被该文件的上传者查看。" -_externalResourceInstaller: - title: "从外部站点安装" - checkVendorBeforeInstall: "请在安装前确保来源可靠" - _plugin: - title: "要安装此插件吗?" - _theme: - title: "要安装此主题吗?" - _meta: - base: "基本配色方案" - _vendorInfo: - title: "来源信息" - endpoint: "参考端点" - hashVerify: "确认文件完整性" - _errors: - _invalidParams: - title: "缺少参数" - description: "缺少从外部站点获取数据所需的信息。请检查 URL。" - _resourceTypeNotSupported: - title: "不支持此外部资源" - description: "不支持从此外部站点获取的资源类型。请联系站点管理员。" - _failedToFetch: - title: "获取数据失败" - fetchErrorDescription: "与外部站点的通信失败。 如果重试后问题仍然存在,请联系站点管理员。" - parseErrorDescription: "无法读取从外部站点取得的数据。请联系站点管理员。" - _hashUnmatched: - title: "无法获取正确数据" - description: "无法验证数据的完整性。安全起见,无法继续安装。请联系站点管理员。" - _pluginParseFailed: - title: "AiScript 错误" - description: "虽然取得了数据,但是由于 AiScript 解析时出现错误,无法读取数据。请联系插件的作者。可在 Javascript 控制台查看错误详情。" - _pluginInstallFailed: - title: "插件安装失败" - description: "安装插件时出现错误。请再试一次。可在 Javascript 控制台查看错误详情。" - _themeParseFailed: - title: "主题解析错误" - description: "虽然取得了主题文件,但是由于解析时出现错误,无法加载主题。请联系主题的作者。可在 Javascript 控制台查看错误详情。" - _themeInstallFailed: - title: "安装主题失败" - description: "安装主题时出错。请再试一次。可在 Javascript 控制台查看错误详情。" -_dataSaver: - _media: - title: "加载媒体" - description: "防止自动加载图像和视频。 点击隐藏的图像/视频即可加载它们。\n" - _avatar: - title: "头像" - description: "停止播放头像的动画。 由于动画图片的文件大小可能比普通图像大,这可以进一步减少数据流量。" - _urlPreviewThumbnail: - title: "不显示 URL预览缩略图" - description: "将不再加载 URL 预览缩略图。" - _disableUrlPreview: - title: "禁用 URL 预览" - description: "关闭 URL 预览功能。与预览缩略图不同,减少了链接信息的加载。" - _code: - title: "代码高亮" - description: "如果使用了代码高亮标记,例如在 MFM 中,则在点击之前不会加载。 代码高亮要求加载每种高亮语言的定义文件,由于这些文件不再自动加载,因此有望减少数据传输量。" -_hemisphere: - N: "北半球" - S: "南半球" - caption: "在某些客户端设置中用来确定季节" -_reversi: - reversi: "黑白棋" - gameSettings: "对局设置" - chooseBoard: "选择棋盘" - blackOrWhite: "先手/后手" - blackIs: "{name}执黑(先手)" - rules: "规则" - thisGameIsStartedSoon: "对局即将开始" - waitingForOther: "等待对手准备" - waitingForMe: "等待你的准备" - waitingBoth: "请准备" - ready: "准备就绪" - cancelReady: "重新准备" - opponentTurn: "对手的回合" - myTurn: "你的回合" - turnOf: "{name}的回合" - pastTurnOf: "{name}的回合" - surrender: "认输" - surrendered: "已认输" - timeout: "超时" - drawn: "平局" - won: "{name}获胜" - black: "黑" - white: "白" - total: "总计" - turnCount: "第{count}回合" - myGames: "我的对局" - allGames: "所有对局" - ended: "结束" - playing: "对局中" - isLlotheo: "落子少的一方获胜(又名奥赛罗)" - loopedMap: "循环棋盘" - canPutEverywhere: "无限制放置模式" - timeLimitForEachTurn: "1回合的时间限制" - freeMatch: "自由匹配" - lookingForPlayer: "正在寻找对手" - gameCanceled: "对局被取消了" - shareToTlTheGameWhenStart: "开始时在时间线发布对局" - iStartedAGame: "对局开始!#MisskeyReversi" - opponentHasSettingsChanged: "对手更改了设定" - allowIrregularRules: "允许非常规规则(完全自由)" - disallowIrregularRules: "禁止非常规规则" - showBoardLabels: "显示行号和列号" - useAvatarAsStone: "用头像作为棋子" -_offlineScreen: - title: "离线——无法连接到服务器" - header: "无法连接到服务器" -_urlPreviewSetting: - title: "设置 URL 预览" - enable: "启用 URL 预览" - allowRedirect: "允许预览目标的重定向" - allowRedirectDescription: "如果输入的 URL 被重定向,可设置是否跟随重定向目标并显示预览。禁用此选项将节省服务器资源,但重定向目标的内容将不会显示。" - timeout: "超时阈值(ms)" - timeoutDescription: "如果获取预览所用时间超过这个值,则不生成预览。" - maximumContentLength: "Content-Length 的最大值(byte)" - maximumContentLengthDescription: "如果 Content-Length 超过这个值,则不生成预览。" - requireContentLength: "仅在能取得 Content-Length 时生成预览" - requireContentLengthDescription: "如果目标服务器不返回 Content-Length,则不生成预览。" - userAgent: "User-Agent" - userAgentDescription: "设定获取预览时使用的 User-Agent。留空时将使用默认的 User-Agent。" - summaryProxy: "用来生成预览的代理的 endpoint。" - summaryProxyDescription: "不使用 Misskey 本体,而是通过 Summaly Proxy 生成预览。" - summaryProxyDescription2: "下面的参数将作为查询字符串发送至代理。代理侧如果不支持此设置,则忽略设定值。" -_mediaControls: - pip: "画中画" - playbackRate: "播放速度" - loop: "循环播放" -_contextMenu: - title: "上下文菜单" - app: "应用" - appWithShift: "Shift 键应用" - native: "浏览器的用户界面" -_gridComponent: - _error: - requiredValue: "此值为必填项" - columnTypeNotSupport: "正则表达式验证仅支持 type:text 列。" - patternNotMatch: "此值与 {pattern} 的模式不一致" - notUnique: "此值必须唯一" -_roleSelectDialog: - notSelected: "未选中" -_customEmojisManager: - _gridCommon: - copySelectionRows: "复制所选行" - copySelectionRanges: "复制所选范围" - deleteSelectionRows: "删除所选行" - deleteSelectionRanges: "删除所选范围的行" - searchSettings: "搜索设置" - searchSettingCaption: "设置详细的搜索条件。" - searchLimit: "显示项目数" - sortOrder: "排序方式" - registrationLogs: "注册日志" - registrationLogsCaption: "将显示更新和删除表情符号的日志。执行更新或删除操作,又或者更改或重新加载页面时会消失。" - alertEmojisRegisterFailedDescription: "更新或删除表情符号失败。详情请确认注册日志。" - _logs: - showSuccessLogSwitch: "显示成功日志" - failureLogNothing: "没有失败日志。" - logNothing: "没有日志" - _remote: - selectionRowDetail: "所选行的详细信息" - importSelectionRows: "导入所选行" - importSelectionRangesRows: "导入所选范围的行" - importEmojisButton: "导入已选择的表情符号" - confirmImportEmojisTitle: "导入表情符号" - confirmImportEmojisDescription: "是否导入从远程服务器接收的 {count} 个表情符号?请密切关注表情符号的许可协议。" - _local: - tabTitleList: "已注册的表情符号列表" - tabTitleRegister: "注册表情符号" - _list: - emojisNothing: "没有已注册的表情符号。" - markAsDeleteTargetRows: "将所选行标记为删除对象" - markAsDeleteTargetRanges: "将所选范围的行标记为删除对象" - alertUpdateEmojisNothingDescription: "没有已更改的表情符号。" - alertDeleteEmojisNothingDescription: "没有被标记为删除对象的表情符号。" - confirmMovePage: "要离开此页吗?" - confirmChangeView: "要更改显示吗?" - confirmUpdateEmojisDescription: "要更新 {count} 个表情符号吗?" - confirmDeleteEmojisDescription: "要删除已选择的 {count} 个表情符号吗?" - confirmResetDescription: "至今为止所做的所有修改都将被重置。" - confirmMovePageDesciption: "此页面上的表情符号已更改。\n若不保存就离开此页,此页面上所有的更改都将丢失。" - dialogSelectRoleTitle: "按角色搜索表情符号" - _register: - uploadSettingTitle: "上传设置" - uploadSettingDescription: "可以在此页面设置上传表情符号时的行为。" - directoryToCategoryLabel: "将目录名设为「category」" - directoryToCategoryCaption: "拖放目录时,将目录名设置为「category」" - confirmRegisterEmojisDescription: "要将列表内显示的表情符号替换为新的自定义表情符号吗?(为降低服务器负载,一次操作最多只能注册 {count} 个表情符号)" - confirmClearEmojisDescription: "要放弃编辑并将列表内表示的表情符号清空吗?" - confirmUploadEmojisDescription: "要将拖放的 {count} 个文件上传到网盘上吗?" -_embedCodeGen: - title: "自定义嵌入代码" - header: "显示标题" - autoload: "连续加载(不推荐)" - maxHeight: "最大高度" - maxHeightDescription: "若将最大值设为 0 则不限制最大高度。为防止小工具无限增高,建议设置一下。" - maxHeightWarn: "最大高度限制已禁用(0)。若这不是您想要的效果,请将最大高度设一个值。" - previewIsNotActual: "由于超出了预览画面可显示的范围,因此显示内容会与实际嵌入时有所不同。" - rounded: "圆角" - border: "外边框" - applyToPreview: "应用预览" - generateCode: "生成嵌入代码" - codeGenerated: "已生成代码" - codeGeneratedDescription: "将生成的代码贴到网站上来使用。" -_selfXssPrevention: - warning: "警告" - title: "「在此处粘贴什么东西」是欺诈行为。" - description1: "如果在此处粘贴了什么,恶意用户可能会接管账户或者盗取个人资料。" - description2: "如果不能完全理解将要粘贴的内容,%c 请立即停止操作并关闭这个窗口。" - description3: "详情请看这里。{link}" -_followRequest: - recieved: "已收到申请" - sent: "已发送申请" -_remoteLookupErrors: - _federationNotAllowed: - title: "无法与此服务器通信" - description: "与此服务器的通信可能被禁用,又或者是屏蔽了此服务器或被此服务器屏蔽了。\n请联系服务器的管理者。" - _uriInvalid: - title: "URI 有误" - description: "输入的 URI 有问题。请确认是否输入了 URI 中无法使用的字符。" - _requestFailed: - title: "请求失败" - description: "与该服务器的通信失败。对面服务器可能不可用。另外,请确认是否输入了无效或不存在的 URI。" - _responseInvalid: - title: "响应无效" - description: "成功与此服务器通信,但返回的数据无效。" - _noSuchObject: - title: "未找到" - description: "未找到请求的资源。请再次检查 URI。" -_captcha: - verify: "请通过 CAPTCHA 验证" - testSiteKeyMessage: "输入测试用的网站密钥及私密密钥后可以生成预览并检查,\n详情请看以下页面。" - _error: - _requestFailed: - title: "请求 CAPTCHA 失败" - text: "请稍后再试,又或者再检查一次设置。" - _verificationFailed: - title: "验证 CAPTCHA 失败" - text: "请再次确认设置是否正确。" - _unknown: - title: "CAPTCHA 错误" - text: "发生意外错误。" -_bootErrors: - title: "加载失败" - serverError: "请稍等片刻再重试。若问题仍无法解决,请将以下 Error ID 一起发送给管理员。" - solution: "以下方法或许可以解决问题:" - solution1: "将浏览器及操作系统更新到最新版本" - solution2: "禁用广告屏蔽插件" - solution3: "清除浏览器缓存" - solution4: "(Tor Browser)将 dom.webaudio.enabled 设定为 true" - otherOption: "其它选项" - otherOption1: "清除客户端设定与缓存" - otherOption2: "使用简易客户端" - otherOption3: "启动修复工具" -_search: - searchScopeAll: "全部" - searchScopeLocal: "本地" - searchScopeServer: "指定服务器" - searchScopeUser: "指定用户" - pleaseEnterServerHost: "请填写服务器主机名" - pleaseSelectUser: "请选择用户" - serverHostPlaceholder: "如:misskey.example.com" -_serverSetupWizard: - installCompleted: "Misskey 安装完成!" - firstCreateAccount: "首先来创建管理员账号吧。" - accountCreated: "管理员账号已创建!" - serverSetting: "服务器设置" - youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "用此向导来轻松地以最佳方式配置服务器。" - settingsYouMakeHereCanBeChangedLater: "这里的设置在之后也能更改。" - howWillYouUseMisskey: "打算怎样使用 Misskey?" - _use: - single: "单用户服务器" - single_description: "仅供自己使用的单人服务器" - single_youCanCreateMultipleAccounts: "使用单用户服务器模式使用时,也可以根据需要创建多个账号。" - group: "小圈子服务器" - group_description: "邀请其他可信用户一起使用的多人服务器" - open: "开放服务器" - open_description: "以容纳不限定数量的用户的模式运行" - openServerAdvice: "容纳不限定数量的用户有风险。推荐建立能应对各种问题的强大的管理体制来运营。" - openServerAntiSpamAdvice: "为防止自己的服务器成为广告发信基地,请打开如 reCAPTCHA 等 Bot 防御功能,并谨慎关注安全性。" - howManyUsersDoYouExpect: "预计会有多少用户?" - _scale: - small: "100 人以下(小规模)" - medium: "100 人以上 1000 人以下(中规模)" - large: "1000 人以上(大规模)" - largeScaleServerAdvice: "运营大规模服务器可能需要高级基础设施知识,如负载均衡和数据库复制。" - doYouConnectToFediverse: "要加入 Fediverse 吗?" - doYouConnectToFediverse_description1: "若加入由分散性服务器所构成的网络(Fediverse),将能与其它服务器交换内容。" - doYouConnectToFediverse_description2: "加入 Fediverse 在这里被称为「联合」。" - youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。" - adminInfo: "管理员信息" - adminInfo_description: "设置用于接受询问的管理员信息。" - adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。" - followingSettingsAreRecommended: "推荐以下设置" - applyTheseSettings: "使用此设置" - skipSettings: "跳过设置" - settingsCompleted: "设置完成!" - settingsCompleted_description: "辛苦了。设置已完成,可以立即开始使用服务器了。" - settingsCompleted_description2: "服务器的详细设置可在「控制面板」进行。" - donationRequest: "请求捐助" - _donationRequest: - text1: "Misskey 是由志愿者开发的免费软件。" - text2: "为了今后也能继续开发,如果可以的话,请考虑一下捐助。" - text3: "也有面向支援者的特典!" -_uploader: - compressedToX: "压缩 {x}" - savedXPercent: "节省了 {x}% 的空间" - abortConfirm: "还有未上传的文件,要中止吗?" - doneConfirm: "还有未上传的文件,要完成吗?" - maxFileSizeIsX: "可上传最大 {x} 的文件。" - allowedTypes: "可上传的文件类型" - tip: "文件还没有被上传。可在此对话框中进行上传前确认、重命名、压缩、裁剪等操作。准备完成后,点击「上传」即可开始上传。" -_clientPerformanceIssueTip: - title: "如果觉得电池耗电过高" - makeSureDisabledAdBlocker: "请关闭广告拦截器" - makeSureDisabledAdBlocker_description: "广告拦截器会影响性能。请检查操作系统功能、浏览器功能或附加组件是否启用了广告拦截器。" - makeSureDisabledCustomCss: "请关闭自定义 CSS" - makeSureDisabledCustomCss_description: "覆盖样式可能会影响性能。请确保没有启用任何自定义 CSS 或覆盖样式的扩展。" - makeSureDisabledAddons: "请关闭扩展" - makeSureDisabledAddons_description: "某些扩展可能会干扰客户端的运行并影响性能。尝试禁用浏览器扩展并查看是否有改善。" -_clip: - tip: "便签功能可以将帖子合并在一起。" -_userLists: - tip: "可创建包含任意用户的列表。已创建的列表可作为时间线查看。" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index e4d7d69e46..2a8dc42b90 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1,17 +1,13 @@ --- -_lang_: "繁體中文(台灣)" +_lang_: "繁體中文" headlineMisskey: "貼文連繫網路" -introMisskey: "歡迎!Misskey 是一個開放原始碼且去中心化的社群網路服務。\n發布「貼文」向身邊的人分享您的想法!📡\n利用「反應」表達您對貼文的感覺!👍\n讓我們一起探索新的世界吧!🚀" -poweredByMisskeyDescription: "{name}是開放原始碼平臺 Misskey 的伺服器之一。" -monthAndDay: "{month} 月 {day} 日" +introMisskey: "歡迎! Misskey是一個開放原始碼且去中心化的社群網路。\n透過「貼文」分享周邊新鮮事,並告訴其他人您的想法!📡\n透過「反應」功能,對大家的貼文表達情感!👍\n一起來探索這個新的世界吧!🚀" +poweredByMisskeyDescription: "{name}是使用開放原始碼平台Misskey的服務之一(稱為 Misskey 伺服器)。\n" +monthAndDay: "{month}月 {day}日" search: "搜尋" -reset: "重設" notifications: "通知" username: "使用者名稱" password: "密碼" -initialPasswordForSetup: "啟動初始設定的密碼" -initialPasswordIsIncorrect: "啟動初始設定的密碼錯誤。" -initialPasswordForSetupDescription: "如果您自己安裝了 Misskey,請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼,請將其留空並繼續。" forgotPassword: "忘記密碼" fetchingAsApObject: "從聯邦宇宙取得中..." ok: "OK" @@ -20,7 +16,7 @@ cancel: "取消" noThankYou: "現在不要" enterUsername: "輸入使用者名稱" renotedBy: "{user} 轉發了" -noNotes: "無貼文" +noNotes: "無貼文。" noNotifications: "沒有通知" instance: "伺服器" settings: "設定" @@ -30,7 +26,7 @@ otherSettings: "其他設定" openInWindow: "在新視窗開啟" profile: "個人檔案" timeline: "時間軸" -noAccountDescription: "此使用者尚未自我介紹" +noAccountDescription: "此用戶還沒有自我介紹" login: "登入" loggingIn: "登入中" logout: "登出" @@ -49,30 +45,25 @@ pin: "置頂" unpin: "取消置頂" copyContent: "複製內容" copyLink: "複製連結" -copyRemoteLink: "複製遠端的連結" -copyLinkRenote: "複製轉發的連結" delete: "刪除" deleteAndEdit: "刪除並編輯" deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。" addToList: "加入至清單" -addToAntenna: "新增至天線" sendMessage: "發送訊息" copyRSS: "複製RSS" copyUsername: "複製使用者名稱" -copyUserId: "複製使用者 ID" -copyNoteId: "複製貼文 ID" -copyFileId: "複製檔案 ID" +copyUserId: "複製使用者ID" +copyNoteId: "複製貼文ID" +copyFileId: "複製檔案ID" copyFolderId: "複製資料夾ID" -copyProfileUrl: "複製個人資料網址" searchUser: "搜尋使用者" -searchThisUsersNotes: "搜尋這個使用者的貼文" reply: "回覆" loadMore: "載入更多" showMore: "載入更多" showLess: "關閉" youGotNewFollower: "您有新的追隨者" receiveFollowRequest: "您有新的追隨請求" -followRequestAccepted: "追隨請求已被接受" +followRequestAccepted: "追隨請求已接受" mention: "提及" mentions: "提及" directNotes: "私訊" @@ -81,10 +72,10 @@ import: "匯入" export: "匯出" files: "檔案" download: "下載" -driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此檔案的貼文也會跟著被刪除。" +driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n" unfollowConfirm: "確定要取消追隨{name}嗎?" -exportRequested: "已請求匯出。這可能會花一點時間。匯出的檔案將會被放到雲端硬碟裡。" -importRequested: "已請求匯入。這可能會花一點時間。" +exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。" +importRequested: "已請求匯入。這可能會花一點時間" lists: "清單" noLists: "你沒有任何清單" note: "貼文" @@ -97,13 +88,13 @@ manageLists: "管理清單" error: "錯誤" somethingHappened: "發生錯誤" retry: "重試" -pageLoadError: "無法載入頁面。" -pageLoadErrorDescription: "這通常是網路錯誤或瀏覽器快取殘留而引起的。請先清除瀏覽器快取,稍後再重試。" +pageLoadError: "載入頁面失敗" +pageLoadErrorDescription: "這通常是因為網路錯誤或是瀏覽器快取殘留的原因。請先清除瀏覽器快取,稍後再重試" serverIsDead: "伺服器沒有回應。請稍等片刻再試。" -youShouldUpgradeClient: "請重新載入以使用新版客戶端顯示此頁面。" +youShouldUpgradeClient: "請重新載入以使用新版本的客戶端顯示此頁面" enterListName: "輸入清單名稱" privacy: "隱私" -makeFollowManuallyApprove: "追隨需要核准" +makeFollowManuallyApprove: "手動審核追隨請求" defaultNoteVisibility: "預設可見性" follow: "追隨" followRequest: "追隨請求" @@ -113,33 +104,24 @@ followRequestPending: "追隨許可待批准" enterEmoji: "輸入表情符號" renote: "轉發" unrenote: "取消轉發" -renoted: "轉發成功。" -renotedToX: "轉發給 {name} 了。" +renoted: "轉發成功" cantRenote: "無法轉發此貼文。" cantReRenote: "無法轉發之前已經轉發過的內容。" quote: "引用" inChannelRenote: "在頻道內轉發" inChannelQuote: "在頻道內引用" -renoteToChannel: "轉發至頻道" -renoteToOtherChannel: "轉發至其他頻道" pinnedNote: "已置頂的貼文" pinned: "置頂" you: "您" -clickToShow: "點擊查看" +clickToShow: "按一下以顯示" sensitive: "敏感內容" add: "新增" reaction: "反應" reactions: "反應" -emojiPicker: "表情符號選擇器" -pinnedEmojisForReactionSettingDescription: "選擇反應時可以設定要固定顯示在頂端的表情符號" -pinnedEmojisSettingDescription: "輸入表情符號時可以設定要固定顯示在頂端的表情符號" -emojiPickerDisplay: "顯示表情符號選擇器" -overwriteFromPinnedEmojisForReaction: "從反應複寫設定" -overwriteFromPinnedEmojis: "從一般複寫設定" -reactionSettingDescription2: "拖動以交換,點擊以刪除,按下「+」以新增。" +reactionSetting: "在選擇器中顯示反應" +reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。" rememberNoteVisibility: "記住貼文可見性" attachCancel: "移除附件" -deleteFile: "刪除檔案" markAsSensitive: "標記為敏感內容" unmarkAsSensitive: "取消標記為敏感內容" enterFileName: "請輸入檔案名稱" @@ -151,16 +133,15 @@ block: "封鎖" unblock: "解除封鎖" suspend: "凍結" unsuspend: "解除凍結" -blockConfirm: "確定要封鎖此使用者嗎?" -unblockConfirm: "確定要解除封鎖此使用者嗎?" -suspendConfirm: "確定凍結此使用者?" -unsuspendConfirm: "確定解凍此使用者?" +blockConfirm: "確定要封鎖此用戶?" +unblockConfirm: "確定解除封鎖此用戶?" +suspendConfirm: "確定凍結此帳戶?" +unsuspendConfirm: "確定解凍此帳戶?" selectList: "選擇清單" editList: "編輯清單" selectChannel: "選擇頻道" selectAntenna: "選擇天線" editAntenna: "編輯天線" -createAntenna: "建立天線" selectWidget: "選擇小工具" editWidgets: "編輯小工具" editWidgetsExit: "完成" @@ -168,29 +149,22 @@ customEmojis: "自訂表情符號" emoji: "表情符號" emojis: "表情符號" emojiName: "表情符號名稱" -emojiUrl: "表情符號 URL" -addEmoji: "新增表情符號" +emojiUrl: "表情符號URL" +addEmoji: "加入表情符號" settingGuide: "推薦設定" cacheRemoteFiles: "快取遠端檔案" -cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但建議將 default.yml 的 proxyRemoteFiles 設為 true,以便產生圖片的縮圖並保護使用者的隱私。" -youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全部刪除。" -cacheRemoteSensitiveFiles: "快取遠端的敏感檔案" -cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。" +cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外連接數據。" flagAsBot: "此使用者是機器人" -flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整 Misskey 內部系統將本帳戶識別為機器人。" -flagAsCat: "此帳戶是一隻貓,喵~~~!!!" -flagAsCatDescription: "喵喵喵??" +flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Misskey內部系統將本帳戶識別為機器人" +flagAsCat: "喵~~~~~~~~~~~~~~!!!!!!!!!!!!" +flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示" flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" -flagShowTimelineRepliesDescription: "啟用後,時間軸除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。" +flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。" autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求" -addAccount: "新增帳戶" +addAccount: "添加帳戶" reloadAccountsList: "更新帳戶清單的資訊" loginFailed: "登入失敗" showOnRemote: "轉到所在實例顯示" -continueOnRemote: "在遠端伺服器繼續" -chooseServerOnMisskeyHub: "從 Misskey Hub 選擇伺服器" -specifyServerHost: "直接指定伺服器網域" -inputHostName: "請輸入域名" general: "一般" wallpaper: "桌布" setWallpaper: "設定桌布" @@ -199,9 +173,8 @@ searchWith: "搜尋: {q}" youHaveNoLists: "你沒有任何清單" followConfirm: "你真的要追隨{name}嗎?" proxyAccount: "代理帳戶" -proxyAccountDescription: "代理帳戶是在特定條件下充當遠端追隨者的帳戶。例如,當使用者新增遠端使用者至其列表時,若沒有本地使用者追隨該遠端使用者,則其活動將不會傳送至伺服器,此時便會由代理帳戶代為追隨以解決問題。" +proxyAccountDescription: "代理帳戶是在某些情況下充當其他伺服器用戶的帳戶。例如,當使用者將一個來自其他伺服器的帳戶放在列表中時,由於沒有其他使用者追隨該帳戶,該指令不會傳送到該伺服器上,因此會由代理帳戶追隨。" host: "主機" -selectSelf: "選擇自己" selectUser: "選取使用者" recipient: "收件人" annotation: "註解" @@ -216,41 +189,33 @@ perHour: "每小時" perDay: "每日" stopActivityDelivery: "停止發送活動" blockThisInstance: "封鎖此伺服器" -silenceThisInstance: "禁言此伺服器" -mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言" operations: "操作" software: "軟體" -softwareName: "軟體名稱" version: "版本" -metadata: "詮釋資料" -withNFiles: "{n} 個檔案" +metadata: "元資料" +withNFiles: "{n}個檔案" monitor: "監視器" jobQueue: "佇列" -cpuAndMemory: "CPU 及記憶體" +cpuAndMemory: "CPU及記憶體用量" network: "網路" disk: "硬碟" instanceInfo: "伺服器資訊" statistics: "統計" clearQueue: "清除佇列" clearQueueConfirmTitle: "確定要清除佇列嗎?" -clearQueueConfirmText: "未成功發佈的貼文將不會再嘗試發佈。通常不需要進行這項操作。" +clearQueueConfirmText: "未發佈的貼文將不會發佈。您通常不需要確認。" clearCachedFiles: "清除快取資料" clearCachedFilesConfirm: "確定要清除所有遠端暫存資料嗎?" blockedInstances: "已封鎖的伺服器" blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。" -silencedInstances: "被禁言的伺服器" -silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。" -mediaSilencedInstances: "媒體被禁言的伺服器" -mediaSilencedInstancesDescription: "設定您想要對媒體設定禁言的伺服器,以換行符號區隔。來自被媒體禁言的伺服器所屬帳戶的所有檔案都會被視為敏感檔案,且自訂表情符號不能使用。被封鎖的伺服器不受影響。" -federationAllowedHosts: "允許聯邦通訊的伺服器" -federationAllowedHostsDescription: "設定允許聯邦通訊的伺服器主機,以換行符號分隔。" muteAndBlock: "靜音和封鎖" -mutedUsers: "被靜音的使用者" -blockedUsers: "被封鎖的使用者" +mutedUsers: "已靜音用戶" +blockedUsers: "已封鎖用戶" noUsers: "沒有任何使用者" editProfile: "編輯個人檔案" noteDeleteConfirm: "確定刪除此貼文嗎?" pinLimitExceeded: "不能置頂更多貼文了" +intro: "Misskey 部署完成!請建立管理員帳戶。" done: "完成" processing: "處理中" preview: "預覽" @@ -260,14 +225,14 @@ noCustomEmojis: "沒有自訂的表情符號" noJobs: "沒有任務" federating: "聯邦運作中" blocked: "已封鎖" -suspended: "停止發送" +suspended: "已凍結" all: "全部" subscribing: "訂閱中" -publishing: "發送中" +publishing: "直播中" notResponding: "沒有回應" instanceFollowing: "追隨的伺服器" instanceFollowers: "伺服器的追隨者" -instanceUsers: "伺服器使用者" +instanceUsers: "用戶" changePassword: "修改密碼" security: "安全性" retypedNotMatch: "兩次輸入不一致。" @@ -277,7 +242,7 @@ newPasswordRetype: "確認密碼" attachFile: "上傳附件" more: "更多!" featured: "精選" -usernameOrUserId: "使用者名稱或使用者 ID" +usernameOrUserId: "使用者名稱或使用者ID" noSuchUser: "使用者不存在" lookup: "查詢" announcements: "公告" @@ -287,23 +252,22 @@ removed: "已刪除" removeAreYouSure: "確定要刪掉「{x}」嗎?" deleteAreYouSure: "確定要刪掉「{x}」嗎?" resetAreYouSure: "確定要重設嗎?" -areYouSure: "是否確定?" saved: "已儲存" +messaging: "聊天" upload: "上傳" keepOriginalUploading: "保留原圖" -keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成適用於網路傳送的版本。" -fromDrive: "從雲端空間中選擇" -fromUrl: "從 URL 上傳" +keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。" +fromDrive: "從雲端空間" +fromUrl: "從URL" uploadFromUrl: "從網址上傳" -uploadFromUrlDescription: "您要上傳的檔案網址" +uploadFromUrlDescription: "您要上傳的文件的URL" uploadFromUrlRequested: "已請求上傳" uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。" -uploadNFiles: "上傳了 {n} 個檔案" explore: "探索" messageRead: "已讀" noMoreHistory: "沒有更多歷史紀錄" -startChat: "開始聊天" -nUsersRead: "{n} 人已讀" +startMessaging: "開始聊天" +nUsersRead: "{n}人已讀" agreeTo: "我同意{0}" agree: "同意" agreeBelow: "同意以下內容" @@ -311,38 +275,34 @@ basicNotesBeforeCreateAccount: "基本注意事項" termsOfService: "服務條款" start: "開始" home: "首頁" -remoteUserCaution: "由於該使用者來自其他實例,因此其資訊可能不完整。" +remoteUserCaution: "由於該使用者來自遠端實例,因此資訊可能非即時的。" activity: "動態" images: "圖片" image: "圖片" birthday: "生日" -yearsOld: "{age} 歲" +yearsOld: "{age}歲" registeredDate: "註冊日期" location: "位置" -theme: "佈景主題" -themeForLightMode: "在淺色模式下使用的佈景主題" -themeForDarkMode: "在深色模式下使用的佈景主題" +theme: "外觀主題" +themeForLightMode: "在淺色模式下使用的主題" +themeForDarkMode: "在黑暗模式下使用的主題" light: "淺色" -dark: "深色" -lightThemes: "淺色佈景主題" -darkThemes: "深色佈景主題" -syncDeviceDarkMode: "與裝置的深色模式同步" -switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」已開啟。要關閉同步並手動切換模式嗎?\n" +dark: "黑暗" +lightThemes: "明亮主題" +darkThemes: "黑暗主題" +syncDeviceDarkMode: "將黑暗模式與設備設置同步" drive: "雲端硬碟" fileName: "檔案名稱" selectFile: "選擇檔案" selectFiles: "選擇檔案" selectFolder: "選擇資料夾" selectFolders: "選擇資料夾" -fileNotSelected: "尚未選擇檔案" renameFile: "重新命名檔案" folderName: "資料夾名稱" createFolder: "新增資料夾" renameFolder: "重新命名資料夾" deleteFolder: "刪除資料夾" -folder: "資料夾" addFile: "加入附件" -showFile: "瀏覽文件" emptyDrive: "雲端硬碟為空" emptyFolder: "資料夾為空" unableToDelete: "無法刪除" @@ -355,44 +315,46 @@ copyUrl: "複製URL" rename: "重新命名" avatar: "大頭貼" banner: "橫幅" -displayOfSensitiveMedia: "敏感檔案的顯示" +displayOfSensitiveMedia: "敏感性媒體的顯示" whenServerDisconnected: "與伺服器的連接中斷時" disconnectedFromServer: "與伺服器中斷連線" reload: "重新整理" doNothing: "無視" reloadConfirm: "確定要重新整理嗎?" watch: "關注" -unwatch: "取消關注" +unwatch: "取消追隨" accept: "接受" reject: "拒絕" normal: "正常" instanceName: "伺服器名稱" instanceDescription: "伺服器介紹" maintainerName: "管理員名稱" -maintainerEmail: "管理員信箱" -tosUrl: "服務條款 URL" +maintainerEmail: "管理員郵箱" +tosUrl: "服務條款URL" thisYear: "本年" thisMonth: "本月" today: "本日" -dayX: "{day} 日" -monthX: "{month} 月" -yearX: "{year} 年" +dayX: "{day}日" +monthX: "{month}月" +yearX: "{year}年" pages: "頁面" integration: "整合" -connectService: "已連結" -disconnectService: "已斷開 " -enableLocalTimeline: "啟用本地時間軸" +connectService: "己連結" +disconnectService: "己斷開 " +enableLocalTimeline: "開啟本地時間軸" enableGlobalTimeline: "啟用全域時間軸" -disablingTimelinesInfo: "為了方便,即使您關閉了時間軸功能,管理員和審查員仍可以繼續使用。" +disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審查員仍可以繼續使用。" registration: "註冊" +enableRegistration: "開啟新使用者註冊" invite: "邀請" -driveCapacityPerLocalAccount: "每個本地使用者的雲端硬碟容量" +driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小" driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小" -inMb: "以 MB 為單位" +inMb: "以Mbps為單位" +iconUrl: "圖標URL" bannerUrl: "橫幅圖片URL" backgroundImageUrl: "背景圖片的來源網址 " basicInfo: "基本資訊" -pinnedUsers: "置頂使用者" +pinnedUsers: "置頂用戶" pinnedUsersDescription: "在「探索」頁面中使用換行標記想要置頂的使用者。" pinnedPages: "釘選頁面" pinnedPagesDescription: "輸入要固定至實例首頁的頁面路徑,以換行符分隔。" @@ -400,43 +362,36 @@ pinnedClipId: "置頂的摘錄ID" pinnedNotes: "已置頂的貼文" hcaptcha: "hCaptcha" enableHcaptcha: "啟用 hCaptcha" -hcaptchaSiteKey: "hcaptchaSiteKey" -hcaptchaSecretKey: "hcaptchaSecretKey" -mcaptcha: "mCaptcha" -enableMcaptcha: "啟用 mCaptcha" -mcaptchaSiteKey: "網站金鑰" -mcaptchaSecretKey: "私密金鑰" -mcaptchaInstanceUrl: "mCaptcha 的實例網址" +hcaptchaSiteKey: "網站金鑰" +hcaptchaSecretKey: "金鑰" recaptcha: "reCAPTCHA" enableRecaptcha: "啟用 reCAPTCHA" recaptchaSiteKey: "網站金鑰" recaptchaSecretKey: "金鑰" turnstile: "Turnstile" enableTurnstile: "啟用 Turnstile" -turnstileSiteKey: "turnstileSiteKey" -turnstileSecretKey: "turnstileSecretKey" -avoidMultiCaptchaConfirm: "使用多種驗證方式可能會造成干擾,您要關閉其他驗證方式嗎?您可以按「取消」保留多種驗證方式。" +turnstileSiteKey: "網站金鑰" +turnstileSecretKey: "金鑰" +avoidMultiCaptchaConfirm: "使用多種驗證方式可能會造成干擾,您要關閉其他驗證方式嗎?您可以按“取消”保留多種驗證方式。" antennas: "天線" manageAntennas: "管理天線" name: "名稱" antennaSource: "接收來源" antennaKeywords: "包含關鍵字" antennaExcludeKeywords: "排除關鍵字" -antennaExcludeBots: "排除機器人帳戶" -antennaKeywordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)" +antennaKeywordsDescription: "用空格分隔指定AND、用換行符分隔指定OR" notifyAntenna: "通知有新貼文" withFileAntenna: "僅帶有附件的貼文" -excludeNotesInSensitiveChannel: "排除敏感頻道的貼文" -enableServiceworker: "啟用瀏覽器的推播通知" -antennaUsersDescription: "填寫使用者名稱,以換行分隔" +enableServiceworker: "開啟 ServiceWorker" +antennaUsersDescription: "指定用換行符分隔的用戶名" caseSensitive: "區分大小寫" withReplies: "包含回覆" connectedTo: "您的帳戶已連接到以下社交帳戶" notesAndReplies: "貼文與回覆" withFiles: "附件" silence: "禁言" -silenceConfirm: "確定要禁言此使用者嗎?" -unsilence: "解除禁言" +silenceConfirm: "確定要靜音此使用者嗎?" +unsilence: "解除靜音" unsilenceConfirm: "確定要解除禁言嗎?" popularUsers: "熱門使用者" recentlyUpdatedUsers: "最近發文的使用者" @@ -450,31 +405,27 @@ about: "關於" aboutMisskey: "關於 Misskey" administrator: "管理員" token: "權杖" -2fa: "雙重驗證" -setupOf2fa: "設定雙重驗證" +2fa: "雙因素驗證" totp: "驗證應用程式" totpDescription: "以驗證應用程式輸入一次性密碼" moderator: "審查員" moderation: "審查" -moderationNote: "管理筆記" -moderationNoteDescription: "您可以編寫僅在審查員之間共用的註解。" -addModerationNote: "新增管理筆記" -moderationLogs: "管理日誌" -nUsersMentioned: "被 {n} 個人提及" -securityKeyAndPasskey: "安全金鑰、通行金鑰" +nUsersMentioned: "提到了{n}" +securityKeyAndPasskey: "安全金鑰・Passkey" securityKey: "安全金鑰" lastUsed: "上次使用" -lastUsedAt: "上次使用:{t}" -unregister: "註銷" -passwordLessLogin: "無密碼登入" -passwordLessLoginDescription: "不使用密碼,以安全金鑰或通行金鑰登入" -resetPassword: "重設密碼" +lastUsedAt: "最後使用:{t}" +unregister: "註銷帳戶" +passwordLessLogin: "設置無密碼登入" +passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入" +resetPassword: "重置密碼" newPasswordIs: "新密碼為「{password}」" reduceUiAnimation: "減少介面的動態視覺" share: "分享" -notFound: "查無項目" -notFoundDescription: "查無此頁" +notFound: "找不到" +notFoundDescription: "找不到與指定URL回應的頁面" uploadFolder: "預設上傳資料夾" +cacheClear: "清除快取" markAsReadAllNotifications: "標記所有通知為已讀" markAsReadAllUnreadNotes: "標記所有貼文為已讀" markAsReadAllTalkMessages: "標記所有訊息為已讀" @@ -488,14 +439,14 @@ title: "標題" text: "文字" enable: "啟用" next: "下一步" -retype: "重新輸入" +retype: "再次輸入" noteOf: "{user}的貼文" quoteAttached: "引用" quoteQuestion: "是否要引用?" -attachAsFileQuestion: "剪貼簿的文字較長。請問是否要將其以文字檔的方式附加呢?" +noMessagesYet: "沒有訊息" +newMessageExists: "有新的訊息" onlyOneFileCanBeAttached: "只能加入一個附件" signinRequired: "請先登入" -signinOrContinueOnRemote: "若要繼續,需前往您所在的伺服器,或者註冊並登入此伺服器" invitations: "邀請" invitationCode: "邀請碼" checking: "確認中" @@ -517,26 +468,22 @@ uiLanguage: "介面語言" aboutX: "關於{x}" emojiStyle: "表情符號的風格" native: "原生" -menuStyle: "選單風格" -style: "風格" -drawer: "側邊欄" -popup: "彈出式視窗" -showNoteActionsOnlyHover: "僅於游標懸停時顯示貼文選項" -showReactionsCount: "顯示貼文的反應數目" +disableDrawer: "不顯示下拉式選單" +showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項" noHistory: "沒有歷史紀錄" signinHistory: "登入歷史" -enableAdvancedMfm: "啟用進階 MFM" -enableAnimatedMfm: "啟用 MFM 動畫" +enableAdvancedMfm: "啟用高級MFM" +enableAnimatedMfm: "啟用MFM動畫" doing: "正在進行" category: "類別" tags: "標籤" docSource: "文件來源" createAccount: "建立帳戶" existingAccount: "現有帳戶" -regenerate: "再次生成" +regenerate: "再生" fontSize: "字體大小" -mediaListWithOneImageAppearance: "只有一張圖片時的檔案列表高度" -limitTo: "上限為 {x}" +mediaListWithOneImageAppearance: "僅1枚圖片的媒體列表高度" +limitTo: "上限為{x}" noFollowRequests: "沒有追隨您的請求" openImageInNewTab: "於新分頁中開啟圖片" dashboard: "儀表板" @@ -544,7 +491,7 @@ local: "本地" remote: "遠端" total: "合計" weekOverWeekChanges: "與上週相比" -dayOverDayChanges: "與昨日相比" +dayOverDayChanges: "與前一日相比" appearance: "外觀" clientSettings: "客戶端設定" accountSettings: "帳戶設定" @@ -553,51 +500,45 @@ promote: "推廣" numberOfDays: "有效天數" hideThisNote: "隱藏此貼文" showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦" -objectStorage: "物件儲存" -useObjectStorage: "使用物件儲存" +objectStorage: "Object Storage (物件儲存)" +useObjectStorage: "使用Object Storage" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "用於引用的 URL。如果您使用的是 CDN 或反向代理,請指定其 URL,例如 S3(https://.s3.amazonaws.com)、GCS(https://storage.googleapis.com/)。" +objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理,请指定其URL,例如S3:“https://.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/”" objectStorageBucket: "儲存空間(Bucket)" -objectStorageBucketDesc: "請填寫所用服務的儲存桶(Bucket)名稱。 " +objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 " objectStoragePrefix: "前綴" -objectStoragePrefixDesc: "它儲存在此前綴目錄下。" +objectStoragePrefixDesc: "它存儲在此前綴目錄下。" objectStorageEndpoint: "端點(Endpoint)" -objectStorageEndpointDesc: "如使用 AWS S3,請留空。如使用其他服務,請按照其說明文件以「」或「:」的形式設定端點(Endpoint)。" -objectStorageRegion: "區域(Region)" -objectStorageRegionDesc: "請填寫一個分區,例如「xx-east-1」。 如果您使用的服務不設分區,請留空或填寫「us-east-1」。" -objectStorageUseSSL: "使用 SSL" -objectStorageUseSSLDesc: "請在不使用 https 連接 API 時關閉" +objectStorageEndpointDesc: "如要使用AWS S3,請留空。否則請依照你使用的服務商的說明書進行設定,以''或 ':'的形式設定端點(Endpoint)。" +objectStorageRegion: "地域(Region)" +objectStorageRegionDesc: "指定一個分區,例如“xx-east-1”。 如果您使用的服務沒有分區的概念,請留空或填寫“us-east-1”。" +objectStorageUseSSL: "使用SSL" +objectStorageUseSSLDesc: "如果不使用https進行API連接,請關閉" objectStorageUseProxy: "使用網路代理" -objectStorageUseProxyDesc: "請在不使用網路代理連接 API 時關閉" -objectStorageSetPublicRead: "上傳時設定為「public-read」" -s3ForcePathStyleDesc: "啟用 s3ForcePathStyle 將強制填寫儲存空間(Bucket)名稱至 URL 路徑內,而非寫入主機名。 使用如 Minio 等自行託管服務時可能需要啟用。" +objectStorageUseProxyDesc: "如果不使用代理進行API連接,請關閉" +objectStorageSetPublicRead: "上傳時設定為\"public-read\"" +s3ForcePathStyleDesc: "啟用 s3ForcePathStyle 會強制將儲存槽名稱指定為 URL 中路徑的一部分,而不是主機名。 使用自託管 Minio 之類的可能需要啟用。" serverLogs: "伺服器日誌" deleteAll: "刪除所有記錄" showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框" showFixedPostFormInChannel: "於時間軸頁頂顯示「發送貼文」方框(頻道)" -withRepliesByDefaultForNewlyFollowed: "在追隨其他人後,預設在時間軸納入回覆的貼文" -newNoteRecived: "發現新貼文" -newNote: "新的貼文" +newNoteRecived: "發現新的貼文" sounds: "音效" sound: "音效" -notificationSoundSettings: "設定通知音效" listen: "聆聽" none: "無" showInPage: "在頁面中顯示" -popout: "彈出式視窗" +popout: "彈出型窗口" volume: "音量" masterVolume: "主音量" -notUseSound: "關閉音效" -useSoundOnlyWhenActive: "僅在 Misskey 於前景運作時發出音效" details: "詳細資訊" -renoteDetails: "轉發貼文的細節" chooseEmoji: "選擇您的表情符號" unableToProcess: "操作無法完成" recentUsed: "最近使用" install: "安裝" uninstall: "解除安裝" installedApps: "已授權的應用程式" -nothing: "查無項目" +nothing: "未發現" installedDate: "安裝時間" lastUsedDate: "最後上線日期" state: "狀態" @@ -605,59 +546,53 @@ sort: "排序" ascendingOrder: "昇冪" descendingOrder: "降冪" scratchpad: "暫存記憶體" -scratchpadDescription: "AiScript 控制臺為 AiScript 的實驗環境。您可以在此編寫、執行和確認程式碼與 Misskey 互動的結果。" -uiInspector: "UI 檢查" -uiInspectorDescription: "您可以看到記憶體中存在的 UI 元件實例的清單。 UI 元件由 Ui:C: 系列函數產生。" +scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。" output: "輸出" script: "腳本" -disablePagesScript: "停用頁面的 AiScript 腳本" +disablePagesScript: "停用頁面的AiScript腳本" updateRemoteUser: "更新遠端使用者資訊" -unsetUserAvatar: "移除使用者的大頭貼" -unsetUserAvatarConfirm: "確定要移除使用者的大頭貼嗎?" -unsetUserBanner: "移除使用者的橫幅圖像" -unsetUserBannerConfirm: "確定要移除使用者的橫幅圖像嗎?" deleteAllFiles: "刪除所有檔案" -deleteAllFilesConfirm: "要刪除所有檔案嗎?" +deleteAllFilesConfirm: "要删除所有檔案嗎?" removeAllFollowing: "解除所有追隨" removeAllFollowingDescription: "解除{host}所有的追隨。在伺服器不再存在時執行。" -userSuspended: "該使用者已被停用。" -userSilenced: "該使用者已被禁言。" +userSuspended: "該使用者已被停用" +userSilenced: "該用戶已被禁言。" yourAccountSuspendedTitle: "帳戶已被凍結" -yourAccountSuspendedDescription: "該帳戶已因違反伺服器服務條款或其他原因而被凍結。您可以向管理員查詢更多資訊。請不要建立新帳戶。" +yourAccountSuspendedDescription: "由於違反了伺服器的服務條款或其他原因,該帳戶已被凍結。 您可以與管理員連繫以了解更多訊息。 請不要創建一個新的帳戶。" tokenRevoked: "權杖無效" tokenRevokedDescription: "登入權杖失效,請重新登入。" accountDeleted: "帳戶已被刪除" accountDeletedDescription: "這個帳戶已被刪除。" menu: "選單" -divider: "分隔線" +divider: "分割線" addItem: "新增項目" rearrange: "排序方式" -relays: "中繼器" -addRelay: "新增中繼器" -inboxUrl: "收件夾 URL" -addedRelays: "已加入的中繼器" -serviceworkerInfo: "如要使用推播通知,需要啟用此選項並設定金鑰。" -deletedNote: "已刪除的貼文" -invisibleNote: "私人貼文" +relays: "中繼" +addRelay: "新增中繼" +inboxUrl: "收件夾URL" +addedRelays: "已加入的中繼" +serviceworkerInfo: "您需要啟用推送通知" +deletedNote: "已删除的貼文" +invisibleNote: "私密的貼文" enableInfiniteScroll: "啟用自動滾動頁面模式" visibility: "可見性" -poll: "票選活動" +poll: "投票" useCw: "隱藏內容" -enablePlayer: "開啟播放器" +enablePlayer: "打開播放器" disablePlayer: "關閉播放器" expandTweet: "展開推文" -themeEditor: "佈景主題編輯器" +themeEditor: "主題編輯器" description: "描述" -describeFile: "新增標題" -enterFileDescription: "輸入標題" +describeFile: "添加標題 " +enterFileDescription: "輸入標題 " author: "作者" -leaveConfirm: "尚未儲存修改。要放棄嗎?" +leaveConfirm: "有未保存的更改。要放棄嗎?" manage: "管理" plugins: "外掛" preferencesBackups: "備份設定檔" deck: "多欄模式" undeck: "取消多欄模式" -useBlurEffectForModal: "在對話框使用模糊效果" +useBlurEffectForModal: "在模態框使用模糊效果" useFullReactionPicker: "使用全尺寸的反應選擇器" width: "寬度" height: "高度" @@ -666,41 +601,34 @@ medium: "中" small: "小" generateAccessToken: "發行存取權杖" permission: "權限" -adminPermission: "管理員權限" enableAll: "啟用全部" disableAll: "停用全部" tokenRequested: "允許存取帳戶" pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。" notificationType: "通知形式" edit: "編輯" -emailServer: "電子郵件伺服器" -enableEmail: "啟用發送電子郵件功能" -emailConfigInfo: "用於確認電子郵件地址及密碼重置" +emailServer: "電郵伺服器" +enableEmail: "啟用發送電郵功能" +emailConfigInfo: "用於確認電郵地址及密碼重置" email: "電子郵件" -emailAddress: "電子郵件位址" -smtpConfig: "SMTP 伺服器設定" +emailAddress: "電郵地址" +smtpConfig: "SMTP伺服器設定" smtpHost: "主機" smtpPort: "埠" smtpUser: "使用者名稱" smtpPass: "密碼" -emptyToDisableSmtpAuth: "將使用者名稱和密碼留空以關閉 SMTP 驗證。" +emptyToDisableSmtpAuth: "留空使用者名稱和密碼以關閉SMTP驗證。" smtpSecure: "在 SMTP 連接中使用隱式 SSL/TLS" -smtpSecureInfo: "使用 STARTTLS 時關閉。" +smtpSecureInfo: "使用STARTTLS時關閉。" testEmail: "測試郵件發送" wordMute: "被靜音的文字" -wordMuteDescription: "將包含指定語句的貼文最小化。 點擊最小化的貼文即可顯示。" -hardWordMute: "硬文字靜音" -showMutedWord: "顯示靜音字" -hardWordMuteDescription: "隱藏含有指定語句的貼文。 與詞彙靜音不同的是,貼文將完全隱藏不見。" regexpError: "正規表達式錯誤" regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:" instanceMute: "被靜音的實例" userSaysSomething: "{name}說了什麼" -userSaysSomethingAbout: "{name} 說了一些關於「{word}」的話" makeActive: "啟用" display: "檢視" copy: "複製" -copiedToClipboard: "已複製到剪貼簿" metrics: "指標" overview: "概覽" logs: "日誌" @@ -714,31 +642,32 @@ useGlobalSetting: "使用全域設定" useGlobalSettingDesc: "啟用時,將使用帳戶通知設定。停用時,則可以單獨設定。" other: "其他" regenerateLoginToken: "重新產生登入權杖" -regenerateLoginTokenDescription: "重新產生用於登入的內部權杖。通常不需要使用此功能。重新產生後,所有裝置都將被登出。" -theKeywordWhenSearchingForCustomEmoji: "這是搜尋自訂表情符號時的關鍵字" +regenerateLoginTokenDescription: "重新產生用於登入的內部權杖。一般情況下是不需要這樣做的。一旦重產,所有裝置將會被登出。" setMultipleBySeparatingWithSpace: "您可以使用空格分隔多個項目。" -fileIdOrUrl: "檔案 ID 或 URL" +fileIdOrUrl: "檔案ID或URL" behavior: "行為" sample: "範例" abuseReports: "檢舉" reportAbuse: "檢舉" -reportAbuseRenote: "檢舉轉發貼文" reportAbuseOf: "檢舉{name}" -fillAbuseReportDescription: "請填寫檢舉的詳細理由。如有需要,請附上相關 URL。" -abuseReported: "檢舉完成。感謝您的報告。" +fillAbuseReportDescription: "請填寫檢舉的詳細理由。可以的話,請附上針對的URL網址。" +abuseReported: "回報已送出。感謝您的報告。" reporter: "檢舉者" reporteeOrigin: "檢舉來源" reporterOrigin: "檢舉者來源" +forwardReport: "將報告轉送給遠端實例" +forwardReportIsAnonymous: "在遠端實例上看不到您的資訊,顯示的報告者是匿名的系统帳戶。" send: "發送" +abuseMarkAsResolved: "處理完畢" openInNewTab: "在新分頁中開啟" openInSideView: "在側欄中開啟" -defaultNavigationBehaviour: "預設導航" +defaultNavigationBehaviour: "默認導航" editTheseSettingsMayBreakAccount: "修改這些設定可能會毀損您的帳戶" -instanceTicker: "貼文的伺服器資訊" +instanceTicker: "貼文的實例來源" waitingFor: "等待{x}" random: "隨機" system: "系統" -switchUi: "切換介面" +switchUi: "切換界面" desktop: "桌面" clip: "摘錄" createNew: "新建" @@ -747,8 +676,7 @@ createNewClip: "建立新摘錄" unclip: "解除摘錄" confirmToUnclipAlreadyClippedNote: "此貼文已包含在摘錄「{name}」中。 你想將貼文從這個摘錄中排除嗎?" public: "公開" -private: "私密" -i18nInfo: "Misskey 已被志願者們翻譯成各種語言版本。您可以前往 {link} 以協助翻譯。" +i18nInfo: "Misskey已經被志願者們翻譯成各種語言版本,如果想要幫忙的話,可以進入{link}幫助翻譯。" manageAccessTokens: "管理存取權杖" accountInfo: "帳戶資訊" notesCount: "貼文數量" @@ -756,7 +684,7 @@ repliesCount: "回覆數量" renotesCount: "轉發數量" repliedCount: "回覆數量" renotedCount: "轉發次數" -followingCount: "正在追隨的使用者數量" +followingCount: "正在追隨的用戶數量" followersCount: "追隨者數量" sentReactionsCount: "反應發送次數" receivedReactionsCount: "收到反應次數" @@ -768,14 +696,13 @@ driveFilesCount: "雲端硬碟檔案數量" driveUsage: "雲端硬碟使用量" noCrawle: "拒絕搜尋引擎索引" noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。" -lockedAccountInfo: "即使追隨需要核准,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。" -alwaysMarkSensitive: "預設標記檔案為敏感內容" +lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。" +alwaysMarkSensitive: "默認將圖像/影像標記為敏感內容" loadRawImages: "以原始圖檔顯示附件圖檔的縮圖" disableShowingAnimatedImages: "不播放動態圖檔" -highlightSensitiveMedia: "強調敏感標記" -verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的連結以完成驗證。" +verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的鏈接完成驗證。" notSet: "未設定" -emailVerified: "已成功驗證您的電子郵件地址" +emailVerified: "已成功驗證您的電郵" noteFavoritesCount: "我的最愛貼文的數目" pageLikesCount: "頁面被按讚次數" pageLikedCount: "頁面被按讚次數" @@ -784,10 +711,11 @@ useSystemFont: "使用系統預設的字型" clips: "摘錄" experimentalFeatures: "實驗中的功能" experimental: "實驗性" -thisIsExperimentalFeature: "這是一項實驗性功能,其行為會隨需要進行調整,也可能無法正常運作。" +thisIsExperimentalFeature: "這是實驗性的功能。可能會有變更規格和不能正常動作的可能性。" developer: "開發者" -makeExplorable: "使自己的帳戶更容易被找到" +makeExplorable: "使自己的帳戶能夠在「探索」頁面中顯示" makeExplorableDescription: "如果關閉,帳戶將不會被顯示在「探索」頁面中。" +showGapBetweenNotesInTimeline: "分開顯示時間線上的貼文。" duplicate: "複製" left: "左" center: "置中" @@ -795,19 +723,18 @@ wide: "寬" narrow: "窄" reloadToApplySetting: "設定將會在頁面重新載入之後生效。要現在就重載頁面嗎?" needReloadToApply: "必須重新載入才會生效。" -needToRestartServerToApply: "必須重新啟動伺服器才會使變更生效。" showTitlebar: "顯示標題列" clearCache: "清除快取資料" -onlineUsersCount: "{n} 人上線" -nUsers: "{n} 使用者" -nNotes: "{n} 貼文" +onlineUsersCount: "{n}人正在線上" +nUsers: "{n}用戶" +nNotes: "{n}貼文" sendErrorReports: "傳送錯誤報告" -sendErrorReportsDescription: "傳送問題報告至開發者以提升軟體品質。問題報告可能包括作業系統版本,瀏覽器類型,行為歷史記錄等。" +sendErrorReportsDescription: "啟用後,問題報告將傳送至開發者以提升軟體品質。問題報告可能包括OS版本,瀏覽器類型,行為歷史記錄等。" myTheme: "我的佈景主題" backgroundColor: "背景" accentColor: "重點色彩" textColor: "文字" -saveAs: "另存新檔" +saveAs: "另存為..." advanced: "進階" advancedSettings: "進階設定" value: "數值" @@ -825,37 +752,37 @@ newVersionOfClientAvailable: "新版本的客戶端可用。" usageAmount: "使用量" capacity: "容量" inUse: "已使用" -editCode: "編輯程式碼" +editCode: "編輯代碼" apply: "套用" -receiveAnnouncementFromInstance: "接收來自伺服器的通知" +receiveAnnouncementFromInstance: "接收由本實例發出的電郵通知" emailNotification: "郵件通知" publish: "發布" -inChannelSearch: "頻道內搜尋" -useReactionPickerForContextMenu: "點擊右鍵開啟反應選擇器" -typingUsers: "{users}輸入中" +inChannelSearch: "頻道内搜尋" +useReactionPickerForContextMenu: "點擊右鍵開啟反應工具欄" +typingUsers: "{users}輸入中..." jumpToSpecifiedDate: "跳轉到特定日期" -showingPastTimeline: "顯示過往的時間軸" +showingPastTimeline: "顯示過往的時間線" clear: "清除" markAllAsRead: "全部標示為已讀" goBack: "返回" unlikeConfirm: "要取消按讚嗎?" -fullView: "全螢幕顯示" -quitFullView: "退出全螢幕顯示" -addDescription: "新增描述" -userPagePinTip: "在貼文的選單中選擇「置頂」,即可置頂該貼文至您的個人檔案頁面。" +fullView: "全熒幕顯示" +quitFullView: "退出全熒幕顯示" +addDescription: "添加描述" +userPagePinTip: "在貼文的選單中選擇\"置頂\",即可置頂該貼文至您的個人檔案頁面。" notSpecifiedMentionWarning: "此貼文有未指定的提及" info: "資訊" -userInfo: "使用者資訊" +userInfo: "用戶資料" unknown: "未知" -onlineStatus: "上線狀態" -hideOnlineStatus: "隱藏上線狀態" -hideOnlineStatusDescription: "隱藏上線狀態後,可能會降低搜尋等功能的便利性。" +onlineStatus: "在線狀態" +hideOnlineStatus: "隱藏在線狀態" +hideOnlineStatusDescription: "隱藏在線狀態後,可能會降低檢索等功能的便利性。" online: "線上" active: "最近活躍" offline: "離線" notRecommended: "不推薦" -botProtection: "Bot 防護" -instanceBlocking: "已封鎖或禁言的伺服器" +botProtection: "Bot防護" +instanceBlocking: "已封鎖的實例" selectAccount: "選擇帳戶" switchAccount: "切換帳戶" enabled: "已啟用" @@ -865,12 +792,11 @@ user: "使用者" administration: "管理" accounts: "帳戶" switch: "切換" -noMaintainerInformationWarning: "尚未設定管理員訊息。" -noInquiryUrlWarning: "尚未設定聯絡表單網址。" -noBotProtectionWarning: "尚未設定 Bot 防護。" +noMaintainerInformationWarning: "尚未設定管理員信息。" +noBotProtectionWarning: "尚未設定Bot防護。" configure: "設定" postToGallery: "發佈到相簿" -postToHashtag: "以此主題標籤發佈" +postToHashtag: "以此主題標籤發布" gallery: "相簿" recentPosts: "最新貼文" popularPosts: "熱門的貼文" @@ -887,9 +813,9 @@ emailNotConfiguredWarning: "沒有設定電子郵件地址" ratio: "%" previewNoteText: "預覽文本" customCss: "自定義 CSS" -customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能導致客戶端無法正常使用。" +customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能导致客戶端無法正常使用。" global: "全域" -squareAvatars: "大頭貼以方形顯示" +squareAvatars: "頭像以方形顯示" sent: "發送" received: "收取" searchResult: "搜尋結果" @@ -905,7 +831,7 @@ accountDeletionInProgress: "正在刪除帳戶" usernameInfo: "在伺服器上您的帳戶是唯一的識別名稱。您可以使用字母 (a ~ z, A ~ Z)、數字 (0 ~ 9) 和下底線 (_)。之後帳戶名是不能更改的。" aiChanMode: "小藍模式" devMode: "開發者模式" -keepCw: "保持隱藏內容" +keepCw: "保持CW" pubSub: "Pub/Sub 帳戶" lastCommunication: "最近的通信" resolved: "已解決" @@ -919,46 +845,42 @@ off: "關閉" emailRequiredForSignup: "註冊帳戶需要電子郵件地址" unread: "未讀" filter: "篩選" -controlPanel: "控制臺" +controlPanel: "控制台" manageAccounts: "管理帳戶" makeReactionsPublic: "將反應設為公開" makeReactionsPublicDescription: "將您做過的反應設為公開可見。" classic: "經典" muteThread: "將貼文串設為靜音" unmuteThread: "將貼文串的靜音解除" -followingVisibility: "追隨中的可見性" -followersVisibility: "追隨者的可見性" +ffVisibility: "連繫的可見性" +ffVisibilityDescription: "您可以設定您的關注/關注者資訊的公開範圍" continueThread: "查看更多貼文" deleteAccountConfirm: "將要刪除帳戶。是否確定?" incorrectPassword: "密碼錯誤。" -incorrectTotp: "一次性密碼錯誤,或者已過期。" voteConfirm: "確定投給「{choice}」?" hide: "隱藏" -useDrawerReactionPickerForMobile: "在行動裝置上使用抽屜顯示" +useDrawerReactionPickerForMobile: "在移動設備上使用抽屜顯示" welcomeBackWithName: "歡迎回來,{name}" clickToFinishEmailVerification: "點擊 [{ok}] 完成電子郵件地址認證。" overridedDeviceKind: "裝置類型" smartphone: "智慧型手機" tablet: "平板" auto: "自動" -themeColor: "佈景主題顏色" +themeColor: "主題顏色" size: "大小" numberOfColumn: "列數" searchByGoogle: "搜尋" -instanceDefaultLightTheme: "實例預設的淺色佈景主題" -instanceDefaultDarkTheme: "實例預設的深色佈景主題" -instanceDefaultThemeDescription: "輸入物件形式的佈景主題代碼" +instanceDefaultLightTheme: "實例預設的淺色主題" +instanceDefaultDarkTheme: "實例預設的深色主題" +instanceDefaultThemeDescription: "輸入物件形式的主题代碼" mutePeriod: "靜音的期限" period: "期限" indefinitely: "無期限" -tenMinutes: "十分鐘" -oneHour: "一小時" -oneDay: "一天" -oneWeek: "一週" -oneMonth: "一個月" -threeMonths: "3 個月" -oneYear: "1 年" -threeDays: "3 日" +tenMinutes: "10分鐘" +oneHour: "1小時" +oneDay: "1天" +oneWeek: "1週" +oneMonth: "1個月" reflectMayTakeTime: "可能需要一些時間才會出現效果。" failedToFetchAccountInformation: "取得帳戶資訊失敗" rateLimitExceeded: "已超過速率限制" @@ -967,14 +889,14 @@ cropImageAsk: "要剪裁圖片嗎?" cropYes: "裁剪" cropNo: "使用原圖" file: "檔案" -recentNHours: "過去 {n} 小時" -recentNDays: "過去 {n} 天" +recentNHours: "過去{n}小時" +recentNDays: "過去{n}天" noEmailServerWarning: "尚未設定電子郵件伺服器。" thereIsUnresolvedAbuseReportWarning: "有尚未處理的檢舉。" recommended: "推薦" check: "檢查" driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限" -driveCapOverrideCaption: "如果指定 0 以下的值,就會被取消。" +driveCapOverrideCaption: "如果指定0以下的值,就會被取消。" requireAdminForView: "必須以管理員帳戶登入才可以檢視。" isSystemAccount: "由系統自動建立與管理的帳戶。" typeToConfirm: "要執行這項操作,請輸入 {x} " @@ -983,7 +905,6 @@ document: "文件" numberOfPageCache: "快取頁面數" numberOfPageCacheDescription: "增加數量會提高便利性,但也會增加負荷與記憶體使用量。" logoutConfirm: "確定要登出嗎?" -logoutWillClearClientData: "當您登出時,客戶端的設定資訊將從瀏覽器中清除。為了能夠在重新登入時恢復您的設定資訊,請啟用設定內的自動備份選項。" lastActiveDate: "上次使用日期及時間" statusbar: "狀態列" pleaseSelect: "請選擇" @@ -995,29 +916,28 @@ type: "類型" speed: "速度" slow: "慢" fast: "快" -sensitiveMediaDetection: "敏感檔案的檢測" +sensitiveMediaDetection: "敏感性媒體的檢測" localOnly: "僅限本地" remoteOnly: "僅限遠端" failedToUpload: "上傳失敗" cannotUploadBecauseInappropriate: "由於判定可能包含不適當的內容,因此無法上傳。" cannotUploadBecauseNoFreeSpace: "由於雲端硬碟沒有可用空間,因此無法上傳。" cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制,無法上傳。" -cannotUploadBecauseUnallowedFileType: "由於檔案類型不被允許,無法上傳。\n" -beta: "測試版" -enableAutoSensitive: "自動 NSFW 判定" -enableAutoSensitiveDescription: "如果可行,它將使用機器學習技術判斷檔案是否需要標記為敏感。即使關閉此功能,也可能會依伺服器規則而自動啟用。" -activeEmailValidationDescription: "主動地驗證使用者的電子郵件地址,以確定是否是一次性地址以及是否可以真正與其進行通訊。關閉時,僅檢查格式是否正確。" +beta: "Beta" +enableAutoSensitive: "自動NSFW判定" +enableAutoSensitiveDescription: "如果可用,請利用機器學習在媒體上自動設置 NSFW 旗標。 即使關閉此功能,依實例而定也可能會自動設置。" +activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址,判斷它是否為免洗地址,或者它是否可以通信。 若關閉,則只會檢查字元是否正確。" navbar: "導覽列" shuffle: "隨機" account: "帳戶" move: "移動 " pushNotification: "推播通知" subscribePushNotification: "啟用推播通知" -unsubscribePushNotification: "停用推播通知" +unsubscribePushNotification: "停止推播通知" pushNotificationAlreadySubscribed: "推播通知啟用中" -pushNotificationNotSupported: "瀏覽器或伺服器不支援推播通知" -sendPushNotificationReadMessage: "如果已閱讀通知與訊息,就刪除推播通知" -sendPushNotificationReadMessageCaption: "可能會導致裝置的電池消耗量增加。" +pushNotificationNotSupported: "瀏覽器或實例不支援推播通知" +sendPushNotificationReadMessage: "通知與訊息如果已讀的話,就將推播通知刪除" +sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通知將立刻顯示。可能會增加設備的電池消耗。" windowMaximize: "最大化" windowMinimize: "最小化" windowRestore: "復原" @@ -1032,9 +952,8 @@ numberOfLikes: "讚數" show: "檢視" neverShow: "不再顯示" remindMeLater: "以後再說" -didYouLikeMisskey: "您喜歡 Misskey 嗎?" -pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發的工作能夠持續!" -correspondingSourceIsAvailable: "對應的原始碼可以在 {anchor} 處找到。" +didYouLikeMisskey: "您是否喜愛Misskey呢?" +pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發能夠持續!" roles: "角色" role: "角色" noRole: "沒有角色" @@ -1044,27 +963,25 @@ assign: "指派" unassign: "取消指派" color: "顏色" manageCustomEmojis: "管理自訂表情符號" -manageAvatarDecorations: "管理頭像裝飾" youCannotCreateAnymore: "您無法再建立更多了。" cannotPerformTemporary: "暫時無法進行" -cannotPerformTemporaryDescription: "由於超過操作次數限制,因此暫時無法進行。請稍後再嘗試。" +cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無法進行。請過一段時間之後再嘗試。" invalidParamError: "參數錯誤" -invalidParamErrorDescription: "請求參數有問題。這可能是漏洞或輸入過多字元所致。" +invalidParamErrorDescription: "請求參數有問題。通常是bug造成的,但也有輸入的字元數過多之類的可能性。" permissionDeniedError: "操作被拒絕" -permissionDeniedErrorDescription: "此帳戶沒有執行這個操作的權限。" +permissionDeniedErrorDescription: "本帳號沒有執行這個操作的權限。" preset: "預設值" selectFromPresets: "從預設值中選擇" achievements: "成就" gotInvalidResponseError: "伺服器的回應無效" gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。" thisPostMayBeAnnoying: "這篇貼文可能會造成別人的困擾。" -thisPostMayBeAnnoyingHome: "發佈到首頁" +thisPostMayBeAnnoyingHome: "發布到首頁" thisPostMayBeAnnoyingCancel: "退出" -thisPostMayBeAnnoyingIgnore: "直接發佈貼文" +thisPostMayBeAnnoyingIgnore: "直接發布貼文" collapseRenotes: "省略顯示已看過的轉發貼文" -collapseRenotesDescription: "將已做過反應和轉發的貼文折疊顯示。" internalServerError: "內部伺服器錯誤" -internalServerErrorDescription: "內部伺服器出現意外錯誤。" +internalServerErrorDescription: "內部伺服器發生了非預期的錯誤。" copyErrorInfo: "複製錯誤資訊" joinThisServer: "在此伺服器上註冊" exploreOtherServers: "探索其他伺服器" @@ -1074,7 +991,7 @@ disableFederationConfirmWarn: "即使停止了聯邦功能,貼文也不會變 disableFederationOk: "停止聯邦功能" invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。" emailNotSupported: "這個伺服器不支援寄送郵件" -postToTheChannel: "發佈到頻道" +postToTheChannel: "發布到頻道" cannotBeChangedLater: "之後不能變更。" reactionAcceptance: "接受表情反應" likeOnly: "僅限讚" @@ -1085,12 +1002,7 @@ rolesAssignedToMe: "指派給自己的角色" resetPasswordConfirm: "重設密碼?" sensitiveWords: "敏感詞" sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。" -sensitiveWordsDescription2: "空格代表「以及」(AND),斜線包圍關鍵字代表使用正規表達式。" -prohibitedWords: "禁語" -prohibitedWordsDescription: "當要發布包含禁語的貼文時,會出現錯誤。可以用換行分隔來設定多個禁語。" -prohibitedWordsDescription2: "空格代表「以及」(AND),斜線包圍關鍵字代表使用正規表達式。" -hiddenTags: "隱藏標籤" -hiddenTagsDescription: "設定的標籤不會在趨勢中顯示,換行可以設定多個標籤。" +sensitiveWordsDescription2: "用空格分隔關鍵詞構成AND格式,用斜線包圍關鍵字構成正規表達式。" notesSearchNotAvailable: "無法使用搜尋貼文功能。" license: "授權" unfavoriteConfirm: "要取消收錄我的最愛嗎?" @@ -1099,17 +1011,13 @@ drivecleaner: "雲端硬碟清掃器" retryAllQueuesNow: "立刻重試所有佇列" retryAllQueuesConfirmTitle: "要現在重試嗎?" retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。" -enableChartsForRemoteUser: "生成遠端使用者的圖表" +enableChartsForRemoteUser: "生成遠端用戶的圖表" enableChartsForFederatedInstances: "生成遠端伺服器的圖表" -enableStatsForFederatedInstances: "取得遠端伺服器資訊" -showClipButtonInNoteFooter: "新增摘錄按鈕至貼文" -reactionsDisplaySize: "反應的顯示尺寸" -limitWidthOfReaction: "限制反應的最大寬度,並縮小顯示尺寸。" -noteIdOrUrl: "貼文 ID 或 URL" +showClipButtonInNoteFooter: "將摘錄添加至貼文" +largeNoteReactions: "將貼文的反應放大顯示" +noteIdOrUrl: "貼文ID或URL" video: "影片" videos: "影片" -audio: "音效" -audioFiles: "音效檔案" dataSaver: "數據節省模式" accountMigration: "遷移帳戶" accountMoved: "這個使用者已遷移至新的帳戶:" @@ -1120,413 +1028,48 @@ addMemo: "新增備註" editMemo: "編輯備註" reactionsList: "反應列表" renotesList: "轉發貼文列表" -notificationDisplay: "通知" +notificationDisplay: "通知的顯示" leftTop: "左上" rightTop: "右上" leftBottom: "左下" rightBottom: "右下" stackAxis: "堆疊方向" -vertical: "直向" -horizontal: "橫向" +vertical: "縱向" +horizontal: "側向" position: "位置" serverRules: "伺服器規則" -pleaseConfirmBelowBeforeSignup: "在本伺服器註冊之前,必須確認並同意以下內容。" +pleaseConfirmBelowBeforeSignup: "在本伺服器註冊之前,請確認下列事項。" pleaseAgreeAllToContinue: "必須全部勾選「同意」才能繼續。" continue: "繼續" preservedUsernames: "保留的使用者名稱" -preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處出現的名稱將在註冊時禁用,但由管理者建立帳戶則不受此限。此外,既有的帳戶也不受影響。" +preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處指定的使用者名稱,在建立帳戶時無法使用,但由管理者所建立的帳戶不受此限。此外,既有的帳戶也不受影響。" createNoteFromTheFile: "由此檔案建立貼文" archive: "封存" -archived: "已封存" -unarchive: "取消封存" channelArchiveConfirmTitle: "要封存{name}嗎?" -channelArchiveConfirmDescription: "封存後,將不會在頻道列表與搜尋結果中顯示,也無法發佈新貼文。" +channelArchiveConfirmDescription: "封存以後,在頻道列表與搜索結果中不會顯示,也無法發布新的貼文。" thisChannelArchived: "這個頻道已被封存。" displayOfNote: "顯示貼文" initialAccountSetting: "初始設定" youFollowing: "追隨中" preventAiLearning: "拒絕接受生成式AI的訓練" -preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤,而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。" +preventAiLearningDescription: "要求外部的文章生成式AI或圖像生成式AI不以發布的貼文和圖像等內容為學習對象。這是透過在HTML響應中包含noai旗標來實現的,但不能完全防止AI的學習,因為這要看該AI是否遵守這個要求。" options: "選項" specifyUser: "指定使用者" -lookupConfirm: "要查詢嗎?" -openTagPageConfirm: "要開啟標籤的頁面嗎?" -specifyHost: "指定主機" failedToPreviewUrl: "無法預覽" update: "更新" -rolesThatCanBeUsedThisEmojiAsReaction: "可以使用此表情符號為反應的角色" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "如沒有指定角色,任何人都可使用此表情回應。" -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "必須為公開角色。" -cancelReactionConfirm: "要取消此反應嗎?" -changeReactionConfirm: "要更改反應嗎?" +rolesThatCanBeUsedThisEmojiAsReaction: "可以用這個做為反應的角色" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "未指定角色的情況,則任何人都可以將它用做反應。" +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必須是公開的角色。" +cancelReactionConfirm: "要取消做出的反應嗎?" +changeReactionConfirm: "要變更做出的反應嗎?" later: "稍後再說" -goToMisskey: "往 Misskey" +goToMisskey: "往Misskey" additionalEmojiDictionary: "表情符號的附加辭典" installed: "已安裝" branding: "品牌宣傳" -enableServerMachineStats: "公佈伺服器的機器資訊" -enableIdenticonGeneration: "啟用生成使用者的 Identicon " +enableServerMachineStats: "公布伺服器的機器資訊" +enableIdenticonGeneration: "啟用每個使用者的Identicon" turnOffToImprovePerformance: "關閉時會提高性能。" -createInviteCode: "建立邀請碼" -createWithOptions: "使用選項建立" -createCount: "建立數" -inviteCodeCreated: "已建立邀請碼" -inviteLimitExceeded: "可建立的邀請碼已達上限。" -createLimitRemaining: "可建立的邀請碼:剩餘 {limit} 個" -inviteLimitResetCycle: "可以在 {time} 內建立最多 {limit} 個邀請碼。" -expirationDate: "有效日期" -noExpirationDate: "不設有效日期" -inviteCodeUsedAt: "使用邀請碼的日期和時間" -registeredUserUsingInviteCode: "用了邀請碼的使用者" -waitingForMailAuth: "等待電子郵件認證" -inviteCodeCreator: "建立了邀請碼的使用者" -usedAt: "使用的日期和時間" -unused: "未使用" -used: "已使用" -expired: "過期" -doYouAgree: "你同意嗎?" -beSureToReadThisAsItIsImportant: "重要,請務必閱讀。" -iHaveReadXCarefullyAndAgree: "我已仔細閱讀並同意「{x}」的內容。" -dialog: "對話方塊" -icon: "圖示" -forYou: "給您" -currentAnnouncements: "最新公告" -pastAnnouncements: "歷史公告" -youHaveUnreadAnnouncements: "有未讀的公告。" -useSecurityKey: "請按照瀏覽器或裝置上的說明來使用安全金鑰或通行金鑰。" -replies: "回覆" -renotes: "轉發" -loadReplies: "閱覽回覆" -loadConversation: "閱覽對話" -pinnedList: "已置頂的清單" -keepScreenOn: "保持裝置螢幕開啟" -verifiedLink: "已驗證連結" -notifyNotes: "開啟貼文通知" -unnotifyNotes: "關閉貼文通知" -authentication: "驗證" -authenticationRequiredToContinue: "請於繼續前完成驗證" -dateAndTime: "日期與時間" -showRenotes: "顯示其他人的轉發貼文" -edited: "已編輯" -notificationRecieveConfig: "接受通知的設定" -mutualFollow: "互相追隨" -followingOrFollower: "追隨中或追隨者" -fileAttachedOnly: "只顯示包含附件的貼文" -showRepliesToOthersInTimeline: "在時間軸上顯示給其他人的回覆" -hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆" -showRepliesToOthersInTimelineAll: "在時間軸包含追隨中所有人的回覆" -hideRepliesToOthersInTimelineAll: "在時間軸不包含追隨中所有人的回覆" -confirmShowRepliesAll: "進行此操作後無法復原。您真的希望時間軸「包含」您目前追隨的所有人的回覆嗎?" -confirmHideRepliesAll: "進行此操作後無法復原。您真的希望時間軸「不包含」您目前追隨的所有人的回覆嗎?" -externalServices: "外部服務" -sourceCode: "原始碼" -sourceCodeIsNotYetProvided: "尚未提供原始碼,請洽詢管理員解決這個問題。" -repositoryUrl: "儲存庫 URL" -repositoryUrlDescription: "如果存在可公開取得原始碼的儲存庫,請輸入其 URL。 如果您按原樣使用 Misskey(不對原始碼進行任何更改),請輸入 https://github.com/misskey-dev/misskey。" -repositoryUrlOrTarballRequired: "如果儲存庫不是公開的,則必須提供 tarball。 詳細資訊請參閱 .config/example.yml。" -feedback: "意見回饋" -feedbackUrl: "意見回饋 URL" -impressum: "營運者資訊" -impressumUrl: "營運者資訊 URL" -impressumDescription: "在德國與部份地區必須要明確顯示營運者資訊。" -privacyPolicy: "隱私政策" -privacyPolicyUrl: "隱私政策 URL" -tosAndPrivacyPolicy: "服務條款和隱私政策" -avatarDecorations: "頭像裝飾" -attach: "裝上" -detach: "取下" -detachAll: "全部移除" -angle: "角度" -flip: "翻轉" -showAvatarDecorations: "顯示頭像裝飾" -releaseToRefresh: "放開以更新內容" -refreshing: "載入更新中" -pullDownToRefresh: "往下拉來更新內容" -useGroupedNotifications: "分組顯示通知訊息" -signupPendingError: "驗證您的電子郵件地址時出現問題。連結可能已過期。" -cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。" -doReaction: "做出反應" -code: "程式碼" -reloadRequiredToApplySettings: "需要重新載入頁面設定才能生效。" -remainingN: "剩餘:{n}" -overwriteContentConfirm: "確定要覆蓋目前的內容嗎?" -seasonalScreenEffect: "隨季節變換畫面的呈現" -decorate: "裝飾" -addMfmFunction: "插入 MFM 功能語法" -enableQuickAddMfmFunction: "顯示進階 MFM 選擇器" -bubbleGame: "氣泡遊戲" -sfx: "音效" -soundWillBePlayed: "將播放音效" -showReplay: "觀看重播" -replay: "重播" -replaying: "重播中" -endReplay: "退出重播" -copyReplayData: "複製重播資料" -ranking: "排行榜" -lastNDays: "過去 {n} 天" -backToTitle: "回到遊戲標題頁" -hemisphere: "您居住的地區" -withSensitive: "顯示包含敏感檔案的貼文" -userSaysSomethingSensitive: "包含 {name} 敏感檔案的貼文" -enableHorizontalSwipe: "滑動切換時間軸" -loading: "載入中" -surrender: "退出" -gameRetry: "再試一次" -notUsePleaseLeaveBlank: "如果不使用的話請留白" -useTotp: "使用一次性密碼" -useBackupCode: "使用備用驗證碼" -launchApp: "啟動 APP" -useNativeUIForVideoAudioPlayer: "使用瀏覽器的 UI 播放影片與音訊" -keepOriginalFilename: "保留原始檔名" -keepOriginalFilenameDescription: "如果關閉此設定,上傳時檔案名稱會自動替換為隨機字串。" -noDescription: "沒有說明文字" -alwaysConfirmFollow: "追隨時總是確認" -inquiry: "聯絡我們" -tryAgain: "請再試一次。" -confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認" -sensitiveMediaRevealConfirm: "這是敏感媒體。確定要顯示嗎?" -createdLists: "已建立的清單" -createdAntennas: "已建立的天線" -fromX: "自 {x}" -genEmbedCode: "產生嵌入程式碼" -noteOfThisUser: "這個使用者的貼文列表" -clipNoteLimitExceeded: "沒辦法在這個摘錄中增加更多貼文了。" -performance: "性能" -modified: "已變更" -discard: "取消" -thereAreNChanges: "有 {n} 處的變更" -signinWithPasskey: "使用通行金鑰登入" -unknownWebAuthnKey: "未註冊的通行金鑰。" -passkeyVerificationFailed: "驗證通行金鑰失敗。" -passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證通行金鑰成功,但是無密碼登入的方式是停用的。" -messageToFollower: "給追隨者的訊息" -target: "目標 " -testCaptchaWarning: "此功能用於 CAPTCHA 的測試。請勿在正式環境中使用。" -prohibitedWordsForNameOfUser: "禁止使用的字詞(使用者名稱)" -prohibitedWordsForNameOfUserDescription: "如果使用者名稱包含此清單中的任何字串,則拒絕重新命名使用者。 具有審查員權限的使用者不受此限制的影響。" -yourNameContainsProhibitedWords: "您嘗試更改的名稱包含禁止的字串" -yourNameContainsProhibitedWordsDescription: "名稱中包含禁止使用的字串。 如果您想使用此名稱,請聯絡您的伺服器管理員。" -thisContentsAreMarkedAsSigninRequiredByAuthor: "作者將其設定為需要登入才能顯示。" -lockdown: "鎖定" -pleaseSelectAccount: "請選擇帳戶" -availableRoles: "可用角色" -acknowledgeNotesAndEnable: "了解注意事項後再開啟。" -federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。" -federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。" -confirmOnReact: "在做出反應前先確認" -reactAreYouSure: "用「 {emoji} 」反應嗎?" -markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?" -unmarkAsSensitiveConfirm: "要解除這個媒體的敏感設定嗎?" -preferences: "環境設定" -accessibility: "輔助工具" -preferencesProfile: "設定檔案" -copyPreferenceId: "複製設定 ID" -resetToDefaultValue: "還原成預設值" -overrideByAccount: "覆寫帳號" -untitled: "無標題" -noName: "沒有名稱" -skip: "跳過" -restore: "還原" -syncBetweenDevices: "裝置之間的同步化" -preferenceSyncConflictTitle: "伺服器上存在設定值" -preferenceSyncConflictText: "已啟用同步的設定項目會將設定值儲存至伺服器,並已找到該設定項目在伺服器上儲存的設定值。請選擇要使用哪個設定值進行覆寫。" -preferenceSyncConflictChoiceMerge: "合併至" -preferenceSyncConflictChoiceServer: "伺服器設定值" -preferenceSyncConflictChoiceDevice: "裝置的設定值" -preferenceSyncConflictChoiceCancel: "取消啟用同步" -paste: "貼上" -emojiPalette: "表情符號調色盤" -postForm: "發文視窗" -textCount: "字數" -information: "關於" -chat: "聊天" -migrateOldSettings: "遷移舊設定資訊" -migrateOldSettings_description: "通常情況下,這會自動進行,但若因某些原因未能順利遷移,您可以手動觸發遷移處理。請注意,當前的設定資訊將會被覆寫。" -compress: "壓縮" -right: "右" -bottom: "下" -top: "上" -embed: "嵌入" -settingsMigrating: "正在移轉設定。請稍候……(之後也可以到「設定 → 其他 → 舊設定資訊移轉」中手動進行移轉)" -readonly: "唯讀" -goToDeck: "回去甲板" -federationJobs: "聯邦通訊作業" -driveAboutTip: "在「雲端硬碟」中,會顯示過去上傳的檔案列表。
\n可以在附加到貼文時重新利用,或者事先上傳之後再用於發布。
\n請注意,刪除檔案後,之前使用過該檔案的所有地方(貼文、頁面、大頭貼、橫幅等)也會一併無法顯示。
\n也可以建立資料夾來整理檔案。" -scrollToClose: "用滾輪關閉" -advice: "建議" -realtimeMode: "即時模式" -turnItOn: "開啟" -turnItOff: "關閉" -emojiMute: "表情符號靜音" -emojiUnmute: "表情符號解除靜音" -muteX: "將 {x} 靜音" -unmuteX: "將 {x} 解除靜音" -abort: "取消" -tip: "提示與技巧" -redisplayAllTips: "重新顯示所有「提示與技巧」" -hideAllTips: "隱藏所有「提示與技巧」" -_chat: - noMessagesYet: "尚無訊息" - newMessage: "新訊息" - individualChat: "ㄧ對一聊天室" - individualChat_description: "可以與特定使用者進行一對一的聊天。" - roomChat: "多人聊天室" - roomChat_description: "可以進行多人聊天。\n此外,即使是未允許個人聊天的使用者,只要對方接受,也可以進行聊天。" - createRoom: "建立聊天室" - inviteUserToChat: "邀請使用者開始聊天" - yourRooms: "已建立的聊天室" - joiningRooms: "已加入的聊天室" - invitations: "邀請" - noInvitations: "沒有邀請" - history: "歷史紀錄" - noHistory: "沒有歷史紀錄" - noRooms: "沒有可用的聊天室" - inviteUser: "邀請使用者" - sentInvitations: "已傳送的邀請" - join: "加入" - ignore: "忽視" - leave: "退出聊天室" - members: "成員" - searchMessages: "搜尋聊天訊息" - home: "首頁" - send: "發送" - newline: "換行" - muteThisRoom: "此聊天室已靜音" - deleteRoom: "刪除聊天室" - chatNotAvailableForThisAccountOrServer: "這個伺服器或這個帳號的聊天功能尚未啟用。" - chatIsReadOnlyForThisAccountOrServer: "在此伺服器或此帳戶上的聊天是唯讀的。您無法發布新訊息、建立或加入聊天室。" - chatNotAvailableInOtherAccount: "對方的帳號無法使用聊天功能。" - cannotChatWithTheUser: "無法與此使用者聊天" - cannotChatWithTheUser_description: "聊天功能目前無法使用,或對方尚未開放聊天功能。" - youAreNotAMemberOfThisRoomButInvited: "您不是此聊天室的參與者,但已收到邀請。若要加入,請先接受邀請。\n" - doYouAcceptInvitation: "您要接受這個邀請嗎?\n" - chatWithThisUser: "聊天" - thisUserAllowsChatOnlyFromFollowers: "此使用者僅接受來自追隨者的聊天訊息。" - thisUserAllowsChatOnlyFromFollowing: "此使用者僅接受自己追隨的使用者傳送聊天訊息。" - thisUserAllowsChatOnlyFromMutualFollowing: "此使用者只接受互相追隨的使用者傳送聊天訊息。" - thisUserNotAllowedChatAnyone: "此使用者不接受來自任何人的聊天訊息。" - chatAllowedUsers: "允許聊天的對象" - chatAllowedUsers_note: "無論此設定為何,您仍可與自己曾發送過聊天訊息的對象進行聊天。" - _chatAllowedUsers: - everyone: "任何人" - followers: "追隨自己的使用者" - following: "只有您追隨的使用者" - mutual: "互相追隨" - none: "無" -_emojiPalette: - palettes: "調色盤" - enableSyncBetweenDevicesForPalettes: "啟用裝置與裝置之間的調色盤同步化" - paletteForMain: "主要使用的調色盤" - paletteForReaction: "反應用的調色盤" -_settings: - driveBanner: "您可以管理和設定雲端硬碟、確認使用量,以及調整上傳檔案時的設定。" - pluginBanner: "可使用外掛擴充用戶端的功能。您可以安裝外掛,實施個別的設定與管理。" - notificationsBanner: "您可以設定從伺服器接收通知的類型和範圍,以及推送通知。" - api: "API" - webhook: "Webhook" - serviceConnection: "服務整合" - serviceConnectionBanner: "您可以管理和設定存取權杖與 Webhooks,以便與外部應用程式和服務整合。" - accountData: "帳戶資料" - accountDataBanner: "您可以管理帳戶資料的匯出 / 匯入。" - muteAndBlockBanner: "您可以設定和管理要隱藏的內容,並限制特定使用者的行動。" - accessibilityBanner: "可針對客戶端的視覺和行為進行個人化設定,以達到更佳的使用效果。" - privacyBanner: "您可以調整帳戶的隱私設定,例如內容的可見性、尋找內容的容易程度,以及追隨是否需要核准。" - securityBanner: "您可以設定與帳戶安全性相關的設定,例如密碼、登入方式、驗證應用程式和通行金鑰。" - preferencesBanner: "您可以根據喜好設定用戶端的整體行為。" - appearanceBanner: "您可以根據喜好設定與用戶端外觀和顯示方式相關的設定。" - soundsBanner: "您可以調整用戶端播放的聲音設定。" - timelineAndNote: "時間軸及貼文" - makeEveryTextElementsSelectable: "允許選取所有文字" - makeEveryTextElementsSelectable_description: "啟用此功能後,可能會在某些情境下降低可用性。" - useStickyIcons: "使大頭貼跟隨捲動" - enableHighQualityImagePlaceholders: "顯示高品質的圖片預覽圖" - uiAnimations: "使用者介面的動畫效果\n" - showNavbarSubButtons: "在導覽列顯示輔助按鈕" - ifOn: "開啟時" - ifOff: "關閉時" - enableSyncThemesBetweenDevices: "在裝置之間同步已安裝的主題" - enablePullToRefresh: "下拉更新" - enablePullToRefresh_description: "使用滑鼠,按下並拖曳滾輪。" - realtimeMode_description: "已與伺服器建立連線,將即時更新內容。這可能會增加資料傳輸量與電池消耗。\n" - contentsUpdateFrequency: "內容取得頻率" - contentsUpdateFrequency_description: "頻率越高,內容更新越即時,但可能會降低效能,並增加資料傳輸量與電池消耗。\n" - contentsUpdateFrequency_description2: "當即時模式開啟時,不論此設定為何,內容都會即時更新。" - showUrlPreview: "顯示網址預覽" - _chat: - showSenderName: "顯示發送者的名稱" - sendOnEnter: "按下 Enter 發送訊息" -_preferencesProfile: - profileName: "設定檔案名稱" - profileNameDescription: "設定一個名稱來識別此裝置。" - profileNameDescription2: "例如:「主要個人電腦」、「智慧型手機」等" - manageProfiles: "管理個人檔案" -_preferencesBackup: - autoBackup: "自動備份" - restoreFromBackup: "從備份還原" - noBackupsFoundTitle: "找不到備份檔" - noBackupsFoundDescription: "沒有找到自動建立的備份,但如果您手動儲存了備份檔案,則可以匯入並還原。" - selectBackupToRestore: "選擇要還原的備份" - youNeedToNameYourProfileToEnableAutoBackup: "要啟用自動備份,必須設定檔案名稱。" - autoPreferencesBackupIsNotEnabledForThisDevice: "此裝置未啟用自動備份設定。" - backupFound: "找到設定的備份" -_accountSettings: - requireSigninToViewContents: "須登入以顯示內容" - requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。" - requireSigninToViewContentsDescription2: "針對您貼文的 URL 預覽 (OGP) 與網頁嵌入功能將會無法使用。而不支援引用貼文的伺服器,也將停止顯示。" - requireSigninToViewContentsDescription3: "這些限制可能不適用於被聯邦發送至遠端伺服器的內容。" - makeNotesFollowersOnlyBefore: "讓過去的貼文僅對追隨者顯示" - makeNotesFollowersOnlyBeforeDescription: "啟用此功能後,超過設定的日期和時間或超過設定時間的貼文將僅對追隨者顯示。 如果您再次停用它,貼文的公開狀態也會恢復原狀。" - makeNotesHiddenBefore: "隱藏過去的貼文" - makeNotesHiddenBeforeDescription: "啟用此功能後,超過設定的日期和時間或超過設定時間的貼文將僅對自己顯示(私密化)。 如果您再次停用它,貼文的公開狀態也會恢復原狀。" - mayNotEffectForFederatedNotes: "聯邦發送至遠端伺服器的貼文可能會不受影響。" - mayNotEffectSomeSituations: "這些限制僅是簡化版本。在某些情況下,例如在遠端伺服器上瀏覽或進行審核時,可能不會套用這些限制。" - notesHavePassedSpecifiedPeriod: "早於指定時間的貼文" - notesOlderThanSpecifiedDateAndTime: "指定時間和日期之前的貼文" -_abuseUserReport: - forward: "轉發" - forwardDescription: "以匿名系統帳戶將檢舉轉發至遠端伺服器。" - resolve: "解決" - accept: "接受" - reject: "拒絕" - resolveTutorial: "如果您已回覆正當的檢舉,請選擇「接受」以將案件標記為已解決。\n 如果檢舉的內容不正當,請選擇「拒絕」將案件標記為已解決。" -_delivery: - status: "傳送狀態" - stop: "停止發送" - resume: "恢復發送" - _type: - none: "發送中" - manuallySuspended: "手動暫停中" - goneSuspended: "因為伺服器刪除所以暫停中" - autoSuspendedForNotResponding: "因為伺服器沒有回應所以暫停中" - softwareSuspended: "此軟體因已停止發佈,目前無法使用" -_bubbleGame: - howToPlay: "玩法說明" - hold: "保留" - _score: - score: "分數" - scoreYen: "賺取的金額" - highScore: "最高分" - maxChain: "最大結合數" - yen: "{yen}円" - estimatedQty: "{qty}個" - scoreSweets: "飯糰 {onigiriQtyWithUnit}" - _howToPlay: - section1: "調整位置並將物體放入盒子中。" - section2: "當相同類型的物體黏在一起時,它們會變成不同的物體,您就會得到分數。" - section3: "如果物體從盒子裡溢出,遊戲就結束了。透過融合物體而不溢出盒子來獲得高分!" -_announcement: - forExistingUsers: "僅限既有的使用者" - forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" - needConfirmationToRead: "必須確認才能標記為已讀" - needConfirmationToReadDescription: "啟用代表此公告將顯示對話方塊以確認是否標記為已讀,同時不會受「標記所有公告為已讀」功能影響。" - end: "結束公告" - tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。" - readConfirmTitle: "標記為已讀嗎?" - readConfirmText: "閱讀「{title}」的內容並標記為已讀。" - shouldNotBeUsedToPresentPermanentInfo: "為了避免損害新用戶的使用體驗,建議使用公告來發布即時性的訊息,而不是用於固定不變的資訊。" - dialogAnnouncementUxWarn: "如果同時有 2 個以上對話方塊形式的公告存在,對於使用者體驗很可能會有不良的影響,因此建議謹慎使用。" - silence: "不發送通知" - silenceDescription: "啟用此選項後,將不會發送此公告的通知,並且無需將其標記為已讀。" _initialAccountSetting: accountCreated: "帳戶已建立完成!" letsStartAccountSetup: "來進行帳戶的初始設定吧。" @@ -1535,236 +1078,133 @@ _initialAccountSetting: privacySetting: "隱私設定" theseSettingsCanEditLater: "這裡的設定可以在之後變更。" youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。" - followUsers: "為了構築時間軸,試著追隨您感興趣的使用者吧。" - pushNotificationDescription: "啟用推送通知後,就可以在裝置上接收來自{name}的通知了。" + followUsers: "為了構築時間軸,試著追蹤您感興趣的使用者吧。" + pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。" initialAccountSettingCompleted: "初始設定完成了!" haveFun: "盡情享受{name}吧!" - youCanContinueTutorial: "您可以繼續學習如何使用{name}(Misskey),也可以就此打住,立即開始使用。" - startTutorial: "開始教學課程" + ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。" skipAreYouSure: "要略過初始設定嗎?" laterAreYouSure: "稍後再重新進行初始設定嗎?" -_initialTutorial: - launchTutorial: "觀看教學課程" - title: "新手教學" - wellDone: "做得好" - skipAreYouSure: "結束教學模式?" - _landing: - title: "歡迎使用本教學課程" - description: "在這裡您可以查看 Misskey 的基本使用方法和功能。" - _note: - title: "什麼是貼文?" - description: "在Misskey上發布的內容稱為「貼文」。貼文在時間軸上按時間順序排列,並即時更新。" - reply: "您可以回覆貼文,並像討論串一樣繼續對話。" - renote: "您可以將此貼文分享到自己的時間軸。您也可以在引用時添加文字。" - reaction: "您可以加入反應。詳細資訊將在下一頁進行說明。" - menu: "可執行各種操作,如查看貼文詳細資訊和複製連結。" - _reaction: - title: "什麼是反應?" - description: "您可以在貼文中加上「反應」。有些用「最愛/大心」無法傳達的感想,可以用反應輕鬆地表達出來。" - letsTryReacting: "按一下貼文上的「+」按鈕即可加入反應。試著對此範例貼文加上反應!" - reactToContinue: "添加反應以繼續教學課程。" - reactNotification: "當有人對您的貼文做出反應時會即時接收到通知。" - reactDone: "按下「-」按鈕可以取消反應。" - _timeline: - title: "時間軸如何運作" - description1: "Misskey根據使用方式提供了多個時間軸(伺服器可能會將部份時間軸停用)。" - home: "您可以查看您追隨的使用者的貼文。" - local: "您可以看到此伺服器上所有使用者的貼文。" - social: "來自首頁時間軸和本地時間軸的貼文都會顯示。" - global: "可以看到其他已連接伺服器的貼文。" - description2: "您可以隨時在螢幕上方切換對應的時間軸。" - description3: "除此之外還有清單時間軸、頻道時間軸等。請參閱{link}以了解更多詳情。" - _postNote: - title: "貼文的發布設定" - description1: "在Misskey上發布貼文時,可以設定各種選項。發布表單如下所示。" - _visibility: - description: "可以限制誰可以看到您的貼文。" - public: "所有人都可以看見。" - home: "僅在首頁時間軸上發布。其他使用者只在下列情況可看見該貼文:追隨者、觀看使用者的個人資料頁面,以及貼文被轉發時。" - followers: "僅追隨者可見。只有發文者本人可轉發,未追隨發文者的使用者無法看見。" - direct: "僅指定的使用者可見,對方也會收到通知。可代替直接訊息使用。" - doNotSendConfidencialOnDirect1: "發送機密訊息時請務必注意。" - doNotSendConfidencialOnDirect2: "目標伺服器的管理員可以看到發布的內容,因此如果您向不受信任的伺服器上的使用者發送直接訊息,必須小心處理機密訊息。" - localOnly: "不將貼文發布到聯邦上的其他伺服器。不論上述發布範圍,使用此設定後,其他伺服器上的使用者將無法直接查看此貼文。" - _cw: - title: "隱藏內容(CW)" - description: "將顯示「註釋」中寫入的內容而不是本文。按一下「顯示內容」以顯示本文。" - _exampleNote: - cw: "注意消夜文" - note: "我吃了一個巧克力甜甜圈🍩😋" - useCases: "伺服器的服務條款可能會規範特定的貼文需要使用隱藏內容,除此之外也會用在隱藏劇情洩漏與敏感內容的貼文。" - _howToMakeAttachmentsSensitive: - title: "如何標記上傳附件為敏感內容?" - description: "如果伺服器的服務條款有規範,又或者不適合直接展示的附件,請記得加上「敏感」標記。" - tryThisFile: "試試看!把附加在發文表單的圖像檔案標記為敏感內容。" - _exampleNote: - note: "打開納豆的包裝失敗了…" - method: "若要使上傳附件標記為敏感內容,請按一下該檔案以開啟選單,然後點擊「標記為敏感內容」。" - sensitiveSucceeded: "上傳附件時,請務必根據伺服器的服務條款適當設定敏感內容。" - doItToContinue: "把圖像標記為敏感內容以繼續教學課程。" - _done: - title: "教學課程已結束" - description: "這裡介紹的功能只是其中的一小部分。要了解更多有關如何使用Misskey的資訊,請瀏覽{link}。" -_timelineDescription: - home: "在首頁時間軸上,可以看到您追隨的使用者的貼文。" - local: "在本地時間軸上,可以看到此伺服器所有使用者的貼文。" - social: "在社交時間軸上,可以看到首頁與本地時間軸的貼文。" - global: "在公開時間軸上,可以看到其他已連接伺服器的貼文。\n" _serverRules: - description: "設定在註冊頁面顯示的伺服器簡要規則。建議是服務條款的摘要。" -_serverSettings: - iconUrl: "圖示的 URL" - appIconDescription: "指定顯示 {host} 為應用程式時的圖示。" - appIconUsageExample: "例如:PWA 或是在手機桌面作為書籤等" - appIconStyleRecommendation: "因為可能會裁剪成圓形或圓角,所以建議用單色填滿邊框及背景。" - appIconResolutionMustBe: "解析度必須為 {resolution}。" - manifestJsonOverride: "覆寫 manifest.json" - shortName: "簡稱" - shortNameDescription: "如果伺服器的正式名稱很長,可用簡稱或通稱代替。" - fanoutTimelineDescription: "如果啟用的話,檢索各個時間軸的性能會顯著提昇,資料庫的負荷也會減少。不過,Redis 的記憶體使用量會增加。如果伺服器的記憶體容量比較少或者運行不穩定,可以停用。" - fanoutTimelineDbFallback: "資料庫的回退" - fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。" - reactionsBufferingDescription: "啟用時,可以顯著提高建立反應時的效能並減少資料庫的負載。 但是,Redis 記憶體使用量會增加。" - inquiryUrl: "聯絡表單網址" - inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。" - openRegistration: "允許建立帳戶" - openRegistrationWarning: "開放註冊伴隨著風險。 建議只有在伺服器受到持續監控,並準備好在出現問題時能立即處理的情況下才開放註冊。" - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "如果在一段期間內沒有偵測到任何審查員活動,此設定將自動關閉,以防止垃圾內容。" - deliverSuspendedSoftware: "已停止發佈的軟體" - deliverSuspendedSoftwareDescription: "由於脆弱性等原因,可以指定伺服器軟體的名稱與版本範圍來停止其發佈。這些版本資訊是由伺服器所提供,其可靠性無法保證。版本的指定可以使用 semver(語意化版本控制) 的範圍語法,但如果指定為 >= 2024.3.1,則像 2024.3.1-custom.0 這樣的自訂版本將不會被包含在內,因此建議使用 >= 2024.3.1-0 的方式來同時包含預發佈版本。" - singleUserMode: "單人模式" - singleUserMode_description: "如果只有自己使用此伺服器的話,啟用此模式將使效能最佳化。" - signToActivityPubGet: "簽署 GET 請求" - signToActivityPubGet_description: "通常應該啟用此功能。停用可能會改善聯邦通訊的問題,但反過來也可能會使某些伺服器無法通訊。" - proxyRemoteFiles: "代理提供遠端檔案" - proxyRemoteFiles_description: "啟用時,它會代理並提供遠端檔案。 這有助於產生影像縮圖和保護使用者隱私。" - allowExternalApRedirect: "允許透過 ActivityPub 查詢時進行重新導向" - allowExternalApRedirect_description: "啟用後,其他伺服器可以透過此伺服器查詢第三方的內容,但也可能導致內容遭到冒充的風險。" - userGeneratedContentsVisibilityForVisitor: "使用者建立的內容對訪客的公開範圍" - userGeneratedContentsVisibilityForVisitor_description: "這有助於防止一些問題的發生,例如未經適當審核的不適當遠端內容無意中透過您自己的伺服器發佈到網際網路上。" - userGeneratedContentsVisibilityForVisitor_description2: "包括伺服器接收到的遠端內容在內,無條件地將伺服器內所有內容公開到網際網路上是具有風險的。特別是對於不了解分散式架構特性的瀏覽者來說,他們可能會誤以為這些遠端內容是由該伺服器所創建的,因此需要特別留意。" - _userGeneratedContentsVisibilityForVisitor: - all: "全部公開\n" - localOnly: "僅公開本地內容,遠端內容則不公開\n" - none: "全部不公開" + description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。" _accountMigration: moveFrom: "從其他帳戶遷移到這個帳戶" moveFromSub: "為另一個帳戶建立別名" - moveFromLabel: "要遷移過來的帳戶 #{n}" + moveFromLabel: "要遷移過來的帳戶:" moveFromDescription: "如果你想把追隨者從別的帳戶遷移過來,必須先在這裡建立別名。請務必在執行遷移之前建立別名!請像這樣輸入要遷移的帳戶:@person@instance.com" moveTo: "將這個帳戶遷移至新的帳戶" moveToLabel: "要遷移到的帳戶:" moveCannotBeUndone: "一旦遷移帳戶,就無法取消。" - moveAccountDescription: "遷移至新帳戶。\n ・此帳戶的追隨者將自動追隨新帳戶;\n ・此帳戶的所有追隨者將被取消追隨;\n ・此帳戶不能再發文。\n\n雖然會自動遷移您的追隨者,但必須手動遷移您追隨的帳戶。請在遷移前匯出此帳戶的「追隨中」名單,並在遷移後自行匯入。\n列表名單、靜音名單及封鎖名單也必須如此處理。\n\n(此說明適用於本伺服器,以及運行 Misskey v13.12.0 或更新版本的其他伺服器;如 Mastodon 等使用 ActivityPub 協定的其他軟體或有不同的處理方式。)" + moveAccountDescription: "這個操作不可撤銷。首先,請確認已在要遷移到的帳戶中為這個帳戶建立了一個別名。建立別名之後,像這樣輸入你要遷移到的帳戶:@person@instance.com" moveAccountHowTo: "要遷移帳戶,首先要在目標帳戶中為此帳戶建立一個別名。\n 建立別名後,像這樣輸入目標帳戶:@username@server.example.com" startMigration: "遷移" migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外,請確認在要遷移到的帳戶已經建立了一個別名。" movedAndCannotBeUndone: "帳戶已遷移。\n遷移無法撤消。" - postMigrationNote: "將在完成遷移的 24 小時後取消追隨所有帳號。\n此帳戶的追隨中/追隨者人數將歸零。由於不會解除粉絲對您的追隨,因此他們仍然可以繼續閱覽此帳戶內僅對追隨者公開的貼文。" + postMigrationNote: "在遷移操作後的24小時之後解除此帳戶的追隨。此帳戶的追隨中、追隨者數量變為0。由於不會解除追隨者,你的追隨者仍然可以繼續檢視這個帳戶發布給追隨者的貼文。" movedTo: "要遷移到的帳戶:" _achievements: earnedAt: "獲得日期" _types: _notes1: - title: "歡迎!" + title: "just setting up my msky" description: "發出了第一則貼文" - flavor: "祝您的 Misskey 生活愉快!" + flavor: "祝您的Misskey生活愉快!" _notes10: title: "若干貼文" - description: "發佈了十篇貼文" + description: "發表了10則貼文" _notes100: title: "許多貼文" - description: "發佈了一百篇貼文" + description: "發表了100則貼文" _notes500: title: "滿滿的貼文" - description: "發佈了五百篇貼文" + description: "發表了500則貼文" _notes1000: title: "堆積如山的貼文" - description: "發佈了一千篇貼文" + description: "發表了1000則貼文" _notes5000: title: "滔滔不絕的貼文" - description: "發佈了五千篇貼文" + description: "發表了5000則貼文" _notes10000: title: "超級貼文" - description: "發佈了一萬篇貼文" + description: "發表了10000則貼文" _notes20000: - title: "需要更多貼文" - description: "發佈了兩萬篇貼文" + title: "需要更多的貼文" + description: "發表了20000則貼文" _notes30000: title: "貼文貼文貼文" - description: "發佈了三萬篇貼文" + description: "發表了30000則貼文" _notes40000: title: "貼文工廠" - description: "發佈了四萬篇貼文" + description: "發表了40000則貼文" _notes50000: title: "貼文星球" - description: "發佈了五萬篇貼文" + description: "發表了50000則貼文" _notes60000: title: "貼文類星體" - description: "發佈了六萬篇貼文" + description: "發表了60000則貼文" _notes70000: title: "貼文黑洞" - description: "發佈了七萬篇貼文" + description: "發表了70000則貼文" _notes80000: title: "貼文銀河" - description: "發佈了八萬篇貼文" + description: "發表了80000則貼文" _notes90000: title: "貼文宇宙" - description: "發佈了九萬篇貼文" + description: "發表了90000則貼文" _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" - description: "發佈了十萬篇貼文" + description: "發表了100,000則貼文" flavor: "有這麼多東西要寫嗎?" _login3: title: "初學者Ⅰ" - description: "總登入天數為三天" - flavor: "從今天開始,我就是 Misskist" + description: "總登入天數為3天" + flavor: "從今天開始,我就是Misskist" _login7: title: "初學者ⅠⅠ" - description: "總登入天數為七天" + description: "總登入天數為7天" flavor: "您開始習慣了嗎?" _login15: title: "初學者ⅠⅠⅠ" - description: "總登入天數為十五天" + description: "總登入天數為15天" _login30: title: "Misskist Ⅰ" - description: "總登入天數為三十天" + description: "總登入天數為30天" _login60: title: "Misskist ⅠⅠ" - description: "總登入天數為六十天" + description: "總登入天數為60天" _login100: title: "Misskist ⅠⅠⅠ" - description: "總登入天數為一百天" - flavor: "凶暴的 Misskist" + description: "總登入天數為100天" + flavor: "辣個 Misskist 用戶" _login200: title: "普通Ⅰ" - description: "總登入天數為兩百天" + description: "總登入天數為200天" _login300: - title: "普通ⅠⅠ" - description: "總登入天數為三百天" + title: "普通IⅠ" + description: "總登入天數為300天" _login400: - title: "普通ⅠⅠⅠ" - description: "總登入天數為四百天" + title: "普通IIⅠ" + description: "總登入天數為400天" _login500: title: "老兵Ⅰ" - description: "總登入天數為五百天" + description: "總登入天數為500天" flavor: "諸君,我喜歡貼文" _login600: title: "老兵ⅠⅠ" - description: "總登入天數為六百天" + description: "總登入天數為600天" _login700: title: "老兵ⅠⅠⅠ" - description: "總登入天數為七百天" + description: "總登入天數為700天" _login800: title: "貼文大師Ⅰ" - description: "總登入天數為八百天" + description: "總登入天數為800天" _login900: title: "貼文大師ⅠⅠ" - description: "總登入天數為九百天" + description: "總登入天數為900天" _login1000: title: "貼文大師ⅠⅠⅠ" - description: "總登入天數為一千天" - flavor: "感謝您使用 Misskey!" + description: "總登入天數為1,000天" + flavor: "感謝您使用Misskey!" _noteClipped1: title: "忍不住要收進摘錄裡" description: "第一次將貼文收進摘錄" @@ -1778,9 +1218,9 @@ _achievements: title: "有備而來" description: "設定了個人檔案" _markedAsCat: - title: "我是貓" + title: "吾輩乃貓是也" description: "已將帳戶設定為貓" - flavor: "沒有名字。" + flavor: "還沒有名字。" _following1: title: "首次追隨" description: "首次追隨了" @@ -1791,16 +1231,16 @@ _achievements: title: "朋友很多" description: "追隨超過50人了" _following100: - title: "一百位朋友" + title: "100位朋友" description: "追隨超過100人了" _following300: - title: "朋友太多" + title: "朋友過多" description: "追隨超過300人了" _followers1: title: "第一個追隨者" description: "第一次被追隨" _followers10: - title: "追隨我吧!" + title: "Follow me!" description: "追隨者超過10人了" _followers50: title: "成群結隊" @@ -1809,24 +1249,24 @@ _achievements: title: "熱門人物" description: "追隨者超過100人了" _followers300: - title: "請排隊" + title: "請排成一排" description: "追隨者超過300人了" _followers500: - title: "基地臺" - description: "超過五百名追隨者了" + title: "基地台" + description: "超過500名追隨者了" _followers1000: - title: "星光熠熠" - description: "超過一千名追隨者了" + title: "影響者" + description: "超過1000名追隨者了" _collectAchievements30: title: "成就收藏家" - description: "獲得三十個以上的成就" + description: "獲得30個以上的成就" _viewAchievements3min: - title: "成就發燒友" - description: "看著成就列表超過三分鐘" + title: "喜愛成就" + description: "看成就列表要花3分鐘以上" _iLoveMisskey: title: "I Love Misskey" - description: "發佈「I ❤ #Misskey」" - flavor: "感謝您使用 Misskey!by 開發團隊" + description: "發布「I ❤ #Misskey」" + flavor: "感謝您使用Misskey! by 開發團隊" _foundTreasure: title: "尋寶" description: "發現了隱藏的寶藏" @@ -1834,34 +1274,34 @@ _achievements: title: "休息一下" description: "客戶端啟動已超過30分鐘" _client60min: - title: "Misskey 看太多" + title: "Misskey看太多" description: "客戶端啟動已超過60分鐘" _noteDeletedWithin1min: - title: "欲言又止" - description: "發文後一分鐘內刪文" + title: "現在沒有了" + description: "發文後1分鐘內刪文" _postedAtLateNight: - title: "夜貓子" + title: "夜行性" description: "在深夜發佈貼文" flavor: "該去睡覺了。" _postedAt0min0sec: title: "報時" - description: "在零分零秒發佈貼文" + description: "在0分0秒發佈貼文" flavor: "啵.啵.啵.嗶ー" _selfQuote: title: "自我引用" description: "引用了自己的貼文" _htl20npm: - title: "源源不絕" - description: "首頁時間軸在一分鐘內出現超過二十篇貼文" + title: "流動的TL" + description: "在首頁時間軸的流速超過20npm" _viewInstanceChart: title: "分析師" - description: "顯示了伺服器的圖表" + description: "顯示了實例的圖表" _outputHelloWorldOnScratchpad: - title: "Hello, world!" - description: "在 AiScript 控制臺輸出了「hello world」" + title: "Hello world!" + description: "在暫存記憶體輸出了 hello world" _open3windows: title: "多重視窗" - description: "開啟過三個以上的視窗" + description: "開啟了3個以上的視窗" _driveFolderCircularReference: title: "循環引用" description: "試圖遞迴套入雲端硬碟資料夾" @@ -1873,60 +1313,45 @@ _achievements: description: "已點擊這裡了" _justPlainLucky: title: "只是運氣好" - description: "每十秒有二萬分之一(0.005%)的機率獲得" + description: "每10秒有0.01%的機率獲得" _setNameToSyuilo: - title: "神與您同在" + title: "神的情結" description: "將名稱設定為 syuilo" _passedSinceAccountCreated1: - title: "一週年" - description: "帳戶加入時間已超過一年" + title: "一周年" + description: "自建立帳戶開始過了1年" _passedSinceAccountCreated2: - title: "二週年" - description: "帳戶加入時間已超過兩年" + title: "二周年" + description: "自建立帳戶開始過了2年" _passedSinceAccountCreated3: - title: "三週年" - description: "帳戶加入時間已超過三年" + title: "三周年" + description: "自建立帳戶開始過了3年" _loggedInOnBirthday: title: "生日快樂" description: "在生日當天登入了" _loggedInOnNewYearsDay: title: "新年快樂" description: "在元旦當天登入了" - flavor: "今年也請您多多指教!" + flavor: "今年也請對敝實例多多指教" _cookieClicked: title: "點擊餅乾的遊戲" description: "點擊了餅乾" flavor: "是不是軟體有問題?" _brainDiver: title: "Brain Driver" - description: "發佈一篇含歌曲《Brain Driver》連結的貼文" + description: "發佈了Brain Driver的連結" flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "過度測試" - description: "極短時間內連續測試通知" - _tutorialCompleted: - title: "Misskey新手講座 結業證書" - description: "已完成教學課程" - _bubbleGameExplodingHead: - title: "🤯" - description: "氣泡遊戲中最大的物體出現了" - _bubbleGameDoubleExplodingHead: - title: "雙重🤯" - description: "氣泡遊戲中最大的物體同時出現了兩個" - flavor: "這樣大小的便當盒,用 🤯 🤯 稍微裝滿一些吧" _role: new: "建立角色" edit: "編輯角色" name: "角色名稱" description: "角色描述 " permission: "角色的權限" - descriptionOfPermission: "審查員執行與審查相關的基本操作。\n管理員能變更伺服器的全部設定。" + descriptionOfPermission: "審查員執行與審查相關的基本操作。\n管理員能變更實例的全部設定" assignTarget: "指派目標" descriptionOfAssignTarget: "手動是以手動管理這個角色包含的人員。\n符合條件是設定條件以自動包含符合條件的使用者。" manual: "手動" - manualRoles: "手動角色" conditional: "符合條件" - conditionalRoles: "有條件的角色" condition: "條件" isConditionalRole: "這是條件角色。" isPublic: "角色為公開" @@ -1936,15 +1361,13 @@ _role: baseRole: "基本角色" useBaseValue: "使用基本角色的值" chooseRoleToAssign: "選擇要指派的角色" - iconUrl: "圖示的 URL" + iconUrl: "圖示的URL" asBadge: "顯示為徽章" - descriptionOfAsBadge: "開啟的話,角色圖示會顯示在使用者名稱旁邊。" + descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。" isExplorable: "讓使用者更容易找到您" descriptionOfIsExplorable: "若開啟則公開角色時間軸。若角色不是公開的,則無法公開時間軸。" displayOrder: "顯示順序" descriptionOfDisplayOrder: "數字越大,顯示在UI上的越上面。" - preserveAssignmentOnMoveAccount: "將指派狀態承接至轉移後的帳戶" - preserveAssignmentOnMoveAccount_description: "開啟此選項後,當具備此角色的帳戶被移轉時,該角色也會承接至轉移後的帳戶。" canEditMembersByModerator: "允許編輯審查員的成員" descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" priority: "優先級" @@ -1956,21 +1379,14 @@ _role: gtlAvailable: "瀏覽全域時間軸" ltlAvailable: "瀏覽本地時間軸" canPublicNote: "允許公開貼文" - mentionMax: "貼文內的最大提及數" - canInvite: "發行伺服器邀請碼" - inviteLimit: "可建立邀請碼的數量" - inviteLimitCycle: "邀請碼的發放間隔" - inviteExpirationTime: "邀請碼的有效日期" + canInvite: "發行實例邀請碼" canManageCustomEmojis: "管理自訂表情符號" - canManageAvatarDecorations: "管理頭像裝飾" driveCapacity: "雲端硬碟容量" - maxFileSize: "可上傳的最大檔案大小" alwaysMarkNsfw: "總是將檔案標記為NSFW" - canUpdateBioMedia: "允許更新大頭貼和橫幅" pinMax: "置頂貼文的最大數量" antennaMax: "可建立的天線數量" wordMuteMax: "靜音文字的最大字數" - webhookMax: "可建立的 Webhook 數量" + webhookMax: "可建立的Webhook數量" clipMax: "可建立的摘錄數量" noteEachClipsMax: "摘錄內貼文的最大數量" userListMax: "可建立的使用者清單數量" @@ -1979,64 +1395,46 @@ _role: descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" canHideAds: "不顯示廣告" canSearchNotes: "可否搜尋貼文" - canUseTranslator: "使用翻譯功能" - avatarDecorationLimit: "頭像可掛上的最大裝飾數量" - canImportAntennas: "允許匯入天線" - canImportBlocking: "允許匯入封鎖名單" - canImportFollowing: "允許匯入追隨名單" - canImportMuting: "允許匯入靜音名單" - canImportUserLists: "允許匯入清單" - chatAvailability: "允許聊天" - uploadableFileTypes: "可上傳的檔案類型" - uploadableFileTypes_caption: "請指定 MIME 類型。可以用換行區隔多個類型,也可以使用星號(*)作為萬用字元進行指定。(例如:image/*)\n" - uploadableFileTypes_caption2: "有些檔案可能無法判斷其類型。若要允許這類檔案,請在指定中加入 {x}。" _condition: - roleAssignedTo: "手動指派角色完成" isLocal: "本地使用者" isRemote: "遠端使用者" - isCat: "貓使用者" - isBot: "機器人使用者" - isSuspended: "被停權的使用者" - isLocked: "上鎖的使用者" - isExplorable: "開啟了「使您的帳戶更容易被找到」功能的使用者" - createdLessThan: "帳戶加入時間不超過" - createdMoreThan: "帳戶加入時間已超過" + createdLessThan: "自建立帳戶開始~以內" + createdMoreThan: "自建立帳戶開始~經過" followersLessThanOrEq: "追隨者人數在~以下" followersMoreThanOrEq: "追隨者人數在~以上" followingLessThanOrEq: "追隨人數在~以下" followingMoreThanOrEq: "追隨人數在~以上" - notesLessThanOrEq: "貼文數在~以下" - notesMoreThanOrEq: "貼文數在~以上" - and: "~及~" + notesLessThanOrEq: "發布數在~以下" + notesMoreThanOrEq: "發布數在~以上" + and: "~和~" or: "~或~" not: "~否" _sensitiveMediaDetection: - description: "您可以使用機器學習自動檢測敏感檔案以便審查。這會稍微增加伺服器負荷。" + description: "您可以使用機器學習自動檢測敏感媒體並將其用於審查。 伺服器的負荷會稍微增加。" sensitivity: "檢測敏感度" sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。" - setSensitiveFlagAutomatically: "設定 NSFW 標籤" + setSensitiveFlagAutomatically: "設定 NSFW 旗標" setSensitiveFlagAutomaticallyDescription: "即使將此設定關閉,判定結果也會保留在內部。" analyzeVideos: "啟用影片分析" analyzeVideosDescription: "除了靜止影像以外,也分析影片。伺服器的負荷會稍微增加。" _emailUnavailable: - used: "已被使用" + used: "已經在使用中" format: "格式無效" disposable: "不是永久可用的地址" mx: "郵件伺服器不正確" smtp: "郵件伺服器沒有應答" - banned: "無法使用此電子郵件地址註冊" _ffVisibility: public: "公開" - followers: "只有關注您的使用者能看到" + followers: "只有關注你的用戶能看到" private: "私密" _signup: almostThere: "即將完成" emailAddressInfo: "請輸入您所使用的電子郵件地址。電子郵件地址不會被公開。" - emailSent: "已發送確認郵件至您輸入的電子郵件地址({email})。請開啟電子郵件中的連結完成註冊。" + emailSent: "已將確認郵件發送至您輸入的電子郵件地址 ({email})。請開啟電子郵件中的連結以完成帳戶創建。" _accountDelete: accountDelete: "刪除帳戶" - mayTakeTime: "刪除帳戶的處理負荷較大,如果帳戶發佈的內容以及上傳的檔案數量較多,則需要一段時間才能完成。" - sendEmail: "帳戶刪除完成後,將向其電子郵件地址發送通知。" + mayTakeTime: "刪除帳戶的處理負荷較大,如果帳戶產生的內容數量上傳的檔案數量較多的話,就需要花费一段時間才能完成。" + sendEmail: "帳戶删除完成後,將向註冊地電子郵件地址發送通知。" requestAccountDelete: "刪除帳戶請求" started: "已開始刪除作業。" inProgress: "正在刪除" @@ -2045,19 +1443,15 @@ _ad: reduceFrequencyOfThisAd: "降低此廣告的頻率 " hide: "隱藏" timezoneinfo: "星期幾是由伺服器的時區指定的。" - adsSettings: "廣告投放設定" - notesPerOneAd: "即時更新中投放廣告的間隔(貼文數)" - setZeroToDisable: "設為 0 則在即時更新時不投放廣告" - adsTooClose: "由於廣告投放的間隔極短,可能會嚴重影響使用者體驗。" _forgotPassword: enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。" ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 " - contactAdmin: "本伺服器不支援電子郵件,請聯繫您的管理員重置您的密碼。 " + contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。 " _gallery: my: "我的貼文" liked: "喜歡的貼文" - like: "讚好" - unlike: "收回讚好" + like: "讚" + unlike: "收回喜歡" _email: _follow: title: "您有新的追隨者" @@ -2065,10 +1459,8 @@ _email: title: "收到追隨請求" _plugin: install: "安裝外掛組件" - installWarn: "請不要安裝來源不明的外掛。" + installWarn: "請不要安裝來源不明的外掛組件。" manage: "管理外掛" - viewSource: "檢視原始碼" - viewLog: "顯示記錄 " _preferencesBackups: list: "已備份的設定檔" saveNew: "另存新檔" @@ -2077,7 +1469,7 @@ _preferencesBackups: save: "覆蓋存檔" inputName: "輸入備份檔名稱" cannotSave: "無法儲存" - nameAlreadyExists: "備份檔名稱「{name}」已經存在。請填寫其他名稱。" + nameAlreadyExists: "備份檔名稱「{name}」已經存在。請指定不同的名稱。" applyConfirm: "將備份檔「{name}」套用在現在的裝置嗎?現在的裝置設定將會消失。" saveConfirm: "要覆蓋存檔{name}嗎?" deleteConfirm: "要刪除{name}嗎?" @@ -2094,25 +1486,22 @@ _registry: domain: "域" createKey: "新增機碼" _aboutMisskey: - about: "Misskey 是由 syuilo 自 2014 年起開發的開放原始碼軟體。" + about: "Misskey是由syuilo自2014年起開發的開源軟體。" contributors: "主要貢獻者" allContributors: "全體貢獻人員" source: "原始碼" - original: "原始" - thisIsModifiedVersion: "{name} 使用原始 Misskey 的修改版本。" - translation: "翻譯 Misskey" - donate: "贊助 Misskey" + translation: "翻譯Misskey" + donate: "贊助Misskey" morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰" patrons: "贊助者" - projectMembers: "專案成員" _displayOfSensitiveMedia: - respect: "隱藏敏感檔案" - ignore: "顯示敏感檔案" - force: "隱藏所有檔案" + respect: "隱藏設定為敏感的媒體" + ignore: "不隱藏設定為敏感的媒體" + force: "隱藏所有媒體" _instanceTicker: none: "隱藏" - remote: "只顯示遠端使用者" - always: "一律顯示" + remote: "向遠端使用者顯示" + always: "總是顯示" _serverDisconnectedBehavior: reload: "自動重載" dialog: "彈出式警告" @@ -2125,40 +1514,43 @@ _channel: featured: "熱門貼文" owned: "管理中" following: "追隨中" - usersCount: "有 {n} 人參與" - notesCount: "有 {n} 篇貼文" - nameAndDescription: "名稱" + usersCount: "有{n}人參與" + notesCount: "有{n}個貼文" + nameAndDescription: "名稱與說明" nameOnly: "僅名稱" - allowRenoteToExternal: "允許在頻道外轉發和引用" _menuDisplay: - sideFull: "橫向" - sideIcon: "橫向(圖示)" + sideFull: "側向" + sideIcon: "側向(圖示)" top: "頂部" hide: "隱藏" _wordMute: muteWords: "加入靜音文字" - muteWordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)。" - muteWordsDescription2: "用斜線包圍關鍵字代表正規表達式。" + muteWordsDescription: "用空格分隔指定AND,用換行分隔指定OR。" + muteWordsDescription2: "將關鍵字用斜線括起來表示正規表達式。" + softDescription: "隱藏時間軸中指定條件的貼文。" + hardDescription: "具有指定條件的貼文將不添加到時間軸。 即使您更改條件,未被添加的貼文也會被排除在外。" + soft: "軟性靜音" + hard: "硬性靜音" + mutedNotes: "已靜音的貼文" _instanceMute: - instanceMuteDescription: "包括對被靜音伺服器上的使用者的回覆,被設定的伺服器上所有貼文及轉發都會被靜音。" + instanceMuteDescription: "包括對被靜音實例上的用戶的回覆,被設定的實例上所有貼文及轉發都會被靜音。" instanceMuteDescription2: "設定時以換行進行分隔" - title: "將隱藏被設定的伺服器貼文。" - heading: "要靜音的伺服器" + title: "被設定的實例,貼文將被隱藏。" + heading: "將實例靜音" _theme: - explore: "探索佈景主題" + explore: "取得佈景主題" install: "安裝佈景主題" - manage: "管理佈景主題" - code: "佈景主題代碼" + manage: "佈景主題管理員" + code: "主題代碼" description: "描述" installed: "{name}已安裝" - installedThemes: "已經安裝的佈景主題" - builtinThemes: "標準佈景主題" - instanceTheme: "伺服器的主題" - alreadyInstalled: "已安裝此佈景主題" - invalid: "佈景主題格式錯誤" - make: "製作佈景主題" + installedThemes: "已經安裝的主題" + builtinThemes: "標準主題" + alreadyInstalled: "此主題已經安裝" + invalid: "主題格式錯誤" + make: "製作主題" base: "基於" - addConstant: "新增常數" + addConstant: "添加常數" constant: "常數" defaultValue: "預設值" color: "顏色" @@ -2168,13 +1560,13 @@ _theme: func: "函数" funcKind: "功能類型" argument: "參數" - basedProp: "基於的屬性名稱 " + basedProp: "要基於的屬性的名稱 " alpha: "透明度" darken: "暗度" lighten: "亮度" - inputConstantName: "請輸入常數名稱" - importInfo: "您可以在此貼上佈景主題代碼,將其匯入編輯器中" - deleteConstantConfirm: "確定要刪除常數{const}嗎?" + inputConstantName: "請輸入常數的名稱" + importInfo: "您可以在此貼上主題代碼,將其匯入編輯器中" + deleteConstantConfirm: "確定要删除常數{const}嗎?" keys: accent: "重點色彩" bg: "背景" @@ -2182,48 +1574,51 @@ _theme: focus: "聚焦" indicator: "指標" panel: "面板" - shadow: "影子" + shadow: "陰影" header: "標題" navBg: "側邊欄的背景 " navFg: "側邊欄的文字" - navActive: "側邊欄文字(活動)" + navHoverFg: "側邊欄文字(懸停) " + navActive: "側邊欄文本 (活動)" navIndicator: "側邊欄指示符" - link: "連結" + link: "鏈接" hashtag: "標籤" mention: "提到" mentionMe: "提到了我" renote: "轉發貼文" modalBg: "對話框背景" - divider: "分隔線" + divider: "分割線" scrollbarHandle: "捲動條" - scrollbarHandleHover: "捲動條(懸浮)" + scrollbarHandleHover: "捲動條 (漂浮)" dateLabelFg: "日期標籤文字" infoBg: "資訊背景" infoFg: "資訊內容" infoWarnBg: "警告背景" - infoWarnFg: "警告文字" + infoWarnFg: "警告字元" + cwBg: "CW 按鈕背景" + cwFg: "CW 按鈕文本" + cwHoverBg: "CW 按鈕背景 (漂浮)" toastBg: "通知背景" toastFg: "通知文本" buttonBg: "按鈕背景" buttonHoverBg: "按鈕背景 (漂浮)" inputBorder: "輸入框邊框" - badge: "徽章" + listItemHoverBg: "列表物品背景 (漂浮)" + driveFolderBg: "雲端硬碟文件夾背景" + wallpaperOverlay: "壁紙覆蓋層" + badge: "獎章" messageBg: "私訊背景" - fgHighlighted: "突顯文字" + accentDarken: "強調色(偏暗)" + accentLighten: "強調色(明亮)" + fgHighlighted: "高亮顯示文本" _sfx: note: "貼文" noteMy: "我的貼文" notification: "通知" - reaction: "選擇反應時" - chatMessage: "聊天訊息" -_soundSettings: - driveFile: "使用雲端硬碟的音效檔案" - driveFileWarn: "請選擇雲端硬碟中的檔案" - driveFileTypeWarn: "不支援此檔案" - driveFileTypeWarnDescription: "請選擇音效檔案" - driveFileDurationWarn: "音效太長了" - driveFileDurationWarnDescription: "使用長音效檔可能會影響 Misskey 的使用體驗。仍要使用此檔案嗎?" - driveFileError: "無法載入語音。請變更設定" + chat: "聊天" + chatBg: "聊天背景" + antenna: "天線接收" + channel: "頻道通知" _ago: future: "未來" justNow: "剛剛" @@ -2234,36 +1629,40 @@ _ago: weeksAgo: "{n}周前" monthsAgo: "{n}個月前" yearsAgo: "{n}年前" - invalid: "無" -_timeIn: - seconds: "{n}秒後" - minutes: "{n}分鐘後" - hours: "{n}小時後" - days: "{n}天後" - weeks: "{n}周後" - months: "{n}個月後" - years: "{n}年後" + invalid: "未發現" _time: second: "秒" minute: "分鐘" hour: "小時" day: "日" +_timelineTutorial: + title: "Misskey的使用方法" + step1_1: "這個畫面是「時間軸」。發布到{name}的「貼文」按照時間順序顯示。" + step1_2: "時間軸有多種類型,例如在「首頁時間軸」中流動的是您追蹤的人的貼文;而在「本地時間軸」流動的是{name}全體的貼文。" + step2_1: "試試看,發布個貼文吧!按畫面上鉛筆圖示的按鈕開啟表格。" + step2_2: "初次貼文的內容,建議包括自我介紹以及「開始使用{name}」。" + step3_1: "貼文發出去了嗎?" + step3_2: "如果你的貼文出現在時間軸上,就代表發文成功。" + step4_1: "可以對貼文標記「反應」。" + step4_2: "點擊貼文的「+」圖示,即可選擇喜好的表情符號來標記反應。" _2fa: - alreadyRegistered: "此裝置已被註冊過了" + alreadyRegistered: "此設備已經被註冊過了" registerTOTP: "開始設定驗證應用程式" - step1: "首先,在您的裝置上安裝驗證程式,例如 {a} 或 {b}。" - step2: "然後,掃描螢幕上的 QR 碼。" - step2Uri: "使用桌面版應用程式時,請輸入以下的 URI" + passwordToTOTP: "請輸入密碼" + step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。" + step2: "然後,掃描螢幕上的QR code。" + step2Click: "點擊QR code,可以使用設備上安裝的驗證應用程式或金鑰環進行註冊。" + step2Url: "在桌面版應用中,請輸入以下的URL:" step3Title: "輸入驗證碼" - step3: "輸入應用程式所提供的權杖以完成設定。" - setupCompleted: "設定完成" + step3: "輸入您的App提供的權杖以完成設定。" step4: "從現在開始,任何登入操作都將要求您提供權杖。" securityKeyNotSupported: "您的瀏覽器不支援安全金鑰。" - registerTOTPBeforeKey: "如要註冊安全金鑰或通行金鑰,請先設定驗證應用程式。" - securityKeyInfo: "註冊 WebAuthn 衍生的金鑰,例如支援 FIDO2 的硬體安全金鑰、裝置生物識別、PIN 鎖和通行金鑰。" - registerSecurityKey: "註冊安全金鑰或通行金鑰" + registerTOTPBeforeKey: "要註冊安全金鑰・Passkey,請先設定驗證應用程式。" + securityKeyInfo: "您可以設定使用支援FIDO2的硬體安全鎖、終端設備的指纹認證或者PIN碼來登入。" + chromePasskeyNotSupported: "目前不支援Chrome的Passkey。" + registerSecurityKey: "註冊安全金鑰・Passkey" securityKeyName: "輸入金鑰名稱" - tapSecurityKey: "按照瀏覽器的說明註冊安全金鑰或通行金鑰。" + tapSecurityKey: "按照瀏覽器的說明操作,註冊安全金鑰和Passkey。" removeKey: "刪除安全金鑰" removeKeyConfirm: "要刪除{name}嗎?" whyTOTPOnlyRenew: "如果註冊了安全金鑰,則無法解除驗證應用程式的設定。" @@ -2271,25 +1670,19 @@ _2fa: renewTOTPConfirm: "目前驗證應用程式的驗證碼將無法使用。" renewTOTPOk: "重設" renewTOTPCancel: "現在不要" - checkBackupCodesBeforeCloseThisWizard: "請先確認下列備用驗證碼,再關閉此精靈視窗。" - backupCodes: "備用驗證碼" - backupCodesDescription: "如果驗證應用程式不能用了,可以使用以下的備用驗證碼存取您的帳戶。請務必妥善保管這個驗證碼。每個驗證碼只能使用一次。" - backupCodeUsedWarning: "已使用備用驗證碼。如果無法使用驗證應用程式,請盡快重新設定。" - backupCodesExhaustedWarning: "已使用所有備用驗證碼。如果無法使用驗證應用程式,則將無法再存取您的帳戶。請重新設定您的驗證應用程式。" - moreDetailedGuideHere: "請點擊此處查看詳細說明。" _permissions: "read:account": "查看我的帳戶資訊" "write:account": "更改我的帳戶資訊" - "read:blocks": "查看封鎖名單" - "write:blocks": "編輯封鎖名單" + "read:blocks": "已封鎖用戶名單" + "write:blocks": "編輯已封鎖用戶名單" "read:drive": "存取雲端硬碟" "write:drive": "編輯雲端硬碟的檔案" "read:favorites": "瀏覽我的最愛" "write:favorites": "編輯我的最愛列表" - "read:following": "查看追隨中的使用者資訊" - "write:following": "追隨/解除追隨" + "read:following": "查看追隨中的用戶資訊" + "write:following": "追隨/解除追隨" "read:messaging": "顯示訊息" - "write:messaging": "撰寫或刪除訊息" + "write:messaging": "撰寫或刪除私人訊息" "read:mutes": "顯示已靜音列表" "write:mutes": "編輯已靜音列表" "write:notes": "撰寫或刪除貼文" @@ -2306,153 +1699,93 @@ _permissions: "write:user-groups": "編輯使用者群組" "read:channels": "已查看的頻道" "write:channels": "編輯頻道" - "read:gallery": "瀏覽相簿" - "write:gallery": "編輯相簿" - "read:gallery-likes": "瀏覽相簿的讚" - "write:gallery-likes": "編輯相簿的讚" - "read:flash": "檢視 Play" - "write:flash": "編輯 Play" - "read:flash-likes": "檢視 Play 的讚" - "write:flash-likes": "編輯 Play 的讚" - "read:admin:abuse-user-reports": "查看來自使用者的檢舉" - "write:admin:delete-account": "刪除使用者帳戶" - "write:admin:delete-all-files-of-a-user": "刪除使用者的所有檔案" - "read:admin:index-stats": "查看資料庫索引的相關資訊" - "read:admin:table-stats": "查看資料庫表格的相關資訊" - "read:admin:user-ips": "查看使用者的 IP 位址" - "read:admin:meta": "查看實例的詮釋資料" - "write:admin:reset-password": "重設使用者的密碼" - "write:admin:resolve-abuse-user-report": "解決來自使用者的檢舉" - "write:admin:send-email": "發送郵件" - "read:admin:server-info": "查看伺服器的資訊" - "read:admin:show-moderation-log": "查看審查紀錄" - "read:admin:show-user": "查看使用者的私密資訊" - "write:admin:suspend-user": "凍結使用者" - "write:admin:unset-user-avatar": "刪除使用者的頭像" - "write:admin:unset-user-banner": "刪除使用者的橫幅" - "write:admin:unsuspend-user": "解除凍結使用者" - "write:admin:meta": "編輯實例的詮釋資料" - "write:admin:user-note": "編輯審查筆記" - "write:admin:roles": "編輯角色" - "read:admin:roles": "查看角色" - "write:admin:relays": "編輯中繼器" - "read:admin:relays": "查看中繼器" - "write:admin:invite-codes": "編輯邀請碼" - "read:admin:invite-codes": "查看邀請碼" - "write:admin:announcements": "編輯公告" - "read:admin:announcements": "查看公告" - "write:admin:avatar-decorations": "編輯頭像裝飾" - "read:admin:avatar-decorations": "查看頭像裝飾" - "write:admin:federation": "編輯站台聯邦的相關資訊" - "write:admin:account": "編輯使用者帳戶" - "read:admin:account": "查看使用者的相關資訊" - "write:admin:emoji": "編輯表情符號" - "read:admin:emoji": "查看表情符號" - "write:admin:queue": "編輯工作佇列" - "read:admin:queue": "查看工作佇列的相關資訊" - "write:admin:promo": "編輯推廣貼文" - "write:admin:drive": "編輯使用者的雲端硬碟" - "read:admin:drive": "查看使用者雲端硬碟的相關資訊" - "read:admin:stream": "使用管理員的 Websocket API" - "write:admin:ad": "編輯廣告" - "read:admin:ad": "查看廣告" - "write:invite-codes": "建立邀請碼" - "read:invite-codes": "取得邀請碼" - "write:clip-favorite": "編輯摘錄的讚" - "read:clip-favorite": "查看摘錄的讚" - "read:federation": "查看站台聯邦的相關資訊" - "write:report-abuse": "檢舉違規行為" - "write:chat": "撰寫或刪除訊息" - "read:chat": "查看聊天訊息" + "read:gallery": "瀏覽圖庫" + "write:gallery": "操作圖庫" + "read:gallery-likes": "讀取喜歡的圖片" + "write:gallery-likes": "操作喜歡的圖片" _auth: shareAccessTitle: "應用程式的存取權限" shareAccess: "要授權「“{name}”」存取您的帳戶嗎?" - shareAccessAsk: "您確定要授權這個應用程式存取您的帳戶嗎?" + shareAccessAsk: "您確定要授權這個應用程式使用您的帳戶嗎?" permission: "{name}要求以下的權限" permissionAsk: "此應用程式需要以下權限" pleaseGoBack: "請返回至應用程式" callback: "回到應用程式" - accepted: "已授予存取權限" denied: "拒絕訪問" - scopeUser: "以下列使用者身分操作" pleaseLogin: "必須登入以提供應用程式的存取權限。" - byClickingYouWillBeRedirectedToThisUrl: "如果授予存取權限,就會自動導向到以下的網址" _antennaSources: all: "全部貼文" homeTimeline: "來自已追隨使用者的貼文" users: "來自特定使用者的貼文" userList: "來自特定清單中的貼文" - userBlacklist: "除指定使用者外的所有貼文" _weekday: - sunday: "星期天" - monday: "星期一" - tuesday: "星期二" - wednesday: "星期三" - thursday: "星期四" - friday: "星期五" - saturday: "星期六" + sunday: "週日" + monday: "週一" + tuesday: "週二" + wednesday: "週三" + thursday: "週四" + friday: "週五" + saturday: "週六" _widgets: profile: "個人檔案" - instanceInfo: "伺服器資訊" + instanceInfo: "實例資訊" memo: "備忘錄" notifications: "通知" timeline: "時間軸" calendar: "行事曆" - trends: "熱門貼文" + trends: "發燒貼文" clock: "時鐘" - rss: "RSS 閱讀器" - rssTicker: "RSS 跑馬燈" + rss: "RSS閱讀器" + rssTicker: "RSS跑馬燈" activity: "動態" photos: "照片" digitalClock: "電子時鐘" - unixClock: "UNIX 時間" - federation: "聯邦宇宙" - instanceCloud: "伺服器雲" - postForm: "發文視窗" + unixClock: "UNIX時間" + federation: "站台聯邦" + instanceCloud: "實例雲" + postForm: "發佈窗口" slideshow: "幻燈片" button: "按鈕" - onlineUsers: "上線使用者" + onlineUsers: "線上的用戶" jobQueue: "佇列" - serverMetric: "伺服器指標 " - aiscript: "AiScript 控制臺" + serverMetric: "服務器指標 " + aiscript: "AiScript控制台" aiscriptApp: "AiScript App" aichan: "小藍" userList: "使用者列表" _userList: chooseList: "選擇清單" clicker: "點擊器" - birthdayFollowings: "今天生日的使用者" - chat: "聊天" _cw: hide: "隱藏" - show: "顯示內容" - chars: "{count} 個字元" + show: "瀏覽更多" + chars: "{count}字元" files: "{count} 個檔案" _poll: - noOnlyOneChoice: "需要至少兩個選項。" - choiceN: "選項 {n}" + noOnlyOneChoice: "至少需要兩個選項。" + choiceN: "選擇{n}" noMore: "沒辦法再添加選項了" - canMultipleVote: "允許複選" + canMultipleVote: "可以多次投票" expiration: "期限" infinite: "無期限" at: "結束時間" - after: "指定時效" + after: "進度指定 " deadlineDate: "截止日期" deadlineTime: "小時" duration: "時長" votesCount: "{n}票" - totalVotes: "合計 {n} 票" + totalVotes: "一共{n}票" vote: "投票" showResult: "顯示結果" voted: "已投票" closed: "已結束" - remainingDays: "{d} 天 {h} 小時後結束" - remainingHours: "{h} 小時 {m} 分後結束" - remainingMinutes: "{m} 分 {s} 秒後結束" - remainingSeconds: "{s} 秒後截止" + remainingDays: "{d}天{h}小時後結束" + remainingHours: "{h}小時{m}分後結束" + remainingMinutes: "{m}分{s}秒後結束" + remainingSeconds: "{s}秒後截止" _visibility: public: "公開" - publicDescription: "發佈給所有使用者" + publicDescription: "發布給所有用戶 " home: "首頁" homeDescription: "僅發布至首頁的時間軸" followers: "追隨者" @@ -2460,109 +1793,104 @@ _visibility: specified: "指定使用者" specifiedDescription: "僅發布至指定使用者" disableFederation: "停用聯邦" - disableFederationDescription: "不發送到其他伺服器" + disableFederationDescription: "不要傳遞給其他實例" _postForm: replyPlaceholder: "回覆此貼文..." quotePlaceholder: "引用此貼文..." channelPlaceholder: "發佈到頻道" _placeholders: - a: "今天過得如何?" - b: "有什麼新鮮事嗎?" + a: "今天過得如何?" + b: "有什麼新鮮事嗎?" c: "有什麼新鮮想法嗎?" - d: "想要發佈些什麼嗎?" - e: "寫些什麼吧……" - f: "靜待發文……" + d: "想要發布些什麼嗎?" + e: "寫些什麼吧..." + f: "期待你發佈的內容..." _profile: - name: "名字" + name: "名稱" username: "使用者名稱" description: "關於我" youCanIncludeHashtags: "你也可以在「關於我」中加上 #tag" - metadata: "附加資訊" - metadataEdit: "編輯附加資訊" + metadata: "進階資訊" + metadataEdit: "編輯進階資訊" metadataDescription: "可以在個人資料中以表格形式顯示其他資訊。" metadataLabel: "標籤" - metadataContent: "內容" + metadataContent: "内容" changeAvatar: "更換大頭貼" changeBanner: "變更橫幅圖像" - verifiedLinkDescription: "如果輸入包含您個人資料的網站 URL,欄位旁邊將出現驗證圖示。" - avatarDecorationMax: "最多可以設置 {max} 個裝飾。" - followedMessage: "被追隨時的訊息" - followedMessageDescription: "可以設定被追隨時顯示給對方的訊息。" - followedMessageDescriptionForLockedAccount: "如果追隨需要核准的話,將在通過追隨請求之後顯示。" _exportOrImport: allNotes: "所有貼文" favoritedNotes: "「我的最愛」貼文" - clips: "摘錄" followingList: "追隨中" muteList: "靜音" blockingList: "封鎖" userLists: "清單" - excludeMutingUsers: "排除被靜音的使用者" + excludeMutingUsers: "排除被靜音的用戶" excludeInactiveUsers: "排除不活躍帳戶" - withReplies: "將被匯入的追隨中清單的貼文回覆包含在時間軸" _charts: - federation: "聯邦宇宙" + federation: "站台聯邦" apRequest: "請求" - usersIncDec: "使用者增減" - usersTotal: "使用者合計" + usersIncDec: "使用者増減" + usersTotal: "使用者合共" activeUsers: "活躍使用者" notesIncDec: "貼文増減" localNotesIncDec: "本地貼文増減" remoteNotesIncDec: "遠端貼文數目增减" - notesTotal: "貼文總數" - filesIncDec: "檔案增減" - filesTotal: "檔案總數" - storageUsageIncDec: "儲存空間增減" - storageUsageTotal: "儲存空間用量" + notesTotal: "貼文合共" + filesIncDec: "檔案増減" + filesTotal: "累計檔案" + storageUsageIncDec: "儲存空間的増減" + storageUsageTotal: "已使用的儲存空間合共" _instanceCharts: requests: "請求" - users: "使用者增減" - usersTotal: "使用者總數" - notes: "貼文增減" + users: "使用者増減" + usersTotal: "總計使用者" + notes: "貼文増減" notesTotal: "累計貼文" - ff: "追隨/追隨者增減" - ffTotal: "追隨/追隨者總數" - cacheSize: "快取用量增減" - cacheSizeTotal: "快取用量總數" - files: "檔案總數增減" - filesTotal: "檔案總數累計" + ff: "追隨/追隨者的増減" + ffTotal: "追隨/追隨者累計" + cacheSize: "增加或減少快取用量" + cacheSizeTotal: "快取大小總計" + files: "檔案數量的増減" + filesTotal: "檔案數量總計" _timelines: home: "首頁" local: "本地" social: "社交" global: "公開" _play: - new: "新增 Play" - edit: "編輯 Play" - created: "已新增 Play " - updated: "已更新 Play " - deleted: "已刪除 Play" - pageSetting: "Play 設定" - editThisPage: "編輯此 Play" + new: "新增Play" + edit: "編輯Play" + created: "已新增Play" + updated: "已更新Play" + deleted: "已刪除Play" + pageSetting: "Play設定" + editThisPage: "編輯這個Play" viewSource: "檢視原始碼" - my: "自己的 Play" - liked: "按讚的 Play" - featured: "熱門" + my: "自己的Play" + liked: "按了讚的Play" + featured: "人氣" title: "標題" script: "腳本" summary: "描述" - visibilityDescription: "如果您將其設為私密,它將不再顯示在您的個人資料中,但知道該 URL 的人仍然可以存取它。" _pages: newPage: "建立頁面" editPage: "編輯頁面" - readPage: "正在檢視原始碼" + readPage: "正檢視原始碼" + created: "頁面已建立" + updated: "頁面已更新" + deleted: "頁面已被刪除" pageSetting: "頁面設定" - nameAlreadyExists: "該頁面 URL 已存在" - invalidNameTitle: "無效的頁面 URL" + nameAlreadyExists: "指定的頁面URL已經存在" + invalidNameTitle: "指定的頁面URL無效" invalidNameText: "請確定是否為非空白" editThisPage: "編輯此頁面" viewSource: "檢視原始碼" viewPage: "顯示頁面" - like: "讚好" - unlike: "收回讚好" + like: "喜歡" + unlike: "收回喜歡" my: "我的頁面" - liked: "已讚好的頁面" - featured: "熱門" + liked: "已喜歡的頁面" + featured: "人氣" inspector: "面板檢查" contents: "內容" content: "頁面方塊" @@ -2574,27 +1902,24 @@ _pages: hideTitleWhenPinned: "被置頂於個人資料時隱藏頁面標題" font: "字型" fontSerif: "襯線體" - fontSansSerif: "黑體" + fontSansSerif: "無襯線體" eyeCatchingImageSet: "設定封面影像" eyeCatchingImageRemove: "刪除封面影像" chooseBlock: "新增方塊" - enterSectionTitle: "輸入區段的標題" selectType: "選擇類型" contentBlocks: "內容" inputBlocks: "輸入" specialBlocks: "特殊" blocks: - text: "文字" + text: "字串" textarea: "字串區域" section: "區段" image: "圖片" button: "按鈕" - dynamic: "動態方塊" - dynamicDescription: "這個方塊已經廢止,現在開始請使用 {play}。" note: "嵌式貼文" _note: id: "貼文ID" - idDescription: "您也可以貼上貼文 URL 來進行設定。 " + idDescription: "您也可以粘貼筆記 URL 並進行設置。 " detailed: "顯示詳細內容" _relayStatus: requesting: "等待核准" @@ -2608,59 +1933,32 @@ _notification: youRenoted: "{name} 轉發了你的貼文" youWereFollowed: "您有新的追隨者" youReceivedFollowRequest: "您有新的追隨請求" - yourFollowRequestAccepted: "您的追隨請求已被核准" + yourFollowRequestAccepted: "您的追隨請求已通過" pollEnded: "問卷調查已產生結果" - newNote: "新的貼文" unreadAntennaNote: "天線 {name}" - roleAssigned: "已授予角色" - chatRoomInvitationReceived: "您被邀請加入聊天室" emptyPushNotificationMessage: "推送通知已更新" achievementEarned: "獲得成就" - testNotification: "通知測試" - checkNotificationBehavior: "確認通知的顯示行為" - sendTestNotification: "發送測試通知" - notificationWillBeDisplayedLikeThis: "通知會以這樣的方式顯示" - reactedBySomeUsers: "{n}人做出了反應" - likedBySomeUsers: "{n} 人按了讚" - renotedBySomeUsers: "{n}人做了轉發" - followedBySomeUsers: "被{n}人追隨了" - flushNotification: "重置通知歷史紀錄" - exportOfXCompleted: "{x} 的匯出已完成。" - login: "已登入" - createToken: "已產生存取權杖" - createTokenDescription: "如果您不知道,請透過「{text}」刪除存取權杖。" _types: all: "全部 " - note: "使用者的最新貼文" follow: "追隨中" mention: "提及" reply: "回覆" - renote: "轉發" + renote: "轉發貼文" quote: "引用" reaction: "反應" pollEnded: "問卷調查結束" receiveFollowRequest: "已收到追隨請求" followRequestAccepted: "追隨請求已接受" - roleAssigned: "已授予角色" - chatRoomInvitationReceived: "已被邀請加入聊天室" achievementEarned: "獲得成就" - exportCompleted: "已完成匯出。" - login: "登入" - createToken: "建立存取權杖" - test: "通知測試" app: "應用程式通知" _actions: - followBack: "追隨回去" + followBack: "回關" reply: "回覆" renote: "轉發" _deck: alwaysShowMainColumn: "總是顯示主欄" columnAlign: "對齊欄位" - columnGap: "欄與欄之間的邊距" - deckMenuPosition: "多欄模式的選單位置" - navbarPosition: "導覽列位置" addColumn: "新增欄位" - newNoteNotificationSettings: "新貼文通知的設定" configureColumn: "欄位的設定" swapLeft: "向左移動" swapRight: "向右移動" @@ -2671,13 +1969,9 @@ _deck: profile: "個人檔案" newProfile: "新建個人檔案" deleteProfile: "刪除個人檔案" - introduction: "組合多個欄位,製作屬於自己的介面吧!" - introduction2: "您可以隨時按畫面右方的「+」新增欄位。" - widgetsIntroduction: "請從欄位選單中選擇「編輯小工具」新增小工具。" - useSimpleUiForNonRootPages: "用簡易介面顯示非根頁面" - usedAsMinWidthWhenFlexible: "如果啟用「自動調整寬度」,此為最小寬度" - flexible: "自動調整寬度" - enableSyncBetweenDevicesForProfiles: "啟用裝置與裝置之間的設定檔資料同步化" + introduction: "組合欄位來製作屬於自己的介面吧!" + introduction2: "您可以隨時透過按畫面右方的 + 來添加欄位。" + widgetsIntroduction: "請從欄位的選單中,選擇「編輯小工具」來添加小工具" _columns: main: "主列" widgets: "小工具" @@ -2689,431 +1983,26 @@ _deck: mentions: "提及" direct: "指定使用者" roleTimeline: "角色時間軸" - chat: "聊天" _dialog: - charactersExceeded: "您的貼文太長了!現時字數 {current}/限制字數 {max}" - charactersBelow: "您的貼文太短了!現時字數 {current}/限制字數 {min}" + charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}" + charactersBelow: "低於最少字數!現在 {current} / 限制 {max}" _disabledTimeline: - title: "時間軸已停用" - description: "目前角色無法使用這個時間軸。" + title: "停用的時間軸" + description: "目前的角色無法使用這個時間軸。" _drivecleaner: - orderBySizeDesc: "按大小降序排列" - orderByCreatedAtAsc: "按新增日期降序排列" + orderBySizeDesc: "檔案由大到小" + orderByCreatedAtAsc: "依照加入的日期順序" _webhookSettings: createWebhook: "建立 Webhook" - modifyWebhook: "編輯 Webhook" - name: "名字" - secret: "密鑰" - trigger: "觸發器" + name: "名稱" + secret: "秘密" + events: "什麼時候運行Webhook" active: "已啟用" _events: follow: "當你追隨時" followed: "當被追隨時" - note: "當發佈貼文時" + note: "當發布貼文時" reply: "當收到回覆時" renote: "當被轉發時" reaction: "當獲得反應時" mention: "當被提到時" - _systemEvents: - abuseReport: "當使用者檢舉時" - abuseReportResolved: "當處理了使用者的檢舉時" - userCreated: "使用者被新增時" - inactiveModeratorsWarning: "當審查員在一段時間內沒有活動時" - inactiveModeratorsInvitationOnlyChanged: "當審查員在一段時間內不活動時,系統會將模式變更為邀請制" - deleteConfirm: "請問是否要刪除 Webhook?" - testRemarks: "按下切換開關右側的按鈕,就會將假資料發送至 Webhook。" -_abuseReport: - _notificationRecipient: - createRecipient: "新增接收檢舉的通知對象" - modifyRecipient: "編輯接收檢舉的通知對象" - recipientType: "通知對象的種類" - _recipientType: - mail: "電子郵件" - webhook: "Webhook" - _captions: - mail: "寄送到擁有監察員權限的使用者電子郵件地址(僅在收到檢舉時)" - webhook: "向指定的 SystemWebhook 發送通知(在收到檢舉和解決檢舉時發送)" - keywords: "關鍵字" - notifiedUser: "通知的使用者" - notifiedWebhook: "使用的 Webhook" - deleteConfirm: "確定要刪除通知對象嗎?" -_moderationLogTypes: - createRole: "新增角色" - deleteRole: "刪除角色 " - updateRole: "更新角色設定" - assignRole: "指派角色" - unassignRole: "撤銷角色" - suspend: "凍結" - unsuspend: "解除凍結" - addCustomEmoji: "新增自訂表情符號" - updateCustomEmoji: "更新自訂表情符號" - deleteCustomEmoji: "刪除自訂表情符號" - updateServerSettings: "更新伺服器設定" - updateUserNote: "更新了使用者的管理筆記" - deleteDriveFile: "刪除檔案" - deleteNote: "刪除貼文" - createGlobalAnnouncement: "建立全網通知" - createUserAnnouncement: "建立使用者通知" - updateGlobalAnnouncement: "更新全部的公告" - updateUserAnnouncement: "更新使用者的公告" - deleteGlobalAnnouncement: "刪除全部的公告" - deleteUserAnnouncement: "刪除使用者的公告" - resetPassword: "重設密碼" - suspendRemoteInstance: "封鎖遠端伺服器" - unsuspendRemoteInstance: "解除封鎖遠端伺服器" - updateRemoteInstanceNote: "更新了遠端伺服器的管理筆記" - markSensitiveDriveFile: "標記為敏感檔案" - unmarkSensitiveDriveFile: "撤銷標記為敏感檔案" - resolveAbuseReport: "解決檢舉" - forwardAbuseReport: "轉發檢舉" - updateAbuseReportNote: "更新檢舉的審查備註" - createInvitation: "建立邀請碼" - createAd: "建立廣告" - deleteAd: "刪除廣告" - updateAd: "更新廣告" - createAvatarDecoration: "建立頭像裝飾" - updateAvatarDecoration: "更新頭像裝飾" - deleteAvatarDecoration: "刪除頭像裝飾" - unsetUserAvatar: "移除使用者的大頭貼" - unsetUserBanner: "移除使用者的橫幅圖像" - createSystemWebhook: "建立 SystemWebhook" - updateSystemWebhook: "更新 SystemWebhook" - deleteSystemWebhook: "刪除 SystemWebhook" - createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象" - updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象" - deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象" - deleteAccount: "刪除帳戶" - deletePage: "刪除頁面" - deleteFlash: "刪除 Play" - deleteGalleryPost: "刪除相簿的貼文" - deleteChatRoom: "刪除聊天室" - updateProxyAccountDescription: "更新代理帳戶的說明" -_fileViewer: - title: "檔案詳細資訊" - type: "檔案類型 " - size: "檔案大小" - url: "URL" - uploadedAt: "加入日期" - attachedNotes: "含有附件的貼文" - thisPageCanBeSeenFromTheAuthor: "本頁面僅限上傳了這個檔案的使用者可以檢視。" -_externalResourceInstaller: - title: "從外部網站安裝" - checkVendorBeforeInstall: "安裝前請確認提供者是可信賴的。" - _plugin: - title: "要安裝此外掛嘛?" - _theme: - title: "要安裝此佈景主題嗎?" - _meta: - base: "基本配色方案" - _vendorInfo: - title: "提供者資訊" - endpoint: "引用端點" - hashVerify: "確認檔案的完整性" - _errors: - _invalidParams: - title: "缺少參數" - description: "缺少從外部網站取得資料的必要資訊。請檢查 URL 是否正確。" - _resourceTypeNotSupported: - title: "不支援此外部資源。" - description: "不支援從此外部網站取得的資源類型。請聯絡網站管理員。" - _failedToFetch: - title: "無法取得資料" - fetchErrorDescription: "與外部站點的通訊失敗。如果重試後問題仍然存在,請聯絡網站管理員。" - parseErrorDescription: "無法讀取從外部站點取得的資料。請聯絡網站管理員。" - _hashUnmatched: - title: "無法取得正確資料" - description: "所提供資料的完整性驗證失敗。出於安全原因,安裝無法繼續。請聯絡網站管理員。" - _pluginParseFailed: - title: "AiScript 錯誤" - description: "已取得資料但解析 AiScript 時發生錯誤,導致無法載入。請聯絡外掛作者。請檢查 Javascript 控制台以取得錯誤詳細資訊。" - _pluginInstallFailed: - title: "外掛安裝失敗" - description: "安裝外掛時出現問題。請再試一次。可參閱 Javascript 控制台以取得錯誤詳細資訊。" - _themeParseFailed: - title: "佈景主題解析錯誤" - description: "已取得資料但解析佈景主題時發生錯誤,導致無法載入。請聯絡佈景主題的作者。請檢查 Javascript 控制台以取得錯誤詳細資訊。" - _themeInstallFailed: - title: "無法安裝佈景主題" - description: "安裝佈景主題時出現問題。請再試一次。請參閱 Javascript 控制台以取得錯誤詳細資訊。" -_dataSaver: - _media: - title: "載入媒體檔案" - description: "防止自動載入圖片和影片。點擊隱藏的圖片/影片即可載入。" - _avatar: - title: "大頭貼" - description: "停止顯示大頭貼的動畫。由於動畫圖片的檔案大小可能比普通圖片大,這可以進一步減少資料流量。" - _urlPreviewThumbnail: - title: "不顯示網址預覽縮圖" - description: "將不再自動載入網址預覽縮圖。" - _disableUrlPreview: - title: "停用網址預覽" - description: "停用網址預覽功能。與單獨使用縮圖不同,這樣可以減少載入連結資訊本身。" - _code: - title: "程式碼突出顯示" - description: "如果使用了程式碼突顯語法(如 MFM),則在點擊之前不會被載入。由於需要為對應的程式語言下載突顯定義檔案,因此關閉自動載入有助於減少資料流量。" -_hemisphere: - N: "北半球" - S: "南半球" - caption: "某些客戶端的設定會用此來判斷季節。" -_reversi: - reversi: "黑白棋" - gameSettings: "對弈設定" - chooseBoard: "選擇棋盤" - blackOrWhite: "先手/後手" - blackIs: "{name} 為黑棋(先攻)" - rules: "規則" - thisGameIsStartedSoon: "對弈即將開始" - waitingForOther: "等待對手準備就緒" - waitingForMe: "等待您準備就緒" - waitingBoth: "請準備" - ready: "準備就緒" - cancelReady: "重新準備" - opponentTurn: "對手的回合" - myTurn: "您的回合" - turnOf: "{name} 的回合" - pastTurnOf: "{name} 的回合" - surrender: "認輸" - surrendered: "對手認輸" - timeout: "時間到" - drawn: "平手" - won: "{name} 獲勝" - black: "黑" - white: "白" - total: "合計" - turnCount: "{count} 回合" - myGames: "我的對弈" - allGames: "所有對弈" - ended: "已結束" - playing: "正在對弈" - isLlotheo: "子較少的一方為勝(顛倒規則)" - loopedMap: "循環棋盤" - canPutEverywhere: "隨意置放模式" - timeLimitForEachTurn: "每回合的時間限制" - freeMatch: "自由對戰" - lookingForPlayer: "正在搜尋對手" - gameCanceled: "對弈已被取消" - shareToTlTheGameWhenStart: "在遊戲開始時將對弈資訊發布到時間軸" - iStartedAGame: "對弈開始了! #MisskeyReversi" - opponentHasSettingsChanged: "對手更改了設定" - allowIrregularRules: "允許異常規則(完全自由)" - disallowIrregularRules: "不允許異常規則" - showBoardLabels: "在棋盤上顯示行、列號" - useAvatarAsStone: "用大頭貼當作棋子" -_offlineScreen: - title: "離線-無法連接伺服器" - header: "無法連接伺服器" -_urlPreviewSetting: - title: "URL 預覽設定" - enable: "啟用 URL 預覽" - allowRedirect: "允許預覽目標的重新導向" - allowRedirectDescription: "設定當輸入的 URL 發生重新導向時,是否追蹤該重新導向並顯示預覽。若停用此功能,雖可節省伺服器資源,但將無法顯示重新導向後的內容。\n" - timeout: "取得預覽的逾時時間 (ms)" - timeoutDescription: "若取得預覽所需的時間超過這個值,則不會產生預覽。" - maximumContentLength: "Content-Length 的最大値 (byte)" - maximumContentLengthDescription: "若 Content-Length 超過這個值,則不會產生預覽。" - requireContentLength: "僅在能夠取得 Content-Length 時,才產生預覽。" - requireContentLengthDescription: "若對方的伺服器未回傳 Content -Length,則不會產生預覽。" - userAgent: "User-Agent" - userAgentDescription: "設定獲取預覽時使用的 User-Agent 。如果留空,將使用預設的 User-Agent 。" - summaryProxy: "產生預覽的代理端點" - summaryProxyDescription: "使用摘要代理程式而不是 Misskey 本身產生預覽。" - summaryProxyDescription2: "以下參數會作為查詢字串連結到代理。如果代理端不支援,這些設定將被忽略。" -_mediaControls: - pip: "畫中畫" - playbackRate: "播放速度" - loop: "循環播放" -_contextMenu: - title: "內容功能表" - app: "應用程式" - appWithShift: "Shift 鍵應用程式" - native: "瀏覽器的使用者介面" -_gridComponent: - _error: - requiredValue: "此值為必填欄位" - columnTypeNotSupport: "正規表達式驗證僅支援 type:text 的欄位。" - patternNotMatch: "此值不符合 {pattern} 中的樣式。" - notUnique: "此值必須是唯一的" -_roleSelectDialog: - notSelected: "未選擇" -_customEmojisManager: - _gridCommon: - copySelectionRows: "複製選取的行" - copySelectionRanges: "複製選取的範圍" - deleteSelectionRows: "刪除所選的行" - deleteSelectionRanges: "刪除選取範圍的行" - searchSettings: "搜尋設定" - searchSettingCaption: "詳細設定搜尋條件。" - searchLimit: "顯示的數量" - sortOrder: "排序" - registrationLogs: "登錄日誌" - registrationLogsCaption: "會顯示更新或刪除表情符號時的日誌。進行更新或刪除操作,或切換頁面、重新載入後,日誌將會消失。" - alertEmojisRegisterFailedDescription: "更新或刪除表情符號失敗。詳情請查看登錄日誌。" - _logs: - showSuccessLogSwitch: "顯示成功日誌" - failureLogNothing: "沒有失敗的日誌。" - logNothing: "沒有日誌。" - _remote: - selectionRowDetail: "選取行的詳細資訊" - importSelectionRows: "匯入選取的行" - importSelectionRangesRows: "匯入選取範圍的行" - importEmojisButton: "匯入勾選的表情符號" - confirmImportEmojisTitle: "匯入表情符號" - confirmImportEmojisDescription: "將從遠端接收的{count}個表情符號進行匯入。請務必注意表情符號的授權。是否執行此操作?" - _local: - tabTitleList: "已登錄的表情符號列表" - tabTitleRegister: "登錄表情符號" - _list: - emojisNothing: "沒有登錄的表情符號。" - markAsDeleteTargetRows: "將選取的行設為刪除對象" - markAsDeleteTargetRanges: "將選取範圍的行設為刪除對象\n" - alertUpdateEmojisNothingDescription: "沒有選取需要變更的表情符號。" - alertDeleteEmojisNothingDescription: "沒有選取需要刪除的表情符號。" - confirmMovePage: "要移動到其他頁面嗎?" - confirmChangeView: "要更改顯示方式嗎?" - confirmUpdateEmojisDescription: "將更新{count}個表情符號。是否執行此操作?" - confirmDeleteEmojisDescription: "將刪除勾選的{count}個表情符號。是否執行此操作?" - confirmResetDescription: "目前所做的所有變更都會重設。" - confirmMovePageDesciption: "此頁面的表情符號已被更改。 \n若未儲存就直接離開此頁面,則在此頁面進行的所有更改將會被捨棄。" - dialogSelectRoleTitle: "根據表情符號設定的角色進行搜尋" - _register: - uploadSettingTitle: "上傳設定" - uploadSettingDescription: "您可以在此畫面設定表情符號上傳時的操作。" - directoryToCategoryLabel: "在「類別」欄位中輸入目錄名稱" - directoryToCategoryCaption: "拖放目錄時,請在「類別」欄位中輸入目錄名稱。" - confirmRegisterEmojisDescription: "將列表中顯示的表情符號登錄為新的自定表情符號。是否確定?(為避免過高負荷,每次操作最多可登錄{count}個表情符號)" - confirmClearEmojisDescription: "放棄編輯內容並清除列表中顯示的表情符號。是否確定?" - confirmUploadEmojisDescription: "將拖放的{count}個檔案上傳到雲端硬碟。是否執行此操作?" -_embedCodeGen: - title: "自訂嵌入程式碼" - header: "檢視標頭 " - autoload: "自動繼續載入(不建議)" - maxHeight: "最大高度" - maxHeightDescription: "設定為 0 時代表沒有最大值。請指定某個值以避免小工具持續在縱向延伸。" - maxHeightWarn: "最大高度限制已停用(0)。如果這個變更不是您想要的,請將最大高度設定為某個值。" - previewIsNotActual: "由於超出了預覽畫面可顯示的範圍,因此顯示內容會與實際嵌入時有所不同。" - rounded: "圓角" - border: "給外框加上邊框" - applyToPreview: "反映在預覽中" - generateCode: "建立嵌入程式碼" - codeGenerated: "已產生程式碼" - codeGeneratedDescription: "請將產生的程式碼貼到您的網站上。" -_selfXssPrevention: - warning: "警告" - title: "「在此畫面貼上一些內容」完全是個騙局。" - description1: "如果您在此處貼上任何內容,惡意使用者可能會接管您的帳戶或竊取您的個人資訊。" - description2: "如果您不確切知道要貼上的內容,%c 請立即停止工作並關閉此視窗。" - description3: "細節請看這裡。{link}" -_followRequest: - recieved: "收到的請求" - sent: "送出的請求" -_remoteLookupErrors: - _federationNotAllowed: - title: "無法與這個伺服器通訊" - description: "與此伺服器的通訊可能被停用、或封鎖了該伺服器,或被該伺服器封鎖。\n請聯繫您的伺服器管理員。" - _uriInvalid: - title: "URI 不正確" - description: "輸入的 URI 有問題。請檢查是否輸入了 URI 中不能使用的字元。" - _requestFailed: - title: "請求失敗" - description: "與此伺服器的通訊失敗。可能是對方伺服器斷線。 此外,請檢查是否輸入了不正確或不存在的 URI。" - _responseInvalid: - title: "回應不正確" - description: "雖然能夠與這個伺服器通訊,但是取得的資料不正確。" - _noSuchObject: - title: "查無項目" - description: "無法找到所要求的資源,請再次檢查 URI。" -_captcha: - verify: "請通過 CAPTCHA 驗證" - testSiteKeyMessage: "可以輸入網站金鑰和秘密金鑰的測試值來檢查預覽。\n詳細資訊請參閱以下頁面。" - _error: - _requestFailed: - title: "CAPTCHA 請求失敗" - text: "請過一段時間後再執行,或再次檢查設定。" - _verificationFailed: - title: "CAPTCHA 驗證失敗" - text: "請再次檢查設定是否正確。" - _unknown: - title: "CAPTCHA 錯誤" - text: "發生了意外的錯誤。" -_bootErrors: - title: "載入失敗" - serverError: "如果稍等片刻並重新載入後問題仍然存在,請聯絡您的伺服器管理員並提供以下的錯誤 ID。" - solution: "執行以下操作或許可以解決問題。" - solution1: "將瀏覽器和作業系統更新至最新版本" - solution2: "停用廣告攔截器" - solution3: "清除瀏覽器的快取" - solution4: "(Tor 瀏覽器)將 dom.webaudio.enabled 設為 true" - otherOption: "其他選項" - otherOption1: "刪除用戶端設定和快取" - otherOption2: "啟動簡易用戶端" - otherOption3: "啟動修復工具" -_search: - searchScopeAll: "全部" - searchScopeLocal: "本地" - searchScopeServer: "指定伺服器" - searchScopeUser: "指定使用者" - pleaseEnterServerHost: "請輸入伺服器的主機名稱" - pleaseSelectUser: "請選擇使用者" - serverHostPlaceholder: "例:misskey.example.com" -_serverSetupWizard: - installCompleted: "Misskey 的安裝已經完成了!" - firstCreateAccount: "首先,請建立管理者帳戶。" - accountCreated: "已建立管理者帳戶!" - serverSetting: "伺服器設定" - youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "利用這個精靈,可以簡單地最佳化伺服器的設定。" - settingsYouMakeHereCanBeChangedLater: "這裡的設定之後也可以進行更改。\n" - howWillYouUseMisskey: "您打算如何使用 Misskey?\n" - _use: - single: "單人伺服器" - single_description: "作為自己專用的伺服器,單獨使用。\n" - single_youCanCreateMultipleAccounts: "即使作為單人伺服器運行,根據需要也可以創建多個帳戶。\n" - group: "群組伺服器\n" - group_description: "邀請可信賴的其他使用者,共同使用伺服器。\n" - open: "開放式伺服器" - open_description: "運營時接納不特定多數的使用者。" - openServerAdvice: "接納不特定多數使用者會帶來風險。為了能夠有效處理問題,建議建立完善的審查機制來進行運營。\n" - openServerAntiSpamAdvice: "為了防止自家伺服器成為垃圾郵件的跳板,必須啟用如 reCAPTCHA 等反機器人功能,並對安全性保持高度警覺。\n" - howManyUsersDoYouExpect: "您預計有多少人使用呢?\n" - _scale: - small: "100人以下(小規模)\n" - medium: "100人以上1000人以下(中規模)\n" - large: "1000人以上(大規模)\n" - largeScaleServerAdvice: "在大規模伺服器中,可能需要具備高階基礎設施知識,如負載平衡和資料庫複寫等。\n" - doYouConnectToFediverse: "您要連接到聯邦宇宙(Fediverse)嗎?\n" - doYouConnectToFediverse_description1: "連接到由分散型伺服器構成的網絡(聯邦宇宙)後,您可以與其他伺服器進行內容的互相交流。\n" - doYouConnectToFediverse_description2: "連接到聯邦宇宙被稱為「聯邦」。\n" - youCanConfigureMoreFederationSettingsLater: "您可以在稍後進行更高級的設定,例如指定可以聯繫的伺服器等。\n" - adminInfo: "管理員資訊" - adminInfo_description: "設定用於接收查詢的管理者資訊。\n" - adminInfo_mustBeFilled: "當設置為開放伺服器或啟用聯邦時,必須填寫此資訊。\n" - followingSettingsAreRecommended: "建議使用下列設定" - applyTheseSettings: "套用此設定" - skipSettings: "跳過設定" - settingsCompleted: "設定完成!" - settingsCompleted_description: "辛苦了!準備已經完成,您可以立即開始使用伺服器了。\n" - settingsCompleted_description2: "詳細的伺服器設定可透過「控制臺」進行。" - donationRequest: "請求捐款" - _donationRequest: - text1: "Misskey 是由志願者開發的免費軟體。" - text2: "為了能夠繼續開發,若您願意的話,請考慮進行捐款。\n" - text3: "也有提供支援者專屬的特典!\n" -_uploader: - compressedToX: "壓縮為 {x}" - savedXPercent: "節省了 {x}%" - abortConfirm: "有些檔案尚未上傳,您要中止嗎?" - doneConfirm: "有些檔案尚未上傳,是否要完成上傳?" - maxFileSizeIsX: "可上傳的最大檔案大小為 {x}。" - allowedTypes: "可上傳的檔案類型" - tip: "檔案尚未上傳。您可以在此對話框中進行上傳前的確認、重新命名、壓縮、裁切等操作。準備完成後,請點選「上傳」按鈕開始上傳。\n" -_clientPerformanceIssueTip: - title: "如果覺得電池消耗過快的話" - makeSureDisabledAdBlocker: "請將廣告阻擋器停用" - makeSureDisabledAdBlocker_description: "廣告阻擋器可能會影響效能。請確認作業系統功能、瀏覽器設定或擴充功能中是否啟用了廣告阻擋器。\n" - makeSureDisabledCustomCss: "請停用自訂 CSS" - makeSureDisabledCustomCss_description: "覆蓋樣式可能會影響效能。請確認是否啟用了自訂 CSS 或其他會覆蓋樣式的擴充功能。\n" - makeSureDisabledAddons: "請停用擴充功能" - makeSureDisabledAddons_description: "部分擴充功能可能會干擾用戶端的運作並影響效能。請嘗試停用瀏覽器的擴充功能,以確認是否能改善情況" -_clip: - tip: "摘錄是一項可以用來整理貼文的功能。" -_userLists: - tip: "您可以建立包含任意使用者的清單。建立後的清單可以作為時間軸顯示。\n" diff --git a/misskey-assets b/misskey-assets new file mode 160000 index 0000000000..0179793ec8 --- /dev/null +++ b/misskey-assets @@ -0,0 +1 @@ +Subproject commit 0179793ec891856d6f37a3be16ba4c22f67a81b5 diff --git a/package.json b/package.json index 5c191a9e60..8222789408 100644 --- a/package.json +++ b/package.json @@ -1,88 +1,69 @@ { "name": "misskey", - "version": "2025.6.0", + "version": "13.14.0-beta.2", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@10.11.0", + "packageManager": "pnpm@8.6.0", "workspaces": [ - "packages/frontend-shared", "packages/frontend", - "packages/frontend-embed", - "packages/icons-subsetter", "packages/backend", - "packages/sw", - "packages/misskey-js", - "packages/misskey-reversi", - "packages/misskey-bubble-game" + "packages/sw" ], "private": true, "scripts": { "build-pre": "node ./scripts/build-pre.js", - "build-assets": "node ./scripts/build-assets.mjs", - "build": "pnpm build-pre && pnpm -r build && pnpm build-assets", + "build": "pnpm build-pre && pnpm -r build && pnpm gulp", "build-storybook": "pnpm --filter frontend build-storybook", - "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", - "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", - "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", + "start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js", + "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js", "init": "pnpm migrate", "migrate": "cd packages/backend && pnpm migrate", - "revert": "cd packages/backend && pnpm revert", "check:connect": "cd packages/backend && pnpm check:connect", "migrateandstart": "pnpm migrate && pnpm start", + "gulp": "pnpm exec gulp build", "watch": "pnpm dev", - "dev": "node scripts/dev.mjs", + "dev": "node ./scripts/dev.mjs", "lint": "pnpm -r lint", - "cy:open": "pnpm cypress open --config-file=cypress.config.ts", + "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:run": "pnpm cypress run", "e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", - "e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run", "jest": "cd packages/backend && pnpm jest", "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", "test": "pnpm -r test", "test-and-coverage": "pnpm -r test-and-coverage", + "format": "pnpm exec gulp format", "clean": "node ./scripts/clean.js", "clean-all": "node ./scripts/clean-all.js", "cleanall": "pnpm clean-all" }, "resolutions": { - "chokidar": "4.0.3", + "chokidar": "3.5.3", "lodash": "4.17.21" }, "dependencies": { - "cssnano": "7.0.7", - "esbuild": "0.25.4", - "execa": "9.5.3", - "fast-glob": "3.3.3", - "glob": "11.0.2", - "ignore-walk": "7.0.0", + "execa": "7.1.1", + "gulp": "4.0.2", + "gulp-cssnano": "2.1.3", + "gulp-rename": "2.0.0", + "gulp-replace": "1.1.4", + "gulp-terser": "2.1.0", "js-yaml": "4.1.0", - "postcss": "8.5.3", - "tar": "7.4.3", - "terser": "5.39.2", - "typescript": "5.8.3" + "typescript": "5.1.6" }, "devDependencies": { - "@misskey-dev/eslint-plugin": "2.1.0", - "@types/node": "22.15.21", - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", + "@types/gulp": "4.0.10", + "@types/gulp-rename": "2.0.1", + "@typescript-eslint/eslint-plugin": "5.61.0", + "@typescript-eslint/parser": "5.61.0", "cross-env": "7.0.3", - "cypress": "14.4.0", - "eslint": "9.27.0", - "globals": "16.1.0", - "ncp": "2.0.0", - "pnpm": "10.11.0", - "start-server-and-test": "2.0.12" + "cypress": "12.17.0", + "eslint": "8.44.0", + "start-server-and-test": "2.0.0" }, "optionalDependencies": { - "@tensorflow/tfjs-core": "4.22.0" - }, - "pnpm": { - "overrides": { - "@aiscript-dev/aiscript-languageserver": "-" - } + "@tensorflow/tfjs-core": "4.4.0" } } diff --git a/packages/backend/.eslintignore b/packages/backend/.eslintignore new file mode 100644 index 0000000000..790eb90145 --- /dev/null +++ b/packages/backend/.eslintignore @@ -0,0 +1,4 @@ +node_modules +/built +/.eslintrc.js +/@types/**/* diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs new file mode 100644 index 0000000000..f9fe4814e6 --- /dev/null +++ b/packages/backend/.eslintrc.cjs @@ -0,0 +1,32 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json', './test/tsconfig.json'], + }, + extends: [ + '../shared/.eslintrc.js', + ], + 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/.swcrc b/packages/backend/.swcrc index f4bf7a4d2a..0504a2d389 100644 --- a/packages/backend/.swcrc +++ b/packages/backend/.swcrc @@ -1,5 +1,5 @@ { - "$schema": "https://swc.rs/schema.json", + "$schema": "https://json.schemastore.org/swcrc", "jsc": { "parser": { "syntax": "typescript", @@ -19,6 +19,5 @@ }, "target": "es2022" }, - "minify": false, - "sourceMaps": "inline" + "minify": false } diff --git a/packages/backend/assets/api-doc.html b/packages/backend/assets/api-doc.html deleted file mode 100644 index 19e0349d47..0000000000 --- a/packages/backend/assets/api-doc.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - Misskey API - - - - - - - - - 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 index 11c42dd354..48499319b4 100644 --- a/packages/backend/migration/1677054292210-ad4.js +++ b/packages/backend/migration/1677054292210-ad4.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class ad1677054292210 { name = 'ad1677054292210'; async up(queryRunner) { 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 index 77d1934925..12406fe085 100644 --- a/packages/backend/migration/1688280713783-add-meta-options.js +++ b/packages/backend/migration/1688280713783-add-meta-options.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class AddMetaOptions1688280713783 { name = 'AddMetaOptions1688280713783' 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 index 90d453418b..cdce0dae09 100644 --- a/packages/backend/migration/1689102832143-nsfw-cache.js +++ b/packages/backend/migration/1689102832143-nsfw-cache.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class NsfwCache1689102832143 { name = 'NsfwCache1689102832143' 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..5da7101750 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,243 +4,216 @@ "private": true, "type": "module", "engines": { - "node": "^20.10.0 || ^22.0.0" + "node": ">=18.16.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.367.0", + "@aws-sdk/lib-storage": "3.367.0", + "@aws-sdk/node-http-handler": "3.360.0", + "@bull-board/api": "5.6.0", + "@bull-board/fastify": "5.6.0", + "@bull-board/ui": "5.6.0", + "@discordapp/twemoji": "14.1.2", + "@fastify/accepts": "4.2.0", + "@fastify/cookie": "8.3.0", + "@fastify/cors": "8.3.0", + "@fastify/http-proxy": "9.2.1", + "@fastify/multipart": "7.7.0", + "@fastify/static": "6.10.2", + "@fastify/view": "8.0.0", + "@nestjs/common": "10.0.5", + "@nestjs/core": "10.0.5", + "@nestjs/testing": "10.0.5", "@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.68", "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", + "async-mutex": "^0.4.0", + "autwh": "0.1.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", - "body-parser": "1.20.3", - "bullmq": "5.53.0", + "bullmq": "4.2.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.19.2", "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": "10.0.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", + "ioredis": "5.3.2", + "ip-cidr": "3.1.0", + "ipaddr.js": "2.1.0", + "is-svg": "5.0.0", "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", "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.3", + "parse5": "7.1.2", + "pg": "8.11.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.6", "tinycolor2": "1.6.0", - "tmp": "0.2.3", - "tsc-alias": "1.8.16", + "tmp": "0.2.1", + "tsc-alias": "1.8.7", "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.6", + "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.6.1", + "@swc/jest": "0.2.26", + "@types/accepts": "1.3.5", + "@types/archiver": "5.3.2", + "@types/bcryptjs": "2.4.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/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.4.0", + "@types/node-fetch": "3.0.3", + "@types/nodemailer": "6.4.8", + "@types/oauth": "0.9.1", + "@types/pg": "8.10.2", + "@types/pug": "2.0.6", + "@types/punycode": "2.1.0", + "@types/qrcode": "1.5.1", + "@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/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.61.0", + "@typescript-eslint/parser": "5.61.0", + "aws-sdk-client-mock": "3.0.0", "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.44.0", + "eslint-plugin-import": "2.27.5", + "execa": "7.1.1", + "jest": "29.6.1", + "jest-mock": "29.6.1" } } 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/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..2a23757253 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 { 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..69d83b13b0 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? @@ -331,14 +301,14 @@ export class AccountMoveService { */ @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..c0596446dd 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -1,22 +1,18 @@ -/* - * 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() @@ -25,14 +21,17 @@ export class AiService { 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) { @@ -41,7 +40,6 @@ export class AiService { } const tf = await import('@tensorflow/tfjs-node'); - tf.env().global.fetch = fetch; if (this.model == null) { await this.modelLoadMutex.runExclusive(async () => { @@ -65,22 +63,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..9310fd8b52 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,50 +109,26 @@ 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 @@ -220,41 +192,6 @@ export class AntennaService implements OnApplicationShutdown { 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..6ccaec26ba 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'; 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..cd6b68e721 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,50 @@ 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 + const localUserByIdCache = new MemoryKVCache(1000 * 60 * 60 * 6 /* 6h */); + this.localUserByIdCache = localUserByIdCache; - this.userProfileCache = new RedisKVCache(this.redisClient, 'userProfile', { + // ローカルユーザーならlocalUserByIdCacheにデータを追加し、こちらにはid(文字列)だけを追加する + const userByIdCache = new MemoryKVCache(1000 * 60 * 60 * 6 /* 6h */, { + toMapConverter: user => { + if (user.host === null) { + localUserByIdCache.set(user.id, user as LocalUser); + return user.id; + } + + return user; + }, + fromMapConverter: userOrId => typeof userOrId === 'string' ? localUserByIdCache.get(userOrId) : userOrId, + }); + this.userByIdCache = userByIdCache; + + this.localUserByNativeTokenCache = new MemoryKVCache(Infinity, { + toMapConverter: user => { + if (user === null) return null; + + localUserByIdCache.set(user.id, user); + return user.id; + }, + fromMapConverter: id => id === null ? null : localUserByIdCache.get(id), + }); + this.uriPersonCache = new MemoryKVCache(Infinity, { + toMapConverter: user => { + if (user === null) return null; + + userByIdCache.set(user.id, user); + return user.id; + }, + fromMapConverter: id => id === null ? null : userByIdCache.get(id), + }); + + 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 +131,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 +155,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 === 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 +183,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 +192,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 +209,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..10cfdba254 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 @@ -108,298 +39,49 @@ export class CaptchaService { @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..0bfbe2b173 --- /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..661d956bd6 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), }); @@ -268,7 +201,7 @@ export class CustomEmojiService implements OnApplicationShutdown { } @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), }, { @@ -284,7 +217,7 @@ export class CustomEmojiService implements OnApplicationShutdown { } @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..3a0592441b 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)); + await this.userSuspendService.doPostSuspend(user).catch(e => {}); - 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, - }); - } + 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..a04e9c1225 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,18 +133,7 @@ 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}`); @@ -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', - }; - } + 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 }; - 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 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..a762038942 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,14 +35,13 @@ 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); @@ -56,11 +50,11 @@ export class FederatedInstanceService implements OnApplicationShutdown { 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; @@ -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 }) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index ce3af7c774..9e8d17442f 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -1,14 +1,8 @@ -/* - * 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 Logger from '@/logger.js'; import { DI } from '@/di-symbols.js'; import { LoggerService } from '@/core/LoggerService.js'; @@ -16,6 +10,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import type { DOMWindow } from 'jsdom'; +import * as Redis from 'ioredis'; type NodeInfo = { openRegistrations?: unknown; @@ -51,47 +46,33 @@ export class FetchInstanceMetadataService { } @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) - ); + public async tryLock(host: string): Promise { + const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET'); + return mutex !== '1'; } @bindThis - // public for test - public unlock(host: string): Promise { - return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`); + public unlock(host: string): Promise<'OK'> { + return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0'); } @bindThis - public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise { + public async fetchInstanceMetadata(instance: Instance, force = false): Promise { const host = instance.host; - - // finallyでunlockされてしまうのでtry内でロックチェックをしない - // (returnであってもfinallyは実行される) - if (!force && await this.tryLock(host) === '1') { - // 1が返ってきていたらロックされているという意味なので、何もしない - return; - } - + // Acquire mutex to ensure no parallel runs + if (!await this.tryLock(host)) return; try { if (!force) { - const _instance = await this.federatedInstanceService.fetchOrRegister(host); + const _instance = await this.federatedInstanceService.fetch(host); const now = Date.now(); if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { // unlock at the finally caluse return; } } - + this.logger.info(`Fetching metadata of ${instance.host} ...`); - + const [info, dom, manifest] = await Promise.all([ this.fetchNodeinfo(instance).catch(() => null), this.fetchDom(instance).catch(() => null), @@ -122,7 +103,7 @@ export class FetchInstanceMetadataService { 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 && !icon.includes('data:image/png;base64')) ? icon : favicon; if (favicon) updates.faviconUrl = favicon; if (themeColor) updates.themeColor = themeColor; @@ -137,7 +118,7 @@ export class FetchInstanceMetadataService { } @bindThis - private async fetchNodeinfo(instance: MiInstance): Promise { + private async fetchNodeinfo(instance: Instance): Promise { this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); try { @@ -154,12 +135,12 @@ export class FetchInstanceMetadataService { throw new Error('No wellknown links'); } - const links = wellknown.links as ({ rel: string, href: string; })[]; + const links = wellknown.links as any[]; - 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 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'); @@ -181,7 +162,7 @@ export class FetchInstanceMetadataService { } @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; @@ -195,7 +176,7 @@ export class FetchInstanceMetadataService { } @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'; @@ -206,7 +187,7 @@ export class FetchInstanceMetadataService { } @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) { @@ -232,7 +213,7 @@ export class FetchInstanceMetadataService { } @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; @@ -261,7 +242,7 @@ export class FetchInstanceMetadataService { } @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) { @@ -273,7 +254,7 @@ export class FetchInstanceMetadataService { } @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; @@ -298,7 +279,7 @@ export class FetchInstanceMetadataService { } @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; diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index 6250d4d3a1..d43575b336 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; }); @@ -191,7 +162,7 @@ export class FileInfoService { 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; @@ -289,6 +260,7 @@ export class FileInfoService { private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator { const watcher = new FSWatcher({ cwd, + disableGlobbing: true, }); let finished = false; command.once('end', () => { @@ -342,34 +314,6 @@ export class FileInfoService { 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, @@ -441,7 +371,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 +381,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 +390,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 +403,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 +418,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..4f2c261140 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) @@ -152,20 +43,17 @@ export class HttpRequestService { 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, - }; + lookup: cache.lookup, + } as http.AgentOptions); - this.httpNative = new http.Agent(agentOption); - - this.httpsNative = new https.Agent(agentOption); - - this.http = new HttpRequestServiceAgent(config, agentOption); - - this.https = new HttpsRequestServiceAgent(config, agentOption); + this.https = new https.Agent({ + keepAlive: true, + keepAliveMsecs: 30 * 1000, + lookup: cache.lookup, + } as https.AgentOptions); const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); @@ -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..4d129407cb 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 { - 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; + public genId(date?: Date): string { + if (!date || (date > new Date())) date = new Date(); 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..2e047dc5c1 --- /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..aae0a9134b 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,12 @@ 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', }, @@ -84,13 +74,13 @@ export class MetaService implements OnApplicationShutdown { // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う const saved = await transactionalEntityManager .upsert( - MiMeta, + Meta, { id: 'x', }, ['id'], ) - .then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0])); + .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); this.cache = saved; return saved; @@ -99,52 +89,32 @@ export class MetaService implements OnApplicationShutdown { } @bindThis - public async update(data: Partial): Promise { - let before: MiMeta | undefined; - + public async update(data: Partial): Promise { const updated = await this.db.transaction(async transactionalEntityManager => { - const metas = await transactionalEntityManager.find(MiMeta, { + const metas = await transactionalEntityManager.find(Meta, { order: { id: 'DESC', }, }); - before = metas[0]; + const meta = metas[0]; - if (before) { - await transactionalEntityManager.update(MiMeta, before.id, data); - } else { - await transactionalEntityManager.save(MiMeta, { - ...data, - id: 'x', + if (meta) { + await transactionalEntityManager.update(Meta, meta.id, data); + + 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..38aaa84524 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( @@ -38,8 +28,6 @@ export class MfmService { // 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 = ''; @@ -50,7 +38,7 @@ export class MfmService { 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'; @@ -62,7 +50,7 @@ export class MfmService { return ''; } - function appendChildren(childNodes: ChildNode[]): void { + function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { if (childNodes) { for (const n of childNodes) { analyze(n); @@ -70,16 +58,14 @@ export class MfmService { } } - 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': { @@ -87,15 +73,16 @@ export class MfmService { 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('@'); @@ -107,7 +94,7 @@ export class MfmService { } else if (part.length === 3) { text += txt; } - // その他 + // その他 } else { const generateLink = () => { if (!href && !txt) { @@ -135,7 +122,8 @@ export class MfmService { break; } - case 'h1': { + case 'h1': + { text += '【'; appendChildren(node.childNodes); text += '】\n'; @@ -143,14 +131,16 @@ export class MfmService { } case 'b': - case 'strong': { + case 'strong': + { text += '**'; appendChildren(node.childNodes); text += '**'; break; } - case 'small': { + case 'small': + { text += ''; appendChildren(node.childNodes); text += ''; @@ -158,7 +148,8 @@ export class MfmService { } case 's': - case 'del': { + case 'del': + { text += '~~'; appendChildren(node.childNodes); text += '~~'; @@ -166,46 +157,14 @@ export class MfmService { } 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') {
@@ -240,7 +199,8 @@ export class MfmService {
 				case 'h3':
 				case 'h4':
 				case 'h5':
-				case 'h6': {
+				case 'h6':
+				{
 					text += '\n\n';
 					appendChildren(node.childNodes);
 					break;
@@ -253,7 +213,8 @@ export class MfmService {
 				case 'article':
 				case 'li':
 				case 'dt':
-				case 'dd': {
+				case 'dd':
+				{
 					text += '\n';
 					appendChildren(node.childNodes);
 					break;
@@ -269,29 +230,21 @@ 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');
@@ -318,69 +271,9 @@ export class MfmService {
 			},
 
 			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) => {
@@ -441,10 +334,8 @@ export class MfmService {
 			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;
@@ -457,10 +348,6 @@ export class MfmService {
 			},
 
 			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));
 
@@ -492,17 +379,8 @@ export class MfmService {
 			},
 		};
 
-		appendChildren(nodes, body);
+		appendChildren(nodes, doc.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; + 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..4f3d66dd58 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,30 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.reply) { // 通知 if (data.reply.userHost === null) { - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ + const isThreadMuted = await this.noteThreadMutingsRepository.exist({ where: { userId: data.reply.userId, threadId: data.reply.threadId ?? data.reply.id, - }, + } }); if (!isThreadMuted) { 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 +603,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 +647,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.relayService.deliverToRelays(user, noteActivity); } - trackPromise(dm.execute()); + dm.execute(); })(); } //#endregion @@ -744,50 +676,45 @@ export class NoteCreateService implements OnApplicationShutdown { } @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({ + const isThreadMuted = await this.noteThreadMutingsRepository.exist({ where: { userId: u.id, threadId: note.threadId ?? note.id, @@ -803,7 +730,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 +744,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 +760,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 +778,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 +795,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..f77ea8aab4 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,10 @@ 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 { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; @Injectable() export class NoteDeleteService { @@ -30,9 +25,6 @@ export class NoteDeleteService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -43,13 +35,14 @@ 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 metaService: MetaService, private searchService: SearchService, - private moderationLogService: ModerationLogService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private instanceChart: InstanceChart, @@ -60,10 +53,16 @@ export class NoteDeleteService { * @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 +74,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,7 +90,7 @@ export class NoteDeleteService { this.deliverToConcerned(user, note, content); } - // also deliver delete activity to cascaded notes + // also deliever 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) { if (!cascadingNote.user) continue; @@ -101,20 +100,20 @@ 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); + } + }); } } @@ -127,22 +126,11 @@ export class NoteDeleteService { 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): Promise { + const recursive = async (noteId: string): Promise => { const query = this.notesRepository.createQueryBuilder('note') .where('note.replyId = :noteId', { noteId }) .orWhere(new Brackets(q => { @@ -158,13 +146,13 @@ export class NoteDeleteService { ].flat(); }; - const cascadingNotes: MiNote[] = await recursive(note.id); + const cascadingNotes: Note[] = await recursive(note.id); return cascadingNotes; } @bindThis - private async getMentionedRemoteUsers(note: MiNote) { + private async getMentionedRemoteUsers(note: Note) { const where = [] as any[]; // mention / reply / dm @@ -186,30 +174,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..52e9bd369a --- /dev/null +++ b/packages/backend/src/core/NoteReadService.ts @@ -0,0 +1,136 @@ +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 isThreadMuted = await this.noteThreadMutingsRepository.exist({ + where: { + userId: userId, + threadId: note.threadId ?? note.id, + }, + }); + if (isThreadMuted) 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.exist({ where: { id: unread.id } }); + + if (!exist) 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..8e25f82284 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -1,54 +1,48 @@ -/* - * 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}`); @@ -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..be19400052 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,7 +37,7 @@ 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'); @@ -72,8 +67,10 @@ export class PollService { 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, @@ -90,12 +87,10 @@ 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'); 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..e1c3d3943c 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 @@ -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,12 +68,14 @@ 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); @@ -100,7 +96,7 @@ 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) => { @@ -114,19 +110,12 @@ export class PushNotificationService implements OnApplicationShutdown { 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..435d5d2389 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,39 +104,61 @@ export class QueryService { } @bindThis - public generateMutedNoteThreadQuery(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { + 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.setParameters(mutedQuery.getParameters()); + } + + @bindThis + 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.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 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') @@ -210,36 +169,27 @@ export class QueryService { // 投稿の返信先の作者をミュートしていない かつ // 投稿の引用元の作者をミュートしていない 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()); @@ -247,7 +197,7 @@ export class QueryService { } @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 }); @@ -258,52 +208,73 @@ export class QueryService { } @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 }); @@ -321,59 +292,4 @@ export class QueryService { 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..e1da0516d1 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, DeliverJobData, 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,34 +69,22 @@ 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 = { 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, }); } @@ -173,30 +98,21 @@ export class QueueService { @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, - }, + removeOnComplete: true, + removeOnFail: true, }; - await this.deliverQueue.addBulk(Array.from(inboxes.entries(), d => ({ - name: d[0].replace('https://', '').replace('/inbox', ''), + await this.deliverQueue.addBulk(Array.from(inboxes.entries()).map(d => ({ + name: d[0], data: { user, - content: contentBody, - digest, + content, to: d[0], isSharedInbox: d[1], } as DeliverJobData, @@ -213,21 +129,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 +144,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 +154,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 +164,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 +174,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 +186,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 +196,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 +206,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 +216,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 +226,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 +286,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 +320,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 +331,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 +379,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 +393,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 +416,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..c0113a21d7 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -1,47 +1,61 @@ -/* - * 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(), - inbox, - status: 'requesting', + private async getRelayActor(): Promise { + const user = await this.usersRepository.findOneBy({ + host: IsNull(), + username: ACTOR_USERNAME, }); - const relayActor = await this.systemAccountService.fetch('relay'); - const follow = this.apRendererService.renderFollowRelay(relay, relayActor); + 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', + }).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); @@ -58,7 +72,7 @@ export class RelayService { 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); @@ -68,7 +82,7 @@ export class RelayService { } @bindThis - public async listRelay(): Promise { + public async listRelay(): Promise { const relays = await this.relaysRepository.find(); return relays; } @@ -92,7 +106,7 @@ export class RelayService { } @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({ 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..ed15a1f1ce 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,14 +27,13 @@ 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) { @@ -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,37 +57,21 @@ 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, { @@ -132,7 +109,7 @@ export class RemoteUserResolveService { if (u == null) { throw new Error('user not found'); } else { - return u as MiLocalUser | MiRemoteUser; + return u as LocalUser | RemoteUser; } }); } @@ -142,7 +119,7 @@ export class RemoteUserResolveService { } @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..b0bfb44dc2 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,41 +374,23 @@ 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 }); @@ -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..28b8ee8073 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,98 @@ 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; - } + 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', + }); } - - 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; + public async unindexNote(note: Note): Promise { 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!.deleteDocument(note.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.limit(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..1e44406c16 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,9 +42,9 @@ 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; }) { @@ -76,20 +68,23 @@ export class SignupService { } // 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.exist({ where: { 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.exist({ where: { 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'); } @@ -112,49 +107,46 @@ export class SignupService { 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..d4cf186a24 --- /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..4d7bfeaf23 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,40 +112,17 @@ 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({ + const isFollowing = await this.followingsRepository.exist({ where: { followerId: follower.id, followeeId: followee.id, @@ -187,7 +134,7 @@ export class UserFollowingService implements OnModuleInit { // フォローしているユーザーは自動承認オプション if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { - const isFollowed = await this.followingsRepository.exists({ + const isFollowed = await this.followingsRepository.exist({ where: { followerId: followee.id, followeeId: follower.id, @@ -201,7 +148,7 @@ export class UserFollowingService implements OnModuleInit { 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 +159,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,7 +210,7 @@ export class UserFollowingService implements OnModuleInit { this.cacheService.userFollowingsCache.refresh(follower.id); - const requestExist = await this.followRequestsRepository.exists({ + const requestExist = await this.followRequestsRepository.exist({ where: { followeeId: followee.id, followerId: follower.id, @@ -275,19 +222,15 @@ export class UserFollowingService implements OnModuleInit { 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 +248,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 +288,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 +322,7 @@ export class UserFollowingService implements OnModuleInit { where: { followerId: follower.id, followeeId: followee.id, - }, + } }); if (following === null || !following.follower || !following.followee) { @@ -382,32 +336,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 +381,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 +412,8 @@ export class UserFollowingService implements OnModuleInit { followerId: user.id, followee: { movedToUri: IsNull(), - }, - }, + } + } }); const nonMovedFollowers = await this.followingsRepository.count({ relations: { @@ -465,8 +423,8 @@ export class UserFollowingService implements OnModuleInit { followeeId: user.id, follower: { movedToUri: IsNull(), - }, - }, + } + } }); await this.usersRepository.update( { id: user.id }, @@ -481,13 +439,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 +457,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 +471,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,21 +497,21 @@ 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({ + const requestExist = await this.followRequestsRepository.exist({ where: { followeeId: followee.id, followerId: follower.id, @@ -575,16 +528,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 +548,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 +646,7 @@ export class UserFollowingService implements OnModuleInit { where: { followeeId: followee.id, followerId: follower.id, - }, + } }); if (!following || !following.followee || !following.follower) return; @@ -722,44 +676,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..28ae32681d 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -1,87 +1,38 @@ -/* - * 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)); @@ -109,7 +60,7 @@ 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)) { @@ -137,26 +88,4 @@ export class UserSuspendService { } } } - - @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..89681f3372 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'; @@ -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..b6f5263901 --- /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..0eab7fa335 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,16 +24,16 @@ 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 { @@ -58,7 +51,7 @@ export class ApAudienceService { }; } - if (toGroups.followers.length > 0 || ccGroups.followers.length > 0) { + if (toGroups.followers.length > 0) { return { visibility: 'followers', mentionedUsers, @@ -74,11 +67,11 @@ export class ApAudienceService { } @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) { @@ -97,16 +90,18 @@ export class ApAudienceService { } @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..20283a163c 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -1,19 +1,13 @@ -/* - * 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 { 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 +30,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,10 +48,9 @@ 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 @@ -65,9 +58,7 @@ export class ApDbResolverService implements OnApplicationShutdown { 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 }; - } + if (uri.origin !== this.config.url) return { local: false, uri: uri.href }; const [, type, id, ...rest] = uri.pathname.split(separator); return { @@ -82,7 +73,7 @@ export class ApDbResolverService implements OnApplicationShutdown { * 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 +93,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,8 +114,8 @@ 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({ @@ -140,12 +129,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 +140,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..82c2c9f71f 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.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 { 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'; @@ -24,7 +20,7 @@ interface IFollowersRecipe extends IRecipe { interface IDirectRecipe extends IRecipe { type: 'Direct'; - to: MiRemoteUser; + to: RemoteUser; } const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe => @@ -33,6 +29,73 @@ const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe => const isDirect = (recipe: IRecipe): recipe is IDirectRecipe => recipe.type === 'Direct'; +@Injectable() +export class ApDeliverManagerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private queueService: QueueService, + ) { + } + + /** + * Deliver activity to followers + * @param actor + * @param activity Activity + */ + @bindThis + public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) { + const manager = new DeliverManager( + this.userEntityService, + this.followingsRepository, + this.queueService, + actor, + activity, + ); + manager.addFollowersRecipe(); + await manager.execute(); + } + + /** + * Deliver activity to user + * @param actor + * @param activity Activity + * @param to Target user + */ + @bindThis + public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) { + const manager = new DeliverManager( + this.userEntityService, + this.followingsRepository, + this.queueService, + actor, + activity, + ); + manager.addDirectRecipe(to); + await manager.execute(); + } + + @bindThis + public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) { + return new DeliverManager( + this.userEntityService, + this.followingsRepository, + this.queueService, + + actor, + activity, + ); + } +} + class DeliverManager { private actor: ThinUser; private activity: IActivity | null; @@ -51,11 +114,10 @@ class DeliverManager { private followingsRepository: FollowingsRepository, private queueService: QueueService, - actor: { id: MiUser['id']; host: null; }, + actor: { id: User['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のみに絞る @@ -69,10 +131,10 @@ class DeliverManager { * Add recipe for followers deliver */ @bindThis - public addFollowersRecipe(): void { - const deliver: IFollowersRecipe = { + public addFollowersRecipe() { + const deliver = { type: 'Followers', - }; + } as IFollowersRecipe; this.addRecipe(deliver); } @@ -82,11 +144,11 @@ class DeliverManager { * @param to To */ @bindThis - public addDirectRecipe(to: MiRemoteUser): void { - const recipe: IDirectRecipe = { + public addDirectRecipe(to: RemoteUser) { + const recipe = { type: 'Direct', to, - }; + } as IDirectRecipe; this.addRecipe(recipe); } @@ -96,7 +158,7 @@ class DeliverManager { * @param recipe Recipe */ @bindThis - public addRecipe(recipe: IRecipe): void { + public addRecipe(recipe: IRecipe) { this.recipes.push(recipe); } @@ -104,13 +166,17 @@ class DeliverManager { * Execute delivers */ @bindThis - public async execute(): Promise { + public async execute() { // 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. + /* + 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" みたいな問い合わせにすればよりパフォーマンス向上できそう @@ -124,106 +190,28 @@ class DeliverManager { followerSharedInbox: true, followerInbox: true, }, - }); + }) as { + followerSharedInbox: string | null; + followerInbox: string; + }[]; 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)) { + this.recipes.filter((recipe): recipe is IDirectRecipe => + // followers recipes have already been processed + isDirect(recipe) // check that shared inbox has not been added yet - if (recipe.to.sharedInbox !== null && inboxes.has(recipe.to.sharedInbox)) continue; - + && !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox)) // check that they actually have an inbox - if (recipe.to.inbox === null) continue; - - inboxes.set(recipe.to.inbox, false); - } + && recipe.to.inbox != null, + ) + .forEach(recipe => inboxes.set(recipe.to.inbox!, false)); // deliver - await this.queueService.deliverMany(this.actor, this.activity, inboxes); - } -} - -@Injectable() -export class ApDeliverManagerService { - constructor( - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - - private userEntityService: UserEntityService, - private queueService: QueueService, - ) { - } - - /** - * Deliver activity to followers - * @param actor - * @param activity Activity - */ - @bindThis - public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise { - const manager = new DeliverManager( - this.userEntityService, - this.followingsRepository, - this.queueService, - actor, - activity, - ); - manager.addFollowersRecipe(); - await manager.execute(); - } - - /** - * 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 { - const manager = new DeliverManager( - this.userEntityService, - this.followingsRepository, - this.queueService, - actor, - activity, - ); - manager.addDirectRecipe(to); - 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 { - return new DeliverManager( - this.userEntityService, - this.followingsRepository, - this.queueService, - - actor, - activity, - ); + this.queueService.deliverMany(this.actor, this.activity, inboxes); } } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index e88f60b806..b0428763c9 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,13 +612,13 @@ 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({ + const isFollowing = await this.followingsRepository.exist({ where: { followerId: follower.id, followeeId: actor.id, @@ -682,7 +634,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 +649,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 +665,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,14 +675,14 @@ export class ApInboxService { return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません'; } - const requestExist = await this.followRequestsRepository.exists({ + const requestExist = await this.followRequestsRepository.exist({ where: { followerId: actor.id, followeeId: followee.id, }, }); - const isFollowing = await this.followingsRepository.exists({ + const isFollowing = await this.followingsRepository.exist({ where: { followerId: actor.id, followeeId: followee.id, @@ -751,7 +703,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 +718,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 +733,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 +744,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..411cf17d19 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..96ac5c61f1 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,21 +309,21 @@ 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 inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } }); if (inReplyToUserExist) { if (inReplyToNote.uri) { @@ -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..1f2984894c 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.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 { 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 { 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'; @@ -16,19 +12,17 @@ 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 type { IObject } from '../type.js'; @Injectable() export class ApImageService { private logger: Logger; constructor( - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + private metaService: MetaService, private apResolverService: ApResolverService, private driveService: DriveService, private apLoggerService: ApLoggerService, @@ -40,7 +34,7 @@ export class ApImageService { * Imageを作成します。 */ @bindThis - public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { + public async createImage(actor: RemoteUser, value: string | IObject): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); @@ -48,26 +42,26 @@ export class ApImageService { const image = await this.apResolverService.createResolver().resolve(value); - if (!isDocument(image)) return null; - if (image.url == null) { - return null; + throw new Error('invalid image: url not provided'); } if (typeof image.url !== 'string') { - return null; + throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2)); } 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}`); + const instance = await this.metaService.fetch(); + // 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 shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive); const file = await this.driveService.uploadFromUrl({ url: image.url, @@ -87,11 +81,12 @@ export class ApImageService { /** * 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: string | IObject): 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..62ae3cf93d 100644 --- a/packages/backend/src/core/activitypub/models/ApMentionService.ts +++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts @@ -1,33 +1,34 @@ -/* - * 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 { + public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise { const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href)); - 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; } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 8abacd293f..d3359ef900 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,7 +19,6 @@ 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'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; @@ -45,9 +41,6 @@ export class ApNoteService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -67,6 +60,7 @@ export class ApNoteService { private apMentionService: ApMentionService, private apImageService: ApImageService, private apQuestionService: ApQuestionService, + private metaService: MetaService, private appLockService: AppLockService, private pollService: PollService, private noteCreateService: NoteCreateService, @@ -77,33 +71,20 @@ export class ApNoteService { } @bindThis - public validateNote(object: IObject, uri: string, actor?: MiRemoteUser): Error | null { + public validateNote(object: IObject, uri: string): Error | null { 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 (!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}`); - } - - 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 new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } return null; @@ -115,7 +96,7 @@ export class ApNoteService { * Misskeyに対象のNoteが登録されていればそれを返します。 */ @bindThis - public async fetchNote(object: string | IObject): Promise { + public async fetchNote(object: string | IObject): Promise { return await this.apDbResolverService.getNoteFromApId(object); } @@ -123,39 +104,35 @@ export class ApNoteService { * Noteを作成します。 */ @bindThis - public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise { + public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { // eslint-disable-next-line no-param-reassign 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, }); - 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}`); @@ -165,49 +142,11 @@ export class ApNoteService { throw new Error('invalid note.attributedTo: ' + note.attributedTo); } - const uri = getOneApId(note.attributedTo); + const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as RemoteUser; - // ローカルで投稿者を検索し、もし凍結されていたらスキップ - // 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 apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); - const apHashtags = extractApHashtags(note.tag); - - const cw = note.summary === '' ? null : note.summary; - - // テキストのパース - let text: string | null = null; - if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { - text = note.source.content; - } else if (typeof note._misskey_content !== 'undefined') { - text = note._misskey_content; - } 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'); + throw new Error('actor has been suspended'); } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); @@ -222,17 +161,22 @@ export class ApNoteService { } } - // 添付ファイル - const files: MiDriveFile[] = []; + const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); + const apHashtags = extractApHashtags(note.tag); - for (const attach of toArray(note.attachment)) { - attach.sensitive ??= note.sensitive; - const file = await this.apImageService.resolveImage(actor, attach); - if (file) files.push(file); - } + // 添付ファイル + // TODO: attachmentは必ずしもImageではない + // TODO: attachmentは必ずしも配列ではない + const limit = promiseLimit(2); + const files = (await Promise.all(toArray(note.attachment).map(attach => ( + limit(() => this.apImageService.resolveImage(actor, { + ...attach, + sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする + })) + )))); // リプライ - const reply: MiNote | null = note.inReplyTo + const reply: Note | null = note.inReplyTo ? await this.resolveNote(note.inReplyTo, { resolver }) .then(x => { if (x == null) { @@ -249,29 +193,29 @@ export class ApNoteService { : null; // 引用 - let quote: MiNote | undefined | null = null; + let quote: Note | undefined | null = null; - if (note._misskey_quote ?? note.quoteUrl) { + if (note._misskey_quote || note.quoteUrl) { const tryResolveNote = async (uri: string): Promise< - | { status: 'ok'; res: MiNote } + | { status: 'ok'; res: Note } | { status: 'permerror' | 'temperror' } > => { - if (!/^https?:/.test(uri)) return { status: 'permerror' }; + if (!uri.match(/^https?:/)) 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', + status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', }; } }; - const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null)); + const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); 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); + quote = results.filter((x): x is { status: 'ok', res: Note } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { throw new Error('quote resolve failed'); @@ -279,6 +223,18 @@ export class ApNoteService { } } + const cw = note.summary === '' ? null : note.summary; + + // テキストのパース + let text: string | null = null; + if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { + text = note.source.content; + } else if (typeof note._misskey_content !== 'undefined') { + text = note._misskey_content; + } else if (typeof note.content === 'string') { + text = this.apMfmService.htmlToMfm(note.content, note.tag); + } + // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); @@ -308,36 +264,26 @@ export class ApNoteService { 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); } /** @@ -347,10 +293,12 @@ export class ApNoteService { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise { + public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise { const uri = getApId(value); - if (!this.utilityService.isFederationAllowedUri(uri)) { + // ブロックしていたら中断 + const meta = await this.metaService.fetch(); + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) { throw new StatusError('blocked host', 451); } @@ -362,7 +310,7 @@ export class ApNoteService { 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'); } @@ -370,14 +318,14 @@ export class ApNoteService { // ここで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(createFrom, options.resolver, true); } finally { unlock(); } } @bindThis - public async extractEmojis(tags: IObject | IObject[], host: string): Promise { + public async extractEmojis(tags: IObject | IObject[], host: string): Promise { // eslint-disable-next-line no-param-reassign host = this.utilityService.toPuny(host); @@ -408,8 +356,6 @@ export class ApNoteService { 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 }); @@ -422,8 +368,8 @@ export class ApNoteService { 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, @@ -431,9 +377,7 @@ export class ApNoteService { publicUrl: tag.icon.url, updatedAt: new Date(), aliases: [], - // _misskey_license が存在しなければ `null` - license: (tag._misskey_license?.freeText ?? null) - }); + }).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..e89ee4632c 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -1,40 +1,35 @@ -/* - * 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 { 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 { 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,9 +40,9 @@ 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; @@ -61,6 +56,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 +78,6 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, - @Inject(DI.db) private db: DataSource, @@ -102,8 +95,6 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - - private roleService: RoleService, ) { } @@ -113,6 +104,7 @@ export class ApPersonService implements OnModuleInit { 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 +121,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 +134,7 @@ 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 (!isActor(x)) { throw new Error(`invalid Actor type '${x.type}'`); @@ -150,36 +148,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 +171,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 +181,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 +196,20 @@ 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 { + const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null | undefined; 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 | null; 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 | null; if (exist) { this.cacheService.uriPersonCache.set(uri, exist); @@ -252,60 +220,14 @@ 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'); } @@ -319,58 +241,32 @@ export class ApPersonService implements OnModuleInit { this.logger.info(`Creating the Person: ${person.id}`); + const host = this.punyHost(object.id); + const fields = this.analyzeAttachments(person.attachment ?? []); const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); - const isBot = getApType(object) === 'Service' || getApType(object) === 'Application'; - - 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 | null = null; try { // 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, @@ -382,42 +278,27 @@ export class ApPersonService implements OnModuleInit { usernameLower: person.preferredUsername?.toLowerCase(), host, inbox: person.inbox, - sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null, + sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox, 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, + description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, url, fields, - followingVisibility, - followersVisibility, birthday: 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, @@ -431,7 +312,7 @@ export class ApPersonService implements OnModuleInit { const u = await this.usersRepository.findOneBy({ uri: person.id }); if (u == null) throw new Error('already registered'); - user = u as MiRemoteUser; + user = u as RemoteUser; } else { this.logger.error(e instanceof Error ? e : new Error(e as string)); throw e; @@ -440,19 +321,14 @@ export class ApPersonService implements OnModuleInit { 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); @@ -460,16 +336,45 @@ export class ApPersonService implements OnModuleInit { 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 => { + if (img == null) return null; + if (user == null) throw new Error('failed to create user: user is null'); + return 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?.id ?? null; + const bannerId = banner?.id ?? null; + const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null; + const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null; + const avatarBlurhash = avatar?.blurhash ?? null; + const bannerBlurhash = 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 + + //#region カスタム絵文字取得 + const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => { + this.logger.info(`extractEmojis: ${err}`); + return []; + }); + + 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)); @@ -492,10 +397,10 @@ 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; + const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null; if (exist === null) return; //#endregion @@ -508,6 +413,12 @@ 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 => { + if (img == null) return null; + return this.apImageService.resolveImage(exist, img).catch(() => null); + })); + // カスタム絵文字取得 const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => { this.logger.info(`extractEmojis: ${e}`); @@ -520,58 +431,30 @@ export class ApPersonService implements OnModuleInit { 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 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?.sharedInbox, 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, movedToUri: person.movedTo ?? null, alsoKnownAs: person.alsoKnownAs ?? null, isExplorable: person.discoverable, - ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))), - } as Partial & Pick; + } as Partial & Pick; const moving = ((): boolean => { // 移行先がない→ある @@ -593,11 +476,21 @@ export class ApPersonService implements OnModuleInit { 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,21 +498,10 @@ 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, fields, - description: _description, - followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null, - followingVisibility, - followersVisibility, + description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, }); @@ -632,7 +514,7 @@ export class ApPersonService implements OnModuleInit { // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする await this.followingsRepository.update( { followerId: exist.id }, - { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null }, + { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox }, ); await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); @@ -670,7 +552,7 @@ export class ApPersonService implements OnModuleInit { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolvePerson(uri: string, resolver?: Resolver): Promise { + public async resolvePerson(uri: string, resolver?: Resolver): Promise { //#region このサーバーに既に登録されていたらそれを返す const exist = await this.fetchPerson(uri); if (exist) return exist; @@ -700,8 +582,8 @@ export class ApPersonService implements OnModuleInit { } @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): Promise { + const user = await this.usersRepository.findOneByOrFail({ id: userId }); if (!this.userEntityService.isRemoteUser(user)) return; if (!user.featured) return; @@ -718,7 +600,7 @@ 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) @@ -728,14 +610,15 @@ export class ApPersonService implements OnModuleInit { })))); 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 is 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, }); @@ -749,7 +632,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 +642,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 +650,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'; } @@ -791,16 +674,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..229a44f90f 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'; @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,7 +27,6 @@ export class ApQuestionService { private apResolverService: ApResolverService, private apLoggerService: ApLoggerService, - private utilityService: UtilityService, ) { this.logger = this.apLoggerService.logger; } @@ -57,7 +46,7 @@ export class ApQuestionService { const choices = question[multiple ? 'anyOf' : 'oneOf'] ?.map((x) => x.name) - .filter(x => x != null) + .filter((x): x is string => typeof x === 'string') ?? []; const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); @@ -71,39 +60,28 @@ export class ApQuestionService { * @returns true if updated */ @bindThis - public async updateQuestion(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver): Promise { + public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise { 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); @@ -113,7 +91,7 @@ export class ApQuestionService { 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); + if (newCount == null) throw new Error('invalid newCount: ' + newCount); if (oldCount !== newCount) { changed = true; 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..9aeb843562 100644 --- a/packages/backend/src/core/activitypub/models/tag.ts +++ b/packages/backend/src/core/activitypub/models/tag.ts @@ -1,8 +1,3 @@ -/* - * 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'; @@ -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..d24657260f 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,14 +47,21 @@ export class ChannelEntityService { const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null; - const isFollowing = meId ? await this.channelFollowingsRepository.exists({ + const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({ + where: { + noteChannelId: channel.id, + userId: meId + }, + }) : undefined; + + const isFollowing = meId ? await this.channelFollowingsRepository.exist({ where: { followerId: meId, followeeId: channel.id, }, }) : false; - const isFavorited = meId ? await this.channelFavoritesRepository.exists({ + const isFavorited = meId ? await this.channelFavoritesRepository.exist({ where: { userId: meId, channelId: channel.id, @@ -73,7 +76,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 +87,11 @@ export class ChannelEntityService { isArchived: channel.isArchived, usersCount: channel.usersCount, notesCount: channel.notesCount, - isSensitive: channel.isSensitive, - allowRenoteToExternal: channel.allowRenoteToExternal, ...(me ? { isFollowing, isFavorited, - hasUnreadNote: false, // 後方互換性のため + 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..f558cbc33d 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.exist({ where: { clipId: clip.id, userId: meId } }) : 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..80442af09b 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,7 +45,6 @@ export class DriveFileEntityService { private utilityService: UtilityService, private driveFolderEntityService: DriveFolderEntityService, private videoProcessingService: VideoProcessingService, - private idService: IdService, ) { } @@ -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..92345457c9 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.exist({ where: { flashId: flash.id, userId: meId } }) : 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..c44a5df118 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.exist({ where: { postId: post.id, userId: meId } }) : 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..546e5f56d2 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,88 +67,53 @@ 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 isFollowing = await this.followingsRepository.exist({ + where: { + followeeId: packedNote.userId, + followerId: meId, + }, + }); - hide = !isFollowing; - } + hide = !isFollowing; } } @@ -190,12 +125,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 +162,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 +199,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 +209,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 +253,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 +264,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 +301,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 +334,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 +386,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 +395,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 +450,17 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - private findNoteOrFail(id: string): Promise { - return this.notesRepository.findOneOrFail({ - where: { id }, - relations: ['user'], - }); - } + 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 }); - @bindThis - public async fetchDiffs(noteIds: MiNote['id'][]) { - if (noteIds.length === 0) return []; + // 指定した投稿を除く + if (excludeNoteId) { + query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); + } - 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); + 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..d454ddb70a 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,7 +17,6 @@ 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..02c6982847 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..94b26a5017 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.exist({ where: { pageId: page.id, userId: meId } }) : 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..7d248f8524 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,159 +150,87 @@ 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({ + const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({ where: { antennaId: In(myAntennas.map(x => x.id)), read: false, @@ -330,38 +243,21 @@ export class UserEntityService implements OnModuleInit { } @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 +266,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 +278,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 +293,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 +374,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 +410,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 +429,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 +451,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 +500,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..4d0cb96a2f 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) diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index d229efb123..375fd5e516 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.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 { Injectable } from '@nestjs/common'; import si from 'systeminformation'; import Xev from 'xev'; import * as osUtils from 'os-utils'; import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; -import { MiMeta } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; const ev = new Xev(); @@ -21,11 +15,10 @@ 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 | null = null; constructor( - @Inject(DI.meta) - private meta: MiMeta, + private metaService: MetaService, ) { } @@ -34,12 +27,12 @@ export class ServerStatsService implements OnApplicationShutdown { */ @bindThis public async start(): Promise { - if (!this.meta.enableServerMachineStats) return; + if (!(await this.metaService.fetch(true)).enableServerMachineStats) return; 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 () => { @@ -110,5 +103,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..e825d51371 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: イベント発行して他プロセスのメモリキャッシュも更新できるようにする } @@ -206,23 +181,39 @@ export class RedisSingleCache { // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? -export class MemoryKVCache { - private readonly cache = new Map(); - private readonly gcIntervalHandle = setInterval(() => this.gc(), 1000 * 60 * 3); // 3m +function nothingToDo(value: T): V { + return value as unknown as V; +} - constructor( - private readonly lifetime: number, - ) {} +export class MemoryKVCache { + public cache: Map; + private lifetime: number; + private gcIntervalHandle: NodeJS.Timer; + private toMapConverter: (value: T) => V; + private fromMapConverter: (cached: V) => T | undefined; + + constructor(lifetime: MemoryKVCache['lifetime'], options: { + toMapConverter: (value: T) => V; + fromMapConverter: (cached: V) => T | undefined; + } = { + toMapConverter: nothingToDo, + fromMapConverter: nothingToDo, + }) { + this.cache = new Map(); + this.lifetime = lifetime; + this.toMapConverter = options.toMapConverter; + this.fromMapConverter = options.fromMapConverter; + + 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(), - value, + value: this.toMapConverter(value), }); } @@ -234,7 +225,7 @@ export class MemoryKVCache { this.cache.delete(key); return undefined; } - return cached.value; + return this.fromMapConverter(cached.value); } @bindThis @@ -245,9 +236,10 @@ export class MemoryKVCache { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * fetcherの引数はcacheに保存されている値があれば渡されます */ @bindThis - public async fetch(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetch(key: string, fetcher: (value: V | undefined) => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -262,7 +254,7 @@ export class MemoryKVCache { } // Cache MISS - const value = await fetcher(); + const value = await fetcher(this.cache.get(key)?.value); this.set(key, value); return value; } @@ -270,9 +262,10 @@ export class MemoryKVCache { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * fetcherの引数はcacheに保存されている値があれば渡されます */ @bindThis - public async fetchMaybe(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetchMaybe(key: string, fetcher: (value: V | undefined) => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -287,7 +280,7 @@ export class MemoryKVCache { } // Cache MISS - const value = await fetcher(); + const value = await fetcher(this.cache.get(key)?.value); if (value !== undefined) { this.set(key, value); } @@ -297,14 +290,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 +301,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..612032fe97 100644 --- a/packages/backend/src/misc/check-https.ts +++ b/packages/backend/src/misc/check-https.ts @@ -1,8 +1,3 @@ -/* - * 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'); 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..f5343d187c 100644 --- a/packages/backend/src/misc/is-duplicate-key-value-error.ts +++ b/packages/backend/src/misc/is-duplicate-key-value-error.ts @@ -1,8 +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 { 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..7579040c68 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; @@ -218,17 +133,7 @@ type NullOrUndefined

= // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection // 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..5239678280 100644 --- a/packages/backend/src/misc/prelude/url.ts +++ b/packages/backend/src/misc/prelude/url.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /* objを検査して * 1. 配列に何も入っていない時はクエリを付けない * 2. プロパティが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 84% rename from packages/backend/src/models/Ad.ts rename to packages/backend/src/models/entities/Ad.ts index 108e991c70..a496a6d276 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.', @@ -58,7 +59,7 @@ export class MiAd { 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 60% rename from packages/backend/src/models/Meta.ts rename to packages/backend/src/models/entities/Meta.ts index 3ee6190d45..7bb1b67712 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -1,42 +1,20 @@ -/* - * 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 { 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 +66,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 +102,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,7 +121,7 @@ export class MiMeta { public infoImageUrl: string | null; @Column('boolean', { - default: false, + default: true, }) public cacheRemoteFiles: boolean; @@ -184,6 +130,18 @@ export class MiMeta { }) public cacheRemoteSensitiveFiles: boolean; + @Column({ + ...id(), + nullable: true, + }) + public proxyAccountId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public proxyAccount: User | null; + @Column('boolean', { default: false, }) @@ -206,29 +164,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 +198,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 +220,12 @@ export class MiMeta { }) public enableSensitiveMediaDetectionForVideos: boolean; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public summalyProxy: string | null; + @Column('boolean', { default: false, }) @@ -368,9 +302,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 +313,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 +407,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,11 +417,6 @@ export class MiMeta { }) public enableChartsForFederatedInstances: boolean; - @Column('boolean', { - default: true, - }) - public enableStatsForFederatedInstances: boolean; - @Column('boolean', { default: false, }) @@ -556,153 +439,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..c4ed9db9bb 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..f575b1718e 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 relationshipLogger = this.logger.createSubLogger('relationship'); - 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}`)); - } + 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..65ded170b7 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -1,28 +1,27 @@ -/* - * 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'; +import { SearchService } from "@/core/SearchService.js"; @Injectable() export class DeleteAccountProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -53,7 +52,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,13 +64,13 @@ 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)); @@ -84,7 +83,7 @@ export class DeleteAccountProcessorService { } { // Delete files - let cursor: MiDriveFile['id'] | null = null; + let cursor: DriveFile['id'] | null = null; while (true) { const files = await this.driveFilesRepository.find({ @@ -96,13 +95,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..21c0bfe80e 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..74ef20fdd8 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -1,28 +1,22 @@ -/* - * 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'] }, + src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, userListAccts: { 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..4ba749ec52 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..25e91761ef 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,7 +28,7 @@ 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}`); @@ -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, @@ -71,7 +65,7 @@ export class UserWebhookDeliverProcessorService { if (res instanceof StatusError) { // 4xx - if (!res.isRetryable) { + if (res.isClientError) { throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } 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.limit(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..da86b2c1d3 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,9 @@ 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'; @Module({ imports: [ @@ -59,7 +46,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j ClientServerService, ClientLoggerService, FeedService, - HealthServerService, UrlPreviewService, ActivityPubServerService, FileServerService, @@ -74,7 +60,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j AuthenticateService, RateLimiterService, SigninApiService, - SigninWithPasskeyApiService, SigninService, SignupApiService, StreamingApiServerService, @@ -86,10 +71,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j GlobalTimelineChannelService, HashtagChannelService, RoleTimelineChannelService, - ChatUserChannelService, - ChatRoomChannelService, - ReversiChannelService, - ReversiGameChannelService, HomeTimelineChannelService, HybridTimelineChannelService, LocalTimelineChannelService, @@ -97,7 +78,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j ServerStatsChannelService, UserListChannelService, OpenApiServerService, - OAuth2ProviderService, ], exports: [ ServerService, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 23c085ee27..1bae71617b 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -1,36 +1,30 @@ -/* - * 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'; +import { MetaService } from '@/core/MetaService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; 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'; const _dirname = fileURLToPath(new URL('.', import.meta.url)); @@ -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, @@ -55,6 +46,7 @@ export class ServerService implements OnApplicationShutdown { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + private metaService: MetaService, private userEntityService: UserEntityService, private apiServerService: ApiServerService, private openApiServerService: OpenApiServerService, @@ -63,20 +55,18 @@ 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 { + public async launch() { 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,12 @@ 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.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { const path = request.params.path; @@ -160,20 +103,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 +138,8 @@ export class ServerService implements OnApplicationShutdown { } return await reply.redirect( - url.toString(), 301, + url.toString(), ); }); @@ -221,7 +156,7 @@ 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'); } @@ -231,8 +166,10 @@ export class ServerService implements OnApplicationShutdown { reply.header('Content-Type', 'image/png'); reply.header('Cache-Control', 'public, max-age=86400'); - if (this.meta.enableIdenticonGeneration) { - return await genIdenticon(request.params.x); + if ((await this.metaService.fetch()).enableIdenticonGeneration) { + const [temp, cleanup] = await createTemp(); + await genIdenticon(request.params.x, fs.createWriteStream(temp)); + return fs.createReadStream(temp).on('close', () => cleanup()); } else { return reply.redirect('/static-assets/avatar.png'); } @@ -250,14 +187,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,18 +224,7 @@ 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(); } @@ -309,13 +235,6 @@ export class ServerService implements OnApplicationShutdown { 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..8b0fff80d9 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,18 +32,18 @@ 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'); @@ -75,7 +70,7 @@ export class AuthenticateService implements OnApplicationShutdown { 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, @@ -84,7 +79,7 @@ export class AuthenticateService implements OnApplicationShutdown { 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..f6ffbfab50 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..96666f1f49 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..5e18dcbe08 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.exist({ where: { 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.exist({ where: { 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..e4291becf0 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, ); @@ -118,8 +112,8 @@ export class StreamingApiServerService { 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; @@ -136,10 +130,14 @@ export class StreamingApiServerService { 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', () => { diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index e061aa3a8e..364fa7a19b 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,16 +23,16 @@ 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) { 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..b8ea74b7c5 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, 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..757030839e 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 = { @@ -40,18 +27,19 @@ export const paramDef = { required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'], } as const; +// eslint-disable-next-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, @@ -62,24 +50,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..725ddb58be 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(); - 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..70082290ba 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: { @@ -40,16 +33,15 @@ export const paramDef = { startsAt: { type: 'integer' }, dayOfWeek: { type: 'integer' }, }, - required: ['id'], + required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'], } as const; +// eslint-disable-next-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,18 +55,10 @@ 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, + expiresAt: new Date(ps.expiresAt), + startsAt: new Date(ps.startsAt), dayOfWeek: ps.dayOfWeek, }); - - const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id }); - - this.moderationLogService.log(me, 'updateAd', { - adId: ad.id, - before: ad, - after: updatedAd, - }); }); } } 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..11231f6e04 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'); - } - - 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..8cf9341a71 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); + }); }); } } 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..2901fdb774 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, 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..8d50413e95 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, 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..80acdd1910 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,7 +80,7 @@ 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) }%` }); @@ -96,7 +91,7 @@ export default class extends Endpoint { // eslint- if (queryarry) { emojis = emojis.filter(emoji => - queryarry.includes(`:${emoji.name}:`), + queryarry.includes(`:${emoji.name}:`) ); } else { emojis = emojis.filter(emoji => 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..edc1af5a53 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..084bdb598b 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.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 { 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', @@ -42,18 +35,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 +51,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 +80,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 +92,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 +133,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,10 +265,6 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - enableStatsForFederatedInstances: { - type: 'boolean', - optional: false, nullable: false, - }, enableServerMachineStats: { type: 'boolean', optional: false, nullable: false, @@ -365,212 +273,10 @@ export const meta = { 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 +288,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 +327,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, @@ -646,21 +339,17 @@ export default class extends Endpoint { // eslint- 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 +374,11 @@ 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..8401cf51d9 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,7 +50,7 @@ export default class extends Endpoint { // eslint- throw e; }); - const exist = await this.promoNotesRepository.exists({ where: { noteId: note.id } }); + const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } }); if (exist) { 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..8330d6c82f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts @@ -0,0 +1,72 @@ +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]; + try { + await queue.promote(); + } catch (e) { + if (e instanceof Error) { + if (e.message.indexOf('not in a delayed state') !== -1) { + throw e; + } + } else { + throw e; + } + } + } + break; + + case 'inbox': + delayedQueues = await this.queueService.inboxQueue.getDelayed(); + for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { + const queue = delayedQueues[queueIndex]; + try { + await queue.promote(); + } catch (e) { + if (e instanceof Error) { + if (e.message.indexOf('not in a delayed state') !== -1) { + throw e; + } + } else { + throw e; + } + } + } + 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..1fedab4540 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) => { - const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); - if (role == null) { + super(meta, paramDef, async (ps) => { + const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } }); + if (!roleExist) { 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..63650bb2bf 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,10 +57,9 @@ 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'); @@ -89,14 +67,11 @@ export default class extends Endpoint { // eslint- .limit(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..69c95ef19c 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,14 +74,6 @@ 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(); 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..0a150d1dfd 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; @@ -110,11 +105,11 @@ export default class extends Endpoint { // eslint- } query.limit(ps.limit); - query.offset(ps.offset); + 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..144360a921 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,12 +36,9 @@ 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 }, @@ -73,29 +48,23 @@ export const paramDef = { 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 +78,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 +95,28 @@ 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 +137,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 +154,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 +178,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; } @@ -347,22 +214,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 +238,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 +254,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 +270,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 +319,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 +398,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,10 +406,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; } @@ -621,118 +422,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..735af51ee2 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(); - 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..aa199ab730 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,7 +58,7 @@ export default class extends Endpoint { // eslint- const accessToken = secureRndstr(32); // Fetch exist access token - const exist = await this.accessTokensRepository.exists({ + const exist = await this.accessTokensRepository.exist({ where: { appId: session.appId, userId: me.id, @@ -80,7 +76,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..4ad40c8f1c 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,7 +84,7 @@ export default class extends Endpoint { // eslint- }); // Check if already blocking - const exist = await this.blockingsRepository.exists({ + const exist = await this.blockingsRepository.exist({ where: { blockerId: blocker.id, blockeeId: blockee.id, @@ -102,7 +98,7 @@ export default class extends Endpoint { // eslint- 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..38913ae932 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,11 +84,11 @@ export default class extends Endpoint { // eslint- }); // Check not blocking - const exist = await this.blockingsRepository.exists({ + const exist = await this.blockingsRepository.exist({ where: { blockerId: blocker.id, blockeeId: blockee.id, - }, + } }); if (!exist) { @@ -103,7 +99,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..d61bb0d214 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, 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..953f027aa2 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, 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..a1656903aa 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,15 +44,7 @@ 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 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..4561bb2e94 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, diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index ae32203603..dfb6937964 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) }%` }); 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..e3119cc40f 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.limit(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..2837f2cf81 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,62 @@ 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.exist({ + where: { + noteId: note.id, + clipId: clip.id, + }, + }); + + if (exist) { + 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..ce09855531 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,7 +58,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchClip); } - const exist = await this.clipFavoritesRepository.exists({ + const exist = await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: me.id, @@ -74,7 +70,8 @@ export default class extends Endpoint { // eslint- } 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..49607babee 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,14 +81,10 @@ 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 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..f4343248b8 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,8 +65,8 @@ 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; 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..cdcdde7e8a 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,14 +26,15 @@ 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({ + const exist = await this.driveFilesRepository.exist({ where: { md5: ps.md5, userId: me.id, 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..c43f812e2f 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 = { @@ -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..eb674f3e15 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, 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..a1c14a8e3f 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, 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..51027f35c0 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..3c2d0ce4a4 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..1b2f9446f8 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, diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index 1a793889c7..c5aa1ec60b 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, diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 41954129e6..ddf1a178b1 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.limit(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..06f252005b 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, @@ -54,7 +50,7 @@ export default class extends Endpoint { // eslint- .limit(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..99c8763b11 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.limit(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..57245f9f41 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,7 +66,7 @@ export default class extends Endpoint { // eslint- } // if already liked - const exist = await this.flashLikesRepository.exists({ + const exist = await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: me.id, @@ -83,7 +79,8 @@ export default class extends Endpoint { // eslint- // 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..7d1149ada9 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, diff --git a/packages/backend/src/server/api/endpoints/flash/my.ts b/packages/backend/src/server/api/endpoints/flash/my.ts index 5746096232..45a3b50e08 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, 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..009fc96f64 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,22 @@ export default class extends Endpoint { // eslint- throw err; }); + // Check if already following + const exist = await this.followingsRepository.exist({ + where: { + followerId: follower.id, + followeeId: followee.id, + }, + }); + + if (exist) { + 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..570c4eb81e 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,7 +84,7 @@ export default class extends Endpoint { // eslint- }); // Check not following - const exist = await this.followingsRepository.exists({ + const exist = await this.followingsRepository.exist({ where: { followerId: follower.id, followeeId: followee.id, 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..29588e8731 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, @@ -71,7 +67,7 @@ export default class extends Endpoint { // eslint- .limit(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..46347247f0 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.limit(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..4ee3d68a92 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, diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts index d398418ab4..b9aac3fb34 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, 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..c0bb55f640 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,7 +66,7 @@ export default class extends Endpoint { // eslint- } // if already liked - const exist = await this.galleryLikesRepository.exists({ + const exist = await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: me.id, @@ -85,16 +79,12 @@ export default class extends Endpoint { // eslint- // 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..810bde03e8 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'; @@ -16,16 +11,6 @@ export const 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 +19,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..693d938bf0 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, diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index d4eb851054..e2e00def79 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) + .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..ce1cd9f01f 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -1,13 +1,26 @@ -/* - * 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'], @@ -50,21 +63,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..b00b005add 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,8 +33,9 @@ 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, @@ -48,9 +43,8 @@ export default class extends Endpoint { // eslint- 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(); - 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..743e3f8abc 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, @@ -71,8 +69,8 @@ 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..2ef5e5a279 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, ) { @@ -69,7 +68,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/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..bdfb63974a 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, 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..915639e5f7 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, 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..5ba9afd4a8 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, 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..8582e98f76 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 } }); + const userExist = await this.usersRepository.exist({ where: { id: me.id } }); if (!userExist) 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..9f073ba596 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, diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts index 1b6359a633..772486befc 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, 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..352fe54c5d 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,51 @@ 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 announcementExist = await this.announcementsRepository.exist({ where: { id: ps.announcementId } }); + + if (!announcementExist) { + throw new ApiError(meta.errors.noSuchAnnouncement); + } + + // Check if already read + const alreadyRead = await this.announcementReadsRepository.exist({ + where: { + announcementId: ps.announcementId, + userId: me.id, + }, + }); + + if (alreadyRead) { + 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..415a60147b 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 tokenExist = await this.accessTokensRepository.exist({ where: { 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 (tokenExist) { + 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..aa8cb5cf42 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, 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..3d0146e315 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, Brackets } 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,226 @@ 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, + }, + cacheRemoteSensitiveFiles: { + 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 +248,115 @@ 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.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(); + + 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, + dayOfWeek: ad.dayOfWeek, + })), + 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, + cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, + 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..ef53f9ef41 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,7 +79,7 @@ export default class extends Endpoint { // eslint- }); // Check if already muting - const exist = await this.mutingsRepository.exists({ + const exist = await this.mutingsRepository.exist({ where: { muterId: muter.id, muteeId: mutee.id, 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..4711e86d6b 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, 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..9013b300e7 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, diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index d457ad1220..5f03fd4b74 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,7 +63,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); + this.queryService.generateBlockedUserQuery(query, me); + } const notes = await query.limit(ps.limit).getMany(); 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..739316997a 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,20 +204,20 @@ 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({ + const blockExist = await this.blockingsRepository.exist({ where: { blockerId: renote.userId, blockeeId: me.id, @@ -289,47 +227,22 @@ export default class extends Endpoint { // eslint- 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({ + const blockExist = await this.blockingsRepository.exist({ where: { blockerId: reply.userId, blockeeId: me.id, @@ -351,7 +264,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 +274,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..9299d66039 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,7 +63,7 @@ export default class extends Endpoint { // eslint- }); // if already favorited - const exist = await this.noteFavoritesRepository.exists({ + const exist = await this.noteFavoritesRepository.exist({ where: { noteId: note.id, userId: me.id, @@ -80,7 +76,8 @@ export default class extends Endpoint { // eslint- // 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..3a3cb0739b 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') + .limit(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..4ce2fdaec7 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,22 +75,17 @@ 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(); 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..af94cf6087 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.limit(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..fe7407f48a 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.limit(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..6ee9de1e23 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 }); @@ -86,6 +81,8 @@ export default class extends Endpoint { // eslint- const mentions = await query.limit(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..0b4ccdcf20 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) + .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..4ee12b3353 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,7 +68,8 @@ 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(); diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 9626947480..900c40d32a 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,7 +52,8 @@ 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(); 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..dc0a5dceee 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`); } })); } diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 3fe19806e3..cd0e351e45 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, 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..7e9bf85d88 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.limit(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..b91bc7b5ec 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..4c19e1a553 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.limit(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..b1c056124e 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, diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 11eed693ad..bc66488103 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,7 +66,7 @@ export default class extends Endpoint { // eslint- } // if already liked - const exist = await this.pageLikesRepository.exists({ + const exist = await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: me.id, @@ -83,7 +79,8 @@ export default class extends Endpoint { // eslint- // 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..9baa930f5f 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,7 +44,7 @@ export default class extends Endpoint { // eslint- throw err; }); - const exist = await this.promoReadsRepository.exists({ + const exist = await this.promoReadsRepository.exist({ where: { noteId: note.id, userId: me.id, @@ -61,7 +56,8 @@ export default class extends Endpoint { // eslint- } 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..cb4e1feba4 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, 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..a30c31b727 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, @@ -84,9 +74,18 @@ export default class extends Endpoint { // eslint- 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); - let noteIds = await this.fanoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId); - noteIds = noteIds.slice(0, ps.limit); + if (noteIdsRes.length === 0) { + return []; + } + + 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..cc27201886 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,10 +58,9 @@ 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'); @@ -92,12 +68,9 @@ export default class extends Endpoint { // eslint- .limit(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..552441e430 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -1,14 +1,8 @@ -/* - * 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'; +import { MetaService } from '@/core/MetaService.js'; export const meta = { requireCredential: false, @@ -16,53 +10,6 @@ export const meta = { 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,14 +18,14 @@ 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, + private metaService: MetaService, ) { super(meta, paramDef, async () => { - if (!this.serverSettings.enableServerMachineStats) return { + if (!(await this.metaService.fetch()).enableServerMachineStats) return { machine: '?', cpu: { model: '?', 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..2582932e3a 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; @@ -85,11 +81,11 @@ export default class extends Endpoint { // eslint- if (me) this.queryService.generateBlockQueryForUsers(query, me); query.limit(ps.limit); - query.offset(ps.offset); + 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..c2ad420cb5 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, 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..18d66500ab 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,23 @@ 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 isFollowing = await this.followingsRepository.exist({ + where: { + followeeId: user.id, + followerId: me.id, + }, + }); + if (!isFollowing) { 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); - } - } } } diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 1fc87151b2..6ea7b923d6 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,23 @@ 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 isFollowing = await this.followingsRepository.exist({ + where: { + followeeId: user.id, + followerId: me.id, + }, + }); + if (!isFollowing) { 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,19 +113,6 @@ 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) .getMany(); 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..3ee01953d4 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, 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..beb0ba85ff 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,7 +84,7 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const listExist = await this.userListsRepository.exists({ + const listExist = await this.userListsRepository.exist({ where: { id: ps.listId, isPublic: true, @@ -100,17 +94,18 @@ 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])); - const users = (await this.userListMembershipsRepository.findBy({ + const users = (await this.userListJoiningsRepository.findBy({ userListId: ps.listId, })).map(x => x.userId); @@ -121,7 +116,7 @@ export default class extends Endpoint { // eslint- }); if (currentUser.id !== me.id) { - const blockExist = await this.blockingsRepository.exists({ + const blockExist = await this.blockingsRepository.exist({ where: { blockerId: currentUser.id, blockeeId: me.id, @@ -132,7 +127,7 @@ export default class extends Endpoint { // eslint- } } - const exist = await this.userListMembershipsRepository.exists({ + const exist = await this.userListJoiningsRepository.exist({ where: { userListId: userList.id, userId: currentUser.id, @@ -144,7 +139,7 @@ export default class extends Endpoint { // eslint- } 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..2c09a47fef 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,7 +41,7 @@ export default class extends Endpoint { private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const userListExist = await this.userListsRepository.exists({ + const userListExist = await this.userListsRepository.exist({ where: { id: ps.listId, isPublic: true, @@ -58,7 +52,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchList); } - const exist = await this.userListFavoritesRepository.exists({ + const exist = await this.userListFavoritesRepository.exist({ where: { userId: me.id, userListId: ps.listId, @@ -70,7 +64,8 @@ export default class extends Endpoint { } 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..6e1f6b2c62 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,7 +100,7 @@ export default class extends Endpoint { // eslint- // Check blocking if (user.id !== me.id) { - const blockExist = await this.blockingsRepository.exists({ + const blockExist = await this.blockingsRepository.exist({ where: { blockerId: user.id, blockeeId: me.id, @@ -115,7 +111,7 @@ export default class extends Endpoint { // eslint- } } - const exist = await this.userListMembershipsRepository.exists({ + const exist = await this.userListJoiningsRepository.exist({ where: { userListId: userList.id, userId: user.id, @@ -127,7 +123,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..3fd418d04e 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,7 +69,7 @@ export default class extends Endpoint { userListId: ps.listId, }); if (me !== null) { - additionalProperties.isLiked = await this.userListFavoritesRepository.exists({ + additionalProperties.isLiked = await this.userListFavoritesRepository.exist({ where: { userId: me.id, userListId: ps.listId, 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..a7c3b58947 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,7 +39,7 @@ export default class extends Endpoint { private userListFavoritesRepository: UserListFavoritesRepository, ) { super(meta, paramDef, async (ps, me) => { - const userListExist = await this.userListsRepository.exists({ + const userListExist = await this.userListsRepository.exist({ where: { id: ps.listId, isPublic: true, 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..f42f84e6a7 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.limit(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..e9d13ba00f 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, diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index d6f1ecd8ed..37fc854c33 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 + 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; + .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..d39657059a 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.limit(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..1d0c7d0c1d 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') + .limit(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') + .limit(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') + .limit(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..1180de3611 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,104 @@ 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' }); + ps.query = ps.query.trim(); + 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') + .limit(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') + .limit(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') + .limit(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..8e25af64fe 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,12 @@ 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(); - } + ps.username = ps.username?.trim(); - if ('userIds' in ps) { + if (ps.userIds) { if (ps.userIds.length === 0) { return []; } @@ -141,29 +106,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 +133,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 +142,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..4a544fadfe 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,10 +52,6 @@ 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..94ebf86418 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..fe0cc37b6b 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..4f36832e42 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -1,20 +1,16 @@ -/* - * 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( @@ -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..ea4cff0bc0 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -1,31 +1,24 @@ -/* - * 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, @@ -37,14 +30,11 @@ 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({ + const listExist = await this.userListsRepository.exist({ where: { id: this.listId, userId: this.user!.id, @@ -63,62 +53,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 +111,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 +130,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 deleted file mode 100644 index cdd7102666..0000000000 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ /dev/null @@ -1,504 +0,0 @@ -/* - * 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'; -import { JSDOM } from 'jsdom'; -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 type { Config } from '@/config.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import type { AccessTokensRepository, UsersRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService.js'; -import type { MiLocalUser } from '@/models/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'; - -// TODO: Consider migrating to @node-oauth/oauth2-server once -// https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out. -// Upstream the various validations and RFC9207 implementation in that case. - -// Follows https://indieauth.spec.indieweb.org/#client-identifier -// This is also mostly similar to https://developers.google.com/identity/protocols/oauth2/web-server#uri-validation -// although Google has stricter rule. -function validateClientId(raw: string): URL { - // "Clients are identified by a [URL]." - const url = ((): URL => { - try { - return new URL(raw); - } catch { throw new AuthorizationError('client_id must be a valid URL', 'invalid_request'); } - })(); - - // "Client identifier URLs MUST have either an https or http scheme" - // But then again: - // 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"' - 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'); - } - - // "MUST contain a path component (new URL() implicitly adds one)" - - // "MUST NOT contain single-dot or double-dot path segments," - const segments = url.pathname.split('/'); - if (segments.includes('.') || segments.includes('..')) { - throw new AuthorizationError('client_id must not contain dot path segments', 'invalid_request'); - } - - // ("MAY contain a query string component") - - // "MUST NOT contain a fragment component" - if (url.hash) { - throw new AuthorizationError('client_id must not contain a fragment component', 'invalid_request'); - } - - // "MUST NOT contain a username or password component" - if (url.username || url.password) { - throw new AuthorizationError('client_id must not contain a username or a password', 'invalid_request'); - } - - // ("MAY contain a port") - - // "host names MUST be domain names or a loopback interface and MUST NOT be - // IPv4 or IPv6 addresses except for IPv4 127.0.0.1 or IPv6 [::1]." - if (!url.hostname.match(/\.\w+$/) && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) { - throw new AuthorizationError('client_id must have a domain name as a host name', 'invalid_request'); - } - - return url; -} - -interface ClientInformation { - id: string; - redirectUris: string[]; - name: string; - logo: string | null; -} - -// https://indieauth.spec.indieweb.org/#client-information-discovery -// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id, -// and if there is an [h-app] with a url property matching the client_id URL, -// then it should use the name and icon and display them on the authorization prompt." -// (But we don't display any icon for now) -// https://indieauth.spec.indieweb.org/#redirect-url -// "The client SHOULD publish one or more tags or Link HTTP headers with a rel attribute -// of redirect_uri at the client_id URL. -// 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 { - try { - const res = await httpRequestService.send(id); - const redirectUris: string[] = []; - - const linkHeader = res.headers.get('link'); - if (linkHeader) { - redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri)); - } - - const text = await res.text(); - const fragment = JSDOM.fragment(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; - } - } - } - - return { - id, - redirectUris: redirectUris.map(uri => new URL(uri, res.url).toString()), - name: typeof name === 'string' ? name : id, - logo, - }; - } 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'); - } - } -} - -type OmitFirstElement = T extends [unknown, ...(infer R)] - ? R - : []; - -interface OAuthParsedRequest extends OAuth2Req { - codeChallenge: string; - codeChallengeMethod: string; -} - -interface OAuthHttpResponse extends ServerResponse { - redirect(location: string): void; -} - -interface OAuth2DecisionRequest extends MiddlewareRequest { - body: { - transaction_id: string; - cancel: boolean; - login_token: string; - } -} - -function getQueryMode(issuerUrl: string): oauth2orize.grant.Options['modes'] { - return { - query: (txn, res, params): void => { - // https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss - // "In authorization responses to the client, including error responses, - // an authorization server supporting this specification MUST indicate its - // identity by including the iss parameter in the response." - params.iss = issuerUrl; - - const parsed = new URL(txn.redirectURI); - for (const [key, value] of Object.entries(params)) { - parsed.searchParams.append(key, value as string); - } - - return (res as OAuthHttpResponse).redirect(parsed.toString()); - }, - }; -} - -/** - * Maps the transaction ID and the oauth/authorize parameters. - * - * Flow: - * 1. oauth/authorize endpoint will call store() to store the parameters - * and puts the generated transaction ID to the dialog page - * 2. oauth/decision will call load() to retrieve the parameters and then remove() - */ -class OAuth2Store { - #cache = new MemoryKVCache(1000 * 60 * 5); // expires after 5min - - load(req: OAuth2DecisionRequest, cb: (err: Error | null, txn?: OAuth2) => void): void { - const { transaction_id } = req.body; - if (!transaction_id) { - cb(new AuthorizationError('Missing transaction ID', 'invalid_request')); - return; - } - const loaded = this.#cache.get(transaction_id); - if (!loaded) { - cb(new AuthorizationError('Invalid or expired transaction ID', 'access_denied')); - return; - } - cb(null, loaded); - } - - store(req: OAuth2DecisionRequest, oauth2: OAuth2, cb: (err: Error | null, transactionID?: string) => void): void { - const transactionId = secureRndstr(128); - this.#cache.set(transactionId, oauth2); - cb(null, transactionId); - } - - remove(req: OAuth2DecisionRequest, tid: string, cb: () => void): void { - this.#cache.delete(tid); - cb(); - } -} - -@Injectable() -export class OAuth2ProviderService { - #server = oauth2orize.createServer({ - store: new OAuth2Store(), - }); - #logger: Logger; - - constructor( - @Inject(DI.config) - private config: Config, - private httpRequestService: HttpRequestService, - @Inject(DI.accessTokensRepository) - accessTokensRepository: AccessTokensRepository, - idService: IdService, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - private cacheService: CacheService, - loggerService: LoggerService, - ) { - this.#logger = loggerService.getLogger('oauth'); - - const grantCodeCache = new MemoryKVCache<{ - clientId: string, - userId: string, - redirectUri: string, - codeChallenge: string, - scopes: string[], - - // fields to prevent multiple code use - grantedToken?: string, - revoked?: boolean, - used?: boolean, - }>(1000 * 60 * 5); // expires after 5m - - // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics - // "Authorization servers MUST support PKCE [RFC7636]." - this.#server.grant(oauth2Pkce.extensions()); - this.#server.grant(oauth2orize.grant.code({ - modes: getQueryMode(config.url), - }, (client, redirectUri, token, ares, areq, locals, done) => { - (async (): Promise>> => { - this.#logger.info(`Checking the user before sending authorization code to ${client.id}`); - - if (!token) { - throw new AuthorizationError('No user', 'invalid_request'); - } - const user = await this.cacheService.localUserByNativeTokenCache.fetch(token, - () => 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); - grantCodeCache.set(code, { - clientId: client.id, - userId: user.id, - redirectUri, - codeChallenge: (areq as OAuthParsedRequest).codeChallenge, - scopes: areq.scope, - }); - return [code]; - })().then(args => done(null, ...args), err => done(err)); - })); - this.#server.exchange(oauth2orize.exchange.authorizationCode((client, code, redirectUri, body, authInfo, done) => { - (async (): Promise> | undefined> => { - this.#logger.info('Checking the received authorization code for the exchange'); - const granted = grantCodeCache.get(code); - if (!granted) { - return; - } - - // https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2 - // "If an authorization code is used more than once, the authorization server - // MUST deny the request and SHOULD revoke (when possible) all tokens - // previously issued based on that authorization code." - if (granted.used) { - this.#logger.info(`Detected multiple code use from ${granted.clientId} for user ${granted.userId}. Revoking the code.`); - grantCodeCache.delete(code); - granted.revoked = true; - if (granted.grantedToken) { - await accessTokensRepository.delete({ token: granted.grantedToken }); - } - return; - } - granted.used = true; - - // https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.3 - if (body.client_id !== granted.clientId) return; - if (redirectUri !== granted.redirectUri) return; - - // https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.6 - if (!body.code_verifier) return; - if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return; - - const accessToken = secureRndstr(128); - const now = new Date(); - - // NOTE: we don't have a setup for automatic token expiration - await accessTokensRepository.insert({ - id: idService.gen(now.getTime()), - lastUsedAt: now, - userId: granted.userId, - token: accessToken, - hash: accessToken, - name: granted.clientId, - permission: granted.scopes, - }); - - if (granted.revoked) { - this.#logger.info('Canceling the token as the authorization code was revoked in parallel during the process.'); - await accessTokensRepository.delete({ token: accessToken }); - return; - } - - granted.grantedToken = accessToken; - this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${granted.scopes}]`); - - return [accessToken, undefined, { scope: granted.scopes.join(' ') }]; - })().then(args => done(null, ...args ?? []), err => done(err)); - })); - } - - // 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) => { - const oauth2 = (request.raw as MiddlewareRequest).oauth2; - if (!oauth2) { - throw new Error('Unexpected lack of authorization information'); - } - - this.#logger.info(`Rendering authorization page for "${oauth2.client.name}"`); - - reply.header('Cache-Control', 'no-store'); - 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.register(fastifyView, { - root: fileURLToPath(new URL('../web/views', import.meta.url)), - engine: { pug }, - defaultContext: { - version: this.config.version, - config: this.config, - }, - }); - - await fastify.register(fastifyExpress); - fastify.use('/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 - - const { codeChallenge, codeChallengeMethod, clientID, redirectURI, scope } = areq as OAuthParsedRequest; - - this.#logger.info(`Validating authorization parameters, with client_id: ${clientID}, redirect_uri: ${redirectURI}, scope: ${scope}`); - - 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." - 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') { - throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request'); - } - } - - // Find client information from the remote. - const clientInfo = await discoverClientInformation(this.#logger, 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 - if (!clientInfo.redirectUris.includes(redirectURI)) { - throw new AuthorizationError('Invalid redirect_uri', 'invalid_request'); - } - - try { - const scopes = [...new Set(scope)].filter(s => (kinds).includes(s)); - if (!scopes.length) { - throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope'); - } - areq.scope = scopes; - - // Require PKCE parameters. - // Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack: - // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack - if (typeof codeChallenge !== 'string') { - throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request'); - } - if (codeChallengeMethod !== 'S256') { - throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request'); - } - } catch (err) { - return [err as Error, clientInfo, redirectURI]; - } - - return [null, clientInfo, redirectURI]; - })().then(args => done(...args), err => done(err)); - }) as ValidateFunctionArity2)); - fastify.use('/authorize', this.#server.errorHandler({ - mode: 'indirect', - modes: getQueryMode(this.config.url), - })); - fastify.use('/authorize', this.#server.errorHandler()); - - fastify.use('/decision', bodyParser.urlencoded({ extended: false })); - fastify.use('/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()); - - // 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) => { - reply.code(404); - reply.send({ - error: { - message: 'Unknown OAuth endpoint.', - code: 'UNKNOWN_OAUTH_ENDPOINT', - id: 'aa49e620-26cb-4e28-aad6-8cbcb58db147', - kind: 'client', - }, - }); - }); - } - - @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..0bd0d3c692 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,13 +29,11 @@ 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, @@ -54,18 +47,18 @@ export class FeedService { 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`, @@ -79,15 +72,14 @@ export class FeedService { 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, }); } 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..51899dd3a3 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 () => { 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..5bb576a27b 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 () => { 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..69a4e37322 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,32 +26,29 @@ 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.24.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 ✨🚀✨') @@ -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..44ebf53cf7 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(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.dont-worry Don't worry, it's (probably) not your fault. + + 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/note.pug b/packages/backend/src/server/web/views/note.pug index ea1993aed0..98d0c9a789 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) @@ -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 deleted file mode 100644 index 4195ccc3a3..0000000000 --- a/packages/backend/src/server/web/views/oauth.pug +++ /dev/null @@ -1,11 +0,0 @@ -extends ./base - -block meta - //- Should be removed by the page when it loads, so that it won't needlessly - //- stay when user navigates away via the navigation bar - //- 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..c6beec4f88 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 } 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, }); @@ -268,42 +218,6 @@ describe('API', () => { assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_request", error_description')); }); - describe('invalid bearer format', () => { - test('No preceding bearer', async () => { - const result = await relativeFetch('api/notes/create', { - method: 'POST', - headers: { - Authorization: alice.token, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ text: 'test' }), - }); - assert.strictEqual(result.status, 401); - }); - - test('Lowercase bearer', async () => { - const result = await relativeFetch('api/notes/create', { - method: 'POST', - headers: { - Authorization: `bearer ${alice.token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ text: 'test' }), - }); - assert.strictEqual(result.status, 401); - }); - - test('No space after bearer', async () => { - const result = await relativeFetch('api/notes/create', { - method: 'POST', - headers: { - Authorization: `Bearer${alice.token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ text: 'test' }), - }); - assert.strictEqual(result.status, 401); - }); - }); + // TODO: insufficient_scope test (authテストが全然なくて書けない) }); }); 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 deleted file mode 100644 index f639f90ea6..0000000000 --- a/packages/backend/test/e2e/oauth.ts +++ /dev/null @@ -1,1020 +0,0 @@ -/* - * 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. - */ - -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import { - AuthorizationCode, - type AuthorizationTokenConfig, - ClientCredentials, - ModuleOptions, - ResourceOwnerPassword, -} 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 type * as misskey from 'misskey-js'; - -const host = `http://127.0.0.1:${port}`; - -const clientPort = port + 1; -const redirect_uri = `http://127.0.0.1:${clientPort}/redirect`; - -const basicAuthParams: AuthorizationParamsExtended = { - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', -}; - -interface AuthorizationParamsExtended { - redirect_uri: string; - scope: string | string[]; - state: string; - code_challenge?: string; - code_challenge_method?: string; -} - -interface AuthorizationTokenConfigExtended extends AuthorizationTokenConfig { - code_verifier: string | undefined; -} - -interface GetTokenError { - data: { - payload: { - error: string; - } - } -} - -const clientConfig: ModuleOptions<'client_id'> = { - client: { - id: `http://127.0.0.1:${clientPort}/`, - secret: '', - }, - auth: { - tokenHost: host, - tokenPath: '/oauth/token', - authorizePath: '/oauth/authorize', - }, - options: { - authorizationMethod: 'body', - }, -}; - -function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined, clientLogo: 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 { - return fetch(new URL('/oauth/decision', host), { - method: 'post', - body: new URLSearchParams({ - transaction_id: transactionId, - login_token: user.token, - cancel: cancel ? 'cancel' : '', - }), - redirect: 'manual', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - }); -} - -async function fetchDecisionFromResponse(response: Response, user: misskey.entities.SignupResponse, { 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 }> { - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope, - state: 'state', - code_challenge, - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(response.status, 200); - - const decisionResponse = await fetchDecisionFromResponse(response, user); - assert.strictEqual(decisionResponse.status, 302); - - const locationHeader = decisionResponse.headers.get('location'); - assert.ok(locationHeader); - - const location = new URL(locationHeader); - assert.ok(location.searchParams.has('code')); - - const code = new URL(location).searchParams.get('code'); - assert.ok(code); - - return { client, code }; -} - -function assertIndirectError(response: Response, error: string): void { - assert.strictEqual(response.status, 302); - - const locationHeader = response.headers.get('location'); - assert.ok(locationHeader); - - const location = new URL(locationHeader); - assert.strictEqual(location.searchParams.get('error'), error); - - // https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss - assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local'); - // https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2.1 - assert.ok(location.searchParams.has('state')); -} - -async function assertDirectError(response: Response, status: number, error: string): Promise { - assert.strictEqual(response.status, status); - - const data = await response.json(); - assert.strictEqual(data.error, error); -} - -describe('OAuth', () => { - let fastify: FastifyInstance; - - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - - let sender: (reply: FastifyReply) => void; - - beforeAll(async () => { - 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 => { - reply.send(` - - -
Misklient - `); - }; - }); - - afterAll(async () => { - await fastify.close(); - }); - - test('Full flow', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge, - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(response.status, 200); - - const meta = getMeta(await response.text()); - assert.strictEqual(typeof meta.transactionId, 'string'); - assert.ok(meta.transactionId); - assert.strictEqual(meta.clientName, 'Misklient'); - - const decisionResponse = await fetchDecision(meta.transactionId, alice); - assert.strictEqual(decisionResponse.status, 302); - assert.ok(decisionResponse.headers.has('location')); - - const locationHeader = decisionResponse.headers.get('location'); - assert.ok(locationHeader); - - const location = new URL(locationHeader); - assert.strictEqual(location.origin + location.pathname, redirect_uri); - assert.ok(location.searchParams.has('code')); - assert.strictEqual(location.searchParams.get('state'), 'state'); - // https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss - assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local'); - - const code = new URL(location).searchParams.get('code'); - assert.ok(code); - - const token = await client.getToken({ - code, - redirect_uri, - code_verifier, - } as AuthorizationTokenConfigExtended); - assert.strictEqual(typeof token.token.access_token, 'string'); - assert.strictEqual(token.token.token_type, 'Bearer'); - assert.strictEqual(token.token.scope, 'write:notes'); - - const createResult = await api('notes/create', { text: 'test' }, { - token: token.token.access_token as string, - bearer: true, - }); - assert.strictEqual(createResult.status, 200); - - const createResultBody = createResult.body as misskey.Endpoints['notes/create']['res']; - assert.strictEqual(createResultBody.createdNote.text, 'test'); - }); - - test('Two concurrent flows', async () => { - const client = new AuthorizationCode(clientConfig); - - const pkceAlice = await pkceChallenge(128); - const pkceBob = await pkceChallenge(128); - - const responseAlice = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: pkceAlice.code_challenge, - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(responseAlice.status, 200); - - const responseBob = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: pkceBob.code_challenge, - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(responseBob.status, 200); - - const decisionResponseAlice = await fetchDecisionFromResponse(responseAlice, alice); - assert.strictEqual(decisionResponseAlice.status, 302); - - const decisionResponseBob = await fetchDecisionFromResponse(responseBob, bob); - assert.strictEqual(decisionResponseBob.status, 302); - - const locationHeaderAlice = decisionResponseAlice.headers.get('location'); - assert.ok(locationHeaderAlice); - const locationAlice = new URL(locationHeaderAlice); - - const locationHeaderBob = decisionResponseBob.headers.get('location'); - assert.ok(locationHeaderBob); - const locationBob = new URL(locationHeaderBob); - - const codeAlice = locationAlice.searchParams.get('code'); - assert.ok(codeAlice); - const codeBob = locationBob.searchParams.get('code'); - assert.ok(codeBob); - - const tokenAlice = await client.getToken({ - code: codeAlice, - redirect_uri, - code_verifier: pkceAlice.code_verifier, - } as AuthorizationTokenConfigExtended); - - const tokenBob = await client.getToken({ - code: codeBob, - redirect_uri, - code_verifier: pkceBob.code_verifier, - } as AuthorizationTokenConfigExtended); - - const createResultAlice = await api('notes/create', { text: 'test' }, { - token: tokenAlice.token.access_token as string, - bearer: true, - }); - assert.strictEqual(createResultAlice.status, 200); - - const createResultBob = await api('notes/create', { text: 'test' }, { - token: tokenBob.token.access_token as string, - bearer: true, - }); - assert.strictEqual(createResultAlice.status, 200); - - const createResultBodyAlice = await createResultAlice.body as misskey.Endpoints['notes/create']['res']; - assert.strictEqual(createResultBodyAlice.createdNote.user.username, 'alice'); - - const createResultBodyBob = await createResultBob.body as misskey.Endpoints['notes/create']['res']; - assert.strictEqual(createResultBodyBob.createdNote.user.username, 'bob'); - }); - - // https://datatracker.ietf.org/doc/html/rfc7636.html - describe('PKCE', () => { - // https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.4.1 - // '... the authorization endpoint MUST return the authorization - // error response with the "error" value set to "invalid_request".' - test('Require PKCE', async () => { - const client = new AuthorizationCode(clientConfig); - - // Pattern 1: No PKCE fields at all - let response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - }), { redirect: 'manual' }); - assertIndirectError(response, 'invalid_request'); - - // Pattern 2: Only code_challenge - response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - } as AuthorizationParamsExtended), { redirect: 'manual' }); - assertIndirectError(response, 'invalid_request'); - - // Pattern 3: Only code_challenge_method - response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended), { redirect: 'manual' }); - assertIndirectError(response, 'invalid_request'); - - // Pattern 4: Unsupported code_challenge_method - response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'SSSS', - } as AuthorizationParamsExtended), { redirect: 'manual' }); - assertIndirectError(response, 'invalid_request'); - }); - - // Use precomputed challenge/verifier set here for deterministic test - const code_challenge = '4w2GDuvaxXlw2l46k5PFIoIcTGHdzw2i3hrn-C_Q6f7u0-nTYKd-beVEYy9XinYsGtAix.Nnvr.GByD3lAii2ibPRsSDrZgIN0YQb.kfevcfR9aDKoTLyOUm4hW4ABhs'; - const code_verifier = 'Ew8VSBiH59JirLlg7ocFpLQ6NXuFC1W_rn8gmRzBKc8'; - - const tests: Record = { - 'Code followed by some junk code': code_verifier + 'x', - 'Clipped code': code_verifier.slice(0, 80), - 'Some part of code is replaced': code_verifier.slice(0, -10) + 'x'.repeat(10), - 'No verifier': undefined, - }; - - describe('Verify PKCE', () => { - for (const [title, wrong_verifier] of Object.entries(tests)) { - test(title, async () => { - const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); - - await assert.rejects(client.getToken({ - code, - redirect_uri, - code_verifier: wrong_verifier, - } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { - assert.strictEqual(err.data.payload.error, 'invalid_grant'); - return true; - }); - }); - } - }); - }); - - // https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2 - // "If an authorization code is used more than once, the authorization server - // MUST deny the request and SHOULD revoke (when possible) all tokens - // previously issued based on that authorization code." - describe('Revoking authorization code', () => { - test('On success', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); - - await client.getToken({ - code, - redirect_uri, - code_verifier, - } as AuthorizationTokenConfigExtended); - - await assert.rejects(client.getToken({ - code, - redirect_uri, - code_verifier, - } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { - assert.strictEqual(err.data.payload.error, 'invalid_grant'); - return true; - }); - }); - - test('On failure', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); - - await assert.rejects(client.getToken({ code, redirect_uri }), (err: GetTokenError) => { - assert.strictEqual(err.data.payload.error, 'invalid_grant'); - return true; - }); - - await assert.rejects(client.getToken({ - code, - redirect_uri, - code_verifier, - } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { - assert.strictEqual(err.data.payload.error, 'invalid_grant'); - return true; - }); - }); - - test('Revoke the already granted access token', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); - - const token = await client.getToken({ - code, - redirect_uri, - code_verifier, - } as AuthorizationTokenConfigExtended); - - const createResult = await api('notes/create', { text: 'test' }, { - token: token.token.access_token as string, - bearer: true, - }); - assert.strictEqual(createResult.status, 200); - - await assert.rejects(client.getToken({ - code, - redirect_uri, - code_verifier, - } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { - assert.strictEqual(err.data.payload.error, 'invalid_grant'); - return true; - }); - - const createResult2 = await api('notes/create', { text: 'test' }, { - token: token.token.access_token as string, - bearer: true, - }); - assert.strictEqual(createResult2.status, 401); - }); - }); - - test('Cancellation', async () => { - 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 decisionResponse = await fetchDecisionFromResponse(response, alice, { cancel: true }); - assert.strictEqual(decisionResponse.status, 302); - - const locationHeader = decisionResponse.headers.get('location'); - assert.ok(locationHeader); - - const location = new URL(locationHeader); - assert.ok(!location.searchParams.has('code')); - assert.ok(location.searchParams.has('error')); - }); - - // https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.3 - describe('Scope', () => { - // "If the client omits the scope parameter when requesting - // authorization, the authorization server MUST either process the - // request using a pre-defined default value or fail the request - // indicating an invalid scope." - // (And Misskey does the latter) - test('Missing scope', async () => { - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri, - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended), { redirect: 'manual' }); - assertIndirectError(response, 'invalid_scope'); - }); - - test('Empty scope', async () => { - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: '', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended), { redirect: 'manual' }); - assertIndirectError(response, 'invalid_scope'); - }); - - test('Unknown scopes', async () => { - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'test:unknown test:unknown2', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended), { redirect: 'manual' }); - assertIndirectError(response, 'invalid_scope'); - }); - - // "If the issued access token scope - // is different from the one requested by the client, the authorization - // server MUST include the "scope" response parameter to inform the - // client of the actual scope granted." - // (Although Misskey always return scope, which is also fine) - test('Partially known scopes', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - - // Just get the known scope for this case for backward compatibility - const { client, code } = await fetchAuthorizationCode( - alice, - 'write:notes test:unknown test:unknown2', - code_challenge, - ); - - const token = await client.getToken({ - code, - redirect_uri, - code_verifier, - } as AuthorizationTokenConfigExtended); - - assert.strictEqual(token.token.scope, 'write:notes'); - }); - - test('Known scopes', async () => { - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes read:account', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - - assert.strictEqual(response.status, 200); - }); - - test('Duplicated scopes', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - - const { client, code } = await fetchAuthorizationCode( - alice, - 'write:notes write:notes read:account read:account', - code_challenge, - ); - - const token = await client.getToken({ - code, - redirect_uri, - code_verifier, - } as AuthorizationTokenConfigExtended); - assert.strictEqual(token.token.scope, 'write:notes read:account'); - }); - - test('Scope check by API', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - - const { client, code } = await fetchAuthorizationCode(alice, 'read:account', code_challenge); - - const token = await client.getToken({ - code, - redirect_uri, - code_verifier, - } as AuthorizationTokenConfigExtended); - assert.strictEqual(typeof token.token.access_token, 'string'); - - const createResult = await api('notes/create', { text: 'test' }, { - token: token.token.access_token as string, - bearer: true, - }); - assert.strictEqual(createResult.status, 403); - assert.ok(createResult.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="insufficient_scope", error_description')); - }); - }); - - // https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.1.2.4 - // "If an authorization request fails validation due to a missing, - // invalid, or mismatching redirection URI, the authorization server - // SHOULD inform the resource owner of the error and MUST NOT - // automatically redirect the user-agent to the invalid redirection URI." - describe('Redirection', () => { - test('Invalid redirect_uri at authorization endpoint', async () => { - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri: 'http://127.0.0.2/', - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - await assertDirectError(response, 400, 'invalid_request'); - }); - - test('Invalid redirect_uri including the valid one at authorization endpoint', async () => { - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - redirect_uri: 'http://127.0.0.1/redirection', - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - await assertDirectError(response, 400, 'invalid_request'); - }); - - test('No redirect_uri at authorization endpoint', async () => { - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL({ - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - await assertDirectError(response, 400, 'invalid_request'); - }); - - test('Invalid redirect_uri at token endpoint', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - - const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); - - await assert.rejects(client.getToken({ - code, - redirect_uri: 'http://127.0.0.2/', - code_verifier, - } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { - assert.strictEqual(err.data.payload.error, 'invalid_grant'); - return true; - }); - }); - - test('Invalid redirect_uri including the valid one at token endpoint', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - - const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); - - await assert.rejects(client.getToken({ - code, - redirect_uri: 'http://127.0.0.1/redirection', - code_verifier, - } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { - assert.strictEqual(err.data.payload.error, 'invalid_grant'); - return true; - }); - }); - - test('No redirect_uri at token endpoint', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - - const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); - - await assert.rejects(client.getToken({ - code, - code_verifier, - } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { - assert.strictEqual(err.data.payload.error, 'invalid_grant'); - return true; - }); - }); - }); - - // https://datatracker.ietf.org/doc/html/rfc8414 - test('Server metadata', async () => { - const response = await fetch(new URL('.well-known/oauth-authorization-server', host)); - assert.strictEqual(response.status, 200); - - const body = await response.json(); - assert.strictEqual(body.issuer, 'http://misskey.local'); - assert.ok(body.scopes_supported.includes('write:notes')); - }); - - // Any error on decision endpoint is solely on Misskey side and nothing to do with the client. - // Do not use indirect error here. - describe('Decision endpoint', () => { - test('No login token', async () => { - const client = new AuthorizationCode(clientConfig); - - const response = await fetch(client.authorizeURL(basicAuthParams)); - assert.strictEqual(response.status, 200); - - const { transactionId } = getMeta(await response.text()); - assert.ok(transactionId); - - const decisionResponse = await fetch(new URL('/oauth/decision', host), { - method: 'post', - body: new URLSearchParams({ - transaction_id: transactionId, - }), - redirect: 'manual', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - }); - await assertDirectError(decisionResponse, 400, 'invalid_request'); - }); - - test('No transaction ID', async () => { - const decisionResponse = await fetch(new URL('/oauth/decision', host), { - method: 'post', - body: new URLSearchParams({ - login_token: alice.token, - }), - redirect: 'manual', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - }); - await assertDirectError(decisionResponse, 400, 'invalid_request'); - }); - - test('Invalid transaction ID', async () => { - const decisionResponse = await fetch(new URL('/oauth/decision', host), { - method: 'post', - body: new URLSearchParams({ - login_token: alice.token, - transaction_id: 'invalid_id', - }), - redirect: 'manual', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - }); - await assertDirectError(decisionResponse, 403, 'access_denied'); - }); - }); - - // Only authorization code grant is supported - describe('Grant type', () => { - test('Implicit grant is not supported', async () => { - const url = new URL('/oauth/authorize', host); - url.searchParams.append('response_type', 'token'); - const response = await fetch(url); - assertDirectError(response, 501, 'unsupported_response_type'); - }); - - test('Resource owner grant is not supported', async () => { - const client = new ResourceOwnerPassword({ - ...clientConfig, - auth: { - tokenHost: host, - tokenPath: '/oauth/token', - }, - }); - - await assert.rejects(client.getToken({ - username: 'alice', - password: 'test', - }), (err: GetTokenError) => { - assert.strictEqual(err.data.payload.error, 'unsupported_grant_type'); - return true; - }); - }); - - test('Client credential grant is not supported', async () => { - const client = new ClientCredentials({ - ...clientConfig, - auth: { - tokenHost: host, - tokenPath: '/oauth/token', - }, - }); - - await assert.rejects(client.getToken({}), (err: GetTokenError) => { - assert.strictEqual(err.data.payload.error, 'unsupported_grant_type'); - return true; - }); - }); - }); - - // https://indieauth.spec.indieweb.org/#client-information-discovery - describe('Client Information Discovery', () => { - describe('Redirection', () => { - const tests: Record void> = { - 'Read HTTP header': reply => { - reply.header('Link', '; rel="redirect_uri"'); - reply.send(` - -
Misklient - `); - }, - 'Mixed links': reply => { - reply.header('Link', '; rel="redirect_uri"'); - reply.send(` - - -
Misklient - `); - }, - 'Multiple items in Link header': reply => { - reply.header('Link', '; rel="redirect_uri",; rel="redirect_uri"'); - reply.send(` - -
Misklient - `); - }, - 'Multiple items in HTML': reply => { - reply.send(` - - - -
Misklient - `); - }, - }; - - for (const [title, replyFunc] of Object.entries(tests)) { - test(title, async () => { - sender = replyFunc; - - 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); - }); - } - - test('No item', async () => { - sender = (reply): void => { - reply.send(` - -
Misklient - `); - }; - - 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)); - - // direct error because there's no redirect URI to ping - await assertDirectError(response, 400, 'invalid_request'); - }); - }); - - test('Disallow loopback', async () => { - await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '1' }); - - 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)); - await assertDirectError(response, 400, 'invalid_request'); - }); - - test('Missing name', async () => { - sender = (reply): void => { - 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(); - }; - - 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('Unknown OAuth endpoint', async () => { - 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..9dbe77a7c4 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; @@ -35,14 +24,13 @@ export class MockResolver extends Resolver { 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, diff --git a/packages/backend/test/prelude/get-api-validator.ts b/packages/backend/test/prelude/get-api-validator.ts index 7aa7a92702..f095774760 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..9ee6d4bcfb 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'; diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts index 1e3605aafc..1ee2939829 100644 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -1,14 +1,8 @@ -/* - * 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 { 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'; @@ -17,14 +11,17 @@ import { LoggerService } from '@/core/LoggerService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; +import type { TestingModule } from '@nestjs/testing'; +import type { MockFunctionMetadata } from 'jest-mock'; +import { Redis } from 'ioredis' function mockRedis() { - const hash = {} as any; - const set = jest.fn((key: string, value) => { - const ret = hash[key]; - hash[key] = value; - return ret; - }); + const hash = {}; + const set = jest.fn((key, value) => { + const ret = hash[key]; + hash[key] = value; + return ret; + }); return set; } @@ -33,9 +30,9 @@ describe('FetchInstanceMetadataService', () => { let fetchInstanceMetadataService: jest.Mocked; let federatedInstanceService: jest.Mocked; let httpRequestService: jest.Mocked; - let redisClient: jest.Mocked; + let redisClient: jest.Mocked; - beforeEach(async () => { + beforeAll(async () => { app = await Test .createTestingModule({ imports: [ @@ -50,87 +47,63 @@ describe('FetchInstanceMetadataService', () => { }) .useMocker((token) => { if (token === HttpRequestService) { - return { getJson: jest.fn(), getHtml: jest.fn(), send: jest.fn() }; + return { getJson: jest.fn(), getHtml: jest.fn(), send: jest.fn(), }; } else if (token === FederatedInstanceService) { - return { fetchOrRegister: jest.fn() }; + return { fetch: jest.fn() }; } else if (token === DI.redis) { return mockRedis; - } - return null; - }) + }}) .compile(); app.enableShutdownHooks(); - fetchInstanceMetadataService = app.get(FetchInstanceMetadataService) as jest.Mocked; + fetchInstanceMetadataService = app.get(FetchInstanceMetadataService); federatedInstanceService = app.get(FederatedInstanceService) as jest.Mocked; - redisClient = app.get(DI.redis) as jest.Mocked; + redisClient = app.get(DI.redis) as jest.Mocked; httpRequestService = app.get(HttpRequestService) as jest.Mocked; }); - afterEach(async () => { + afterAll(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); + federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } }); 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); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: "example.com" }); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1); - expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1); + expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); expect(httpRequestService.getJson).toHaveBeenCalled(); }); - - test('Lock and don\'t update', async () => { + test("Lock and don't update", async () => { redisClient.set = mockRedis(); const now = Date.now(); - federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any); + federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now } }); 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); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: "example.com" }); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1); - expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1); + expect(federatedInstanceService.fetch).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); + federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } }); 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); + await fetchInstanceMetadataService.tryLock("example.com"); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: "example.com" }); + expect(tryLockSpy).toHaveBeenCalledTimes(2); expect(unlockSpy).toHaveBeenCalledTimes(0); - expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0); + expect(federatedInstanceService.fetch).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..efb9bdacc3 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', @@ -121,7 +117,11 @@ describe('FileInfoService', () => { 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', @@ -137,7 +137,11 @@ describe('FileInfoService', () => { 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', @@ -153,7 +157,11 @@ describe('FileInfoService', () => { 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', @@ -170,7 +178,11 @@ describe('FileInfoService', () => { 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', @@ -186,7 +198,11 @@ describe('FileInfoService', () => { 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', @@ -202,7 +218,11 @@ describe('FileInfoService', () => { 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; @@ -236,7 +260,11 @@ 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; @@ -252,7 +280,11 @@ 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; @@ -268,7 +300,11 @@ 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; @@ -282,36 +318,27 @@ 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..6bf08f5091 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,6 +53,8 @@ describe('RelayService', () => { relayService = app.get(RelayService); queueService = app.get(QueueService) as jest.Mocked; + relaysRepository = app.get(DI.relaysRepository); + userEntityService = app.get(UserEntityService); }); afterAll(async () => { @@ -86,8 +85,7 @@ describe('RelayService', () => { 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'); 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..78b916c112 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -1,38 +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 * 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, IApDocument, ICollection, IPost } from '@/core/activitypub/type.js'; +import { Meta, 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 { MetaService } from '@/core/MetaService.js'; +import type { RemoteUser } from '@/models/entities/User.js'; +import { MockResolver } from '../misc/mock-resolver.js'; const host = 'https://host1.test'; @@ -84,7 +70,7 @@ function createRandomFeaturedCollection(actor: NonTransientIActor, length: numbe async function createRandomRemoteUser( resolver: MockResolver, personService: ApPersonService, -): Promise { +): Promise { const actor = createRandomActor(); resolver.register(actor.id, actor); @@ -92,65 +78,44 @@ async function createRandomRemoteUser( } 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); - } + } as Meta; + let meta = metaInitial; beforeAll(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, - ); - } + async downloadUrl(): Promise<{ filename: string }> { return { filename: 'dummy.tmp', }; }, }) - .overrideProvider(DI.meta).useFactory({ factory: () => meta }) - .compile(); + .overrideProvider(MetaService).useValue({ + async fetch(): Promise { + return meta; + }, + }).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 @@ -188,7 +153,7 @@ describe('ActivityPub', () => { 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'); @@ -224,59 +189,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); + } as Note); }); }); @@ -336,21 +254,6 @@ describe('ActivityPub', () => { 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', () => { @@ -365,7 +268,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), imageObject, ); - assert.ok(driveFile && !driveFile.isLink); + assert.ok(!driveFile.isLink); const sensitiveImageObject: IApDocument = { type: 'Document', @@ -378,11 +281,11 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), sensitiveImageObject, ); - assert.ok(sensitiveDriveFile && !sensitiveDriveFile.isLink); + assert.ok(!sensitiveDriveFile.isLink); }); test('cacheRemoteFiles=false disables caching', async () => { - updateMeta({ ...metaInitial, cacheRemoteFiles: false }); + meta = { ...metaInitial, cacheRemoteFiles: false }; const imageObject: IApDocument = { type: 'Document', @@ -394,7 +297,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), imageObject, ); - assert.ok(driveFile && driveFile.isLink); + assert.ok(driveFile.isLink); const sensitiveImageObject: IApDocument = { type: 'Document', @@ -407,11 +310,11 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), sensitiveImageObject, ); - assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink); + assert.ok(sensitiveDriveFile.isLink); }); test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => { - updateMeta({ ...metaInitial, cacheRemoteSensitiveFiles: false }); + meta = { ...metaInitial, cacheRemoteSensitiveFiles: false }; const imageObject: IApDocument = { type: 'Document', @@ -423,7 +326,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), imageObject, ); - assert.ok(driveFile && !driveFile.isLink); + assert.ok(!driveFile.isLink); const sensitiveImageObject: IApDocument = { type: 'Document', @@ -436,57 +339,7 @@ describe('ActivityPub', () => { 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', - }); + assert.ok(sensitiveDriveFile.isLink); }); }); }); 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..40554d3a47 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', () => { 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..48947072e3 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, }; }; -export const relativeFetch = async (path: string, init?: RequestInit | undefined) => { +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..a1275132be 100644 --- a/packages/frontend/.storybook/changes.ts +++ b/packages/frontend/.storybook/changes.ts @@ -1,8 +1,3 @@ -/* - * 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'; @@ -47,15 +42,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..b3d7bd8f5e 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,16 +77,23 @@ 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 { @@ -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') : ''), { @@ -440,35 +396,14 @@ 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/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/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) => { diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts index c1119c2523..b64979980a 100644 --- a/packages/frontend/.storybook/main.ts +++ b/packages/frontend/.storybook/main.ts @@ -1,31 +1,22 @@ -/* - * 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 { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; 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 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 +26,16 @@ 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', - }, + // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8 + (turbosnap as any as typeof turbosnap['default'])({ + rootDir: config.root ?? process.cwd(), + }), ], build: { target: [ @@ -64,7 +48,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/preload-locale.ts b/packages/frontend/.storybook/preload-locale.ts index c823ff9bee..2b7362b88d 100644 --- a/packages/frontend/.storybook/preload-locale.ts +++ b/packages/frontend/.storybook/preload-locale.ts @@ -1,8 +1,3 @@ -/* - * 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'; diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index e5573f2ac3..42fbeff738 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { readFile, writeFile } from 'node:fs/promises'; import JSON5 from 'json5'; @@ -30,7 +25,7 @@ const keys = [ 'd-u0', ] -await Promise.all(keys.map((key) => readFile(new URL(`../../frontend-shared/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { +await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { writeFile( new URL('./themes.ts', import.meta.url), `export default ${JSON.stringify( 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..437dce0a14 100644 --- a/packages/frontend/src/components/MkAvatars.vue +++ b/packages/frontend/src/components/MkAvatars.vue @@ -1,8 +1,3 @@ - -