diff --git a/.circleci/config.yml b/.circleci/config.yml index 4725eb32a..c33688f69 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ jobs: build: docker: # Specify the version you desire here - - image: cimg/php:8.2.5 + - image: cimg/php:8.3.8 # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images @@ -21,7 +21,12 @@ jobs: steps: - checkout - - run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev + - run: + name: "Create Environment file and generate app key" + command: | + mv .env.testing .env + + - run: sudo apt install zlib1g-dev libsqlite3-dev # Download and cache dependencies @@ -36,18 +41,17 @@ jobs: - run: composer install -n --prefer-dist - save_cache: - key: composer-v2-{{ checksum "composer.lock" }} + key: v2-dependencies-{{ checksum "composer.json" }} paths: - vendor - - run: cp .env.testing .env - run: php artisan config:cache - run: php artisan route:clear - run: php artisan storage:link - run: php artisan key:generate # run tests with phpunit or codecept - - run: ./vendor/bin/phpunit + - run: php artisan test - store_test_results: path: tests/_output - store_artifacts: diff --git a/.ddev/commands/redis/redis-cli b/.ddev/commands/redis/redis-cli index 27bf575b3..8824c6a6b 100755 --- a/.ddev/commands/redis/redis-cli +++ b/.ddev/commands/redis/redis-cli @@ -4,4 +4,4 @@ ## Usage: redis-cli [flags] [args] ## Example: "redis-cli KEYS *" or "ddev redis-cli INFO" or "ddev redis-cli --version" -redis-cli -p 6379 -h redis $@ +exec redis-cli -p 6379 -h redis "$@" diff --git a/.dockerignore b/.dockerignore index 70376cdf4..757a67a51 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,30 @@ -data -Dockerfile -contrib/docker/Dockerfile.* -docker-compose*.yml -.dockerignore -.git -.gitignore -.env +.DS_Store +/.bash_history +/.bash_profile +/.bashrc +/.composer +/.env +/.env.dottie-backup +/.git +/.git-credentials +/.gitconfig +/.gitignore +/.idea +/.vagrant +/bootstrap/cache +/docker-compose-state/ +/Homestead.json +/Homestead.yaml +/node_modules +/npm-debug.log +/public/hot +/public/storage +/public/vendor/horizon +/storage/*.key +/storage/docker +/vendor +/yarn-error.log + +# Exceptions - these *MUST* be last +!/bootstrap/cache/.gitignore +!/public/vendor/horizon/.gitignore diff --git a/.editorconfig b/.editorconfig index 1cd7d1077..eff249956 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,27 @@ root = true [*] +indent_style = space indent_size = 4 -indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.{sh,envsh,env,env*}] +indent_style = space +indent_size = 4 + +# ShellCheck config +shell_variant = bash # like -ln=bash +binary_next_line = true # like -bn +switch_case_indent = true # like -ci +space_redirects = false # like -sr +keep_padding = false # like -kp +function_next_line = true # like -fn +never_split = true # like -ns +simplify = true diff --git a/.env.docker b/.env.docker index ce4cfe87c..0e373aedc 100644 --- a/.env.docker +++ b/.env.docker @@ -1,149 +1,1302 @@ -## Crypto +#!/bin/bash +# -*- mode: bash -*- +# vi: ft=bash +# shellcheck disable=SC2034,SC2148 + +# Use Dottie (https://github.com/jippi/dottie) to manage this .env file easier! +# +# For example: +# +# Run [dottie update] to update your [.env] file with upstream (as part of upgrade) +# Run [dottie validate] to validate youe [.env] file +# +# @dottie/source .env.docker + +################################################################################ +# app +################################################################################ + +# The name/title for your site +# @see https://docs.pixelfed.org/technical-documentation/config/#app_name-1 +# @dottie/example My Pixelfed Site +# @dottie/validate required,ne=My Pixelfed Site +APP_NAME= + +# Application domain used for routing. (e.g., pixelfed.org) +# +# @see https://docs.pixelfed.org/technical-documentation/config/#app_domain +# @dottie/example example.com +# @dottie/validate required,ne=example.com,fqdn +APP_DOMAIN="example.com" + +# This URL is used by the console to properly generate URLs when using the Artisan command line tool. +# You should set this to the root of your application so that it is used when running Artisan tasks. +# +# @see https://docs.pixelfed.org/technical-documentation/config/#app_url +# @dottie/validate required,http_url +APP_URL="https://${APP_DOMAIN}" + +# Application domains used for routing. +# +# @see https://docs.pixelfed.org/technical-documentation/config/#admin_domain +# @dottie/validate required,fqdn +ADMIN_DOMAIN="${APP_DOMAIN}" + +# This value determines the “environment” your application is currently running in. +# This may determine how you prefer to configure various services your application utilizes. +# +# @default "production" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_env +# @dottie/validate required,oneof=production dev staging +#APP_ENV="production" + +# When your application is in debug mode, detailed error messages with stack traces will +# be shown on every error that occurs within your application. +# +# If disabled, a simple generic error page is shown. +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_debug +# @dottie/validate required,boolean +#APP_DEBUG="false" + +# Disable config cache +# +# If disabled, settings must be managed by .env variables. +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#config_cache +# @dottie/validate required,boolean +ENABLE_CONFIG_CACHE="true" + +# Enable/disable new local account registrations. +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#open_registration +# @dottie/validate required,boolean +#OPEN_REGISTRATION="true" + +# Require email verification before a new user can do anything. +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#enforce_email_verification +# @dottie/validate required,boolean +#ENFORCE_EMAIL_VERIFICATION="true" + +# Allow a maximum number of user accounts. +# +# @default "1000" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_max_users +# @dottie/validate required,number +#PF_MAX_USERS="1000" + +# Enforce the maximum number of user accounts +# +# @default "true" +# @dottie/validate boolean +#PF_ENFORCE_MAX_USERS="true" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#oauth_enabled +# @dottie/validate required,boolean +#OAUTH_ENABLED="false" + +# ! Do not edit your timezone once the service is running - or things will break! +# +# @default "UTC" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_timezone +# @see https://www.php.net/manual/en/timezones.php +# @dottie/validate required,timezone +APP_TIMEZONE="UTC" + +# The application locale determines the default locale that will be used by the translation service provider. +# You are free to set this value to any of the locales which will be supported by the application. +# +# @default "en" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_locale +# @dottie/validate required +#APP_LOCALE="en" + +# The fallback locale determines the locale to use when the current one is not available. +# +# You may change the value to correspond to any of the language folders that are provided through your application. +# +# @default "en" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_fallback_locale +# @dottie/validate required +#APP_FALLBACK_LOCALE="en" + +# @see https://docs.pixelfed.org/technical-documentation/config/#limit_account_size +# @dottie/validate required,boolean +#LIMIT_ACCOUNT_SIZE="true" + +# Update the max account size, the per user limit of files in kB. +# +# @default "1000000" (1GB) +# @see https://docs.pixelfed.org/technical-documentation/config/#max_account_size-kb +# @dottie/validate required,number +#MAX_ACCOUNT_SIZE="1000000" + +# Update the max photo size, in kB. +# +# @default "15000" (15MB) +# @see https://docs.pixelfed.org/technical-documentation/config/#max_photo_size-kb +# @dottie/validate required,number +#MAX_PHOTO_SIZE="15000" + +# The max number of photos allowed per post. +# +# @default "4" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_album_length +# @dottie/validate required,number +#MAX_ALBUM_LENGTH="4" + +# Update the max avatar size, in kB. +# +# @default "2000" (2MB). +# @see https://docs.pixelfed.org/technical-documentation/config/#max_avatar_size-kb +# @dottie/validate required,number +#MAX_AVATAR_SIZE="2000" + +# Change the caption length limit for new local posts. +# +# @default "500" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_caption_length +# @dottie/validate required,number +#MAX_CAPTION_LENGTH="500" + +# Change the bio length limit for user profiles. +# +# @default "125" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_bio_length +# @dottie/validate required,number +#MAX_BIO_LENGTH="125" + +# Change the length limit for user names. +# +# @default "30" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_name_length +# @dottie/validate required,number +#MAX_NAME_LENGTH="30" + +# Resize and optimize image uploads. +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_images +# @dottie/validate required,boolean +#PF_OPTIMIZE_IMAGES="true" + +# Set the image optimization quality, must be a value between 1-100. +# +# @default "80" +# @see https://docs.pixelfed.org/technical-documentation/config/#image_quality +# @dottie/validate required,number +#IMAGE_QUALITY="80" + +# Resize and optimize video uploads. +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_videos +# @dottie/validate required,boolean +#PF_OPTIMIZE_VIDEOS="true" + +# Enable account deletion. +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#account_deletion +# @dottie/validate required,boolean +#ACCOUNT_DELETION="true" + +# Set account deletion queue after X days, set to false to delete accounts immediately. +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#account_delete_after +# @dottie/validate required,boolean|number +#ACCOUNT_DELETE_AFTER="false" + +# @default "Pixelfed - Photo sharing for everyone" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_description +# @dottie/validate required +#INSTANCE_DESCRIPTION="" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_public_hashtags +# @dottie/validate required,boolean +#INSTANCE_PUBLIC_HASHTAGS="false" + +# The public e-mail address people can use to contact you by +# +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_contact_email +# @dottie/validate required,ne=__CHANGE_ME__,email +INSTANCE_CONTACT_EMAIL="__CHANGE_ME__" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_public_local_timeline +# @dottie/validate required,boolean +#INSTANCE_PUBLIC_LOCAL_TIMELINE="false" + +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#banned_usernames +#BANNED_USERNAMES="" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#stories_enabled +# @dottie/validate required,boolean +#STORIES_ENABLED="false" + +# Level is hardcoded to 1. +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#restricted_instance +# @dottie/validate required,boolean +#RESTRICTED_INSTANCE="false" + +# @default false +# @see https://docs.pixelfed.org/technical-documentation/config/#media_exif_database +# @dottie/validate required,boolean +#MEDIA_EXIF_DATABASE="false" + +# Pixelfed supports GD or ImageMagick to process images. +# +# Possible values: +# - "gd" (default) +# - "imagick" +# +# @default "gd" +# @see https://docs.pixelfed.org/technical-documentation/config/#image_driver +# @dottie/validate required,oneof=gd imagick +#IMAGE_DRIVER="gd" + +# Set trusted proxy IP addresses. +# +# Both IPv4 and IPv6 addresses are supported, along with CIDR notation. +# +# The “*” character is syntactic sugar within TrustedProxy to trust any +# proxy that connects directly to your server, a requirement when you cannot +# know the address of your proxy (e.g. if using Rackspace balancers). +# +# The “**” character is syntactic sugar within TrustedProxy to trust not just any +# proxy that connects directly to your server, but also proxies that connect to those proxies, +# and all the way back until you reach the original source IP. It will mean that +# $request->getClientIp() always gets the originating client IP, no matter how many proxies +# that client’s request has subsequently passed through. +# +# @default "*" +# @see https://docs.pixelfed.org/technical-documentation/config/#trust_proxies +# @dottie/validate required +#TRUST_PROXIES="*" + +# This option controls the default cache connection that gets used while using this caching library. +# +# This connection is used when another is not explicitly specified when executing a given caching function. +# +# Possible values: +# - "apc" +# - "array" +# - "database" +# - "file" (default) +# - "memcached" +# - "redis" +# +# @default "file" +# @see https://docs.pixelfed.org/technical-documentation/config/#cache_driver +# @dottie/validate required,oneof=apc array database file memcached redis +CACHE_DRIVER="redis" + +# @default ${APP_NAME}_cache, or laravel_cache if no APP_NAME is set. +# @see https://docs.pixelfed.org/technical-documentation/config/#cache_prefix +# @dottie/validate required +#CACHE_PREFIX="{APP_NAME}_cache" + +# This option controls the default broadcaster that will be used by the framework when an event needs to be broadcast. +# +# Possible values: +# - "pusher" +# - "redis" +# - "log" +# - "null" (default) +# +# @default null +# @see https://docs.pixelfed.org/technical-documentation/config/#broadcast_driver +# @dottie/validate required,oneof=pusher redis log null +BROADCAST_DRIVER="redis" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#restrict_html_types +# @dottie/validate required,boolean +#RESTRICT_HTML_TYPES="true" + +# Passport uses encryption keys while generating secure access tokens +# for your application. +# +# By default, the keys are stored as local files but can be set via environment +# variables when that is more convenient. + +# @see https://docs.pixelfed.org/technical-documentation/config/#passport_private_key +# @dottie/validate required +#PASSPORT_PRIVATE_KEY="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#passport_public_key +# @dottie/validate required +#PASSPORT_PUBLIC_KEY="" + +################################################################################ +# database +################################################################################ + +# Database version to use (as Docker tag) +# +# @see https://hub.docker.com/_/mariadb +# @dottie/validate required +DB_VERSION="11.2" + +# Here you may specify which of the database connections below +# you wish to use as your default connection for all database work. +# +# Of course you may use many connections at once using the database library. +# +# Possible values: +# +# - "sqlite" +# - "mysql" (default) +# - "pgsql" +# - "sqlsrv" +# +# @see https://docs.pixelfed.org/technical-documentation/config/#db_connection +# @dottie/validate required,oneof=sqlite mysql pgsql sqlsrv +DB_CONNECTION="mysql" + +# @see https://docs.pixelfed.org/technical-documentation/config/#db_host +# @dottie/validate required,hostname +DB_HOST="db" + +# @see https://docs.pixelfed.org/technical-documentation/config/#db_username +# @dottie/validate required +DB_USERNAME="pixelfed" + +# The password to your database. Please make it secure. +# Use a site like https://pwgen.io/ to generate it +# +# @see https://docs.pixelfed.org/technical-documentation/config/#db_password +# @dottie/validate required +DB_PASSWORD= + +# @see https://docs.pixelfed.org/technical-documentation/config/#db_database +# @dottie/validate required +DB_DATABASE="pixelfed_prod" + +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +# +# @see https://docs.pixelfed.org/technical-documentation/config/#db_port +# @dottie/validate required,number +DB_PORT="3306" + +# Automatically run [artisan migrate --force] if new migrations are detected. +# @dottie/validate required,boolean +DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="false" + +################################################################################ +# mail +################################################################################ + +# Laravel supports both SMTP and PHP’s “mail” function as drivers for the sending of e-mail. +# You may specify which one you’re using throughout your application here. +# +# Possible values: +# +# "smtp" (default) +# "sendmail" +# "mailgun" +# "mandrill" +# "ses" +# "sparkpost" +# "log" +# "array" +# +# @default "smtp" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_driver +# @dottie/validate required,oneof=smtp sendmail mailgun mandrill ses sparkpost log array +#MAIL_DRIVER="smtp" + +# The host address of the SMTP server used by your applications. +# +# A default option is provided that is compatible with the Mailgun mail service which will provide reliable deliveries. +# +# @default "smtp.mailgun.org" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_host +# @dottie/validate required_with=MAIL_DRIVER,fqdn +#MAIL_HOST="smtp.mailgun.org" + +# This is the SMTP port used by your application to deliver e-mails to users of the application. +# +# Like the host we have set this value to stay compatible with the Mailgun e-mail application by default. +# +# @default 587. +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_port +# @dottie/validate required_with=MAIL_DRIVER,number +#MAIL_PORT="587" + +# Here, you may specify a name and address that is used globally for all e-mails that are sent by your application. +# +# You may wish for all e-mails sent by your application to be sent from the same address. +# +# @default "bot@example.com" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_from_address +# @dottie/validate required_with=MAIL_DRIVER,email,ne=__CHANGE_ME__ +#MAIL_FROM_ADDRESS="__CHANGE_ME__" + +# The 'name' you send e-mail from +# +# @default "Example" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_from_name +# @dottie/validate required_with=MAIL_DRIVER +#MAIL_FROM_NAME="${APP_NAME}" + +# If your SMTP server requires a username for authentication, you should set it here. +# +# This will get used to authenticate with your server on connection. +# You may also set the “password” value below this one. +# +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_username +# @dottie/validate required_with=MAIL_DRIVER +#MAIL_USERNAME="" + +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_password +# @dottie/validate required_with=MAIL_DRIVER +#MAIL_PASSWORD="" + +# Here you may specify the encryption protocol that should be used when the application send e-mail messages. +# +# A sensible default using the transport layer security protocol should provide great security. +# +# @default "tls" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_encryption +# @dottie/validate required_with=MAIL_DRIVER +#MAIL_ENCRYPTION="tls" + +################################################################################ +# redis +################################################################################ + +# @default "phpredis" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_client +# @dottie/validate required +#REDIS_CLIENT="phpredis" + +# @default "tcp" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_scheme +# @dottie/validate required +#REDIS_SCHEME="tcp" + +# @default "localhost" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_host +# @dottie/validate required +REDIS_HOST="redis" + +# @default "null" (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_password +# @dottie/validate omitempty +#REDIS_PASSWORD= + +# @default "6379" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_port +# @dottie/validate required,number +REDIS_PORT="6379" + +# @default "0" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_database +# @dottie/validate required,number +#REDIS_DATABASE="0" + +################################################################################ +# experiments +################################################################################ + +# Text only posts (alpha). +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_top +# @dottie/validate required,boolean +#EXP_TOP="false" + +# Poll statuses (alpha). +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_polls +# @dottie/validate required,boolean +#EXP_POLLS="false" + +# Cached public timeline for larger instances (beta). +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_cpt +# @dottie/validate required,boolean +#EXP_CPT="false" + +# Enforce Mastodon API Compatibility (alpha). +# +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_emc +# @dottie/validate required,boolean +#EXP_EMC="true" + +################################################################################ +# ActivityPub +################################################################################ + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#activity_pub +# @dottie/validate required,boolean +#ACTIVITY_PUB="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_remote_follow +# @dottie/validate required,boolean +#AP_REMOTE_FOLLOW="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_sharedinbox +# @dottie/validate required,boolean +#AP_SHAREDINBOX="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_inbox +# @dottie/validate required,boolean +#AP_INBOX="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_outbox +# @dottie/validate required,boolean +#AP_OUTBOX="true" + +################################################################################ +# Federation +################################################################################ + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#atom_feeds +# @dottie/validate required,boolean +#ATOM_FEEDS="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#nodeinfo +# @dottie/validate required,boolean +#NODEINFO="true" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#webfinger +# @dottie/validate required,boolean +#WEBFINGER="true" + +################################################################################ +# Storage +################################################################################ + +# Store media on object storage like S3, Digital Ocean Spaces, Rackspace +# +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_enable_cloud +# @dottie/validate required,boolean +#PF_ENABLE_CLOUD="false" + +# Many applications store files both locally and in the cloud. +# +# For this reason, you may specify a default “cloud” driver here. +# This driver will be bound as the Cloud disk implementation in the container. +# +# @default "s3" +# @see https://docs.pixelfed.org/technical-documentation/config/#filesystem_cloud +# @dottie/validate required_with=PF_ENABLE_CLOUD +#FILESYSTEM_CLOUD="s3" + +# @default true. +# @see https://docs.pixelfed.org/technical-documentation/config/#media_delete_local_after_cloud +# @dottie/validate required_with=PF_ENABLE_CLOUD,boolean +#MEDIA_DELETE_LOCAL_AFTER_CLOUD="true" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_access_key_id +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_ACCESS_KEY_ID="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_secret_access_key +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_SECRET_ACCESS_KEY="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_default_region +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_DEFAULT_REGION="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_bucket +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_BUCKET="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_url +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_URL="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_endpoint +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_ENDPOINT="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_use_path_style_endpoint +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 +#AWS_USE_PATH_STYLE_ENDPOINT="false" + +################################################################################ +# COSTAR +################################################################################ + +# Comma-separated list of domains to block. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_domains +# @dottie/validate +#CS_BLOCKED_DOMAINS="" + +# Comma-separated list of domains to add warnings. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_cw_domains +# @dottie/validate +#CS_CW_DOMAINS="" + +# Comma-separated list of domains to remove from public timelines. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_domains +# @dottie/validate +#CS_UNLISTED_DOMAINS="" + +# Comma-separated list of keywords to block. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_keywords +# @dottie/validate +#CS_BLOCKED_KEYWORDS="" + +# Comma-separated list of keywords to add warnings. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_cw_keywords +# @dottie/validate +#CS_CW_KEYWORDS="" + +# Comma-separated list of keywords to remove from public timelines. +# +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_keywords +# @dottie/validate +#CS_UNLISTED_KEYWORDS="" + +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_actor +# @dottie/validate +#CS_BLOCKED_ACTOR="" + +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_cw_actor +# @dottie/validate +#CS_CW_ACTOR="" + +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_actor +# @dottie/validate +#CS_UNLISTED_ACTOR="" + +################################################################################ +# logging +################################################################################ + +# Possible values: +# +# - "stack" (default) +# - "single" +# - "daily" +# - "slack" +# - "stderr" +# - "syslog" +# - "errorlog" +# - "null" +# - "emergency" +# - "media" +# +# @default "stack" +# @dottie/validate required,oneof=stack single daily slack stderr syslog errorlog null emergency media +LOG_CHANNEL="stderr" + +# Used by single, stderr and syslog. +# +# @default "debug" +# @see https://docs.pixelfed.org/technical-documentation/config/#log_level +# @dottie/validate required,oneof=debug info notice warning error critical alert emergency +#LOG_LEVEL="debug" + +# Used by stderr. +# +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#log_stderr_formatter +#LOG_STDERR_FORMATTER="" + +# Used by slack. +# +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#log_slack_webhook_url +# @dottie/validate required,http_url +#LOG_SLACK_WEBHOOK_URL="" + +################################################################################ +# queue +################################################################################ + +# Possible values: +# - "sync" (default) +# - "database" +# - "beanstalkd" +# - "sqs" +# - "redis" +# - "null" +# +# @default "sync" +# @see https://docs.pixelfed.org/technical-documentation/config/#queue_driver +# @dottie/validate required,oneof=sync database beanstalkd sqs redis null +QUEUE_DRIVER="redis" + +# @default "your-public-key" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_key +# @dottie/validate required_if=QUEUE_DRIVER sqs +#SQS_KEY="your-public-key" + +# @default "your-secret-key" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_secret +# @dottie/validate required_if=QUEUE_DRIVER sqs +#SQS_SECRET="your-secret-key" + +# @default "https://sqs.us-east-1.amazonaws.com/your-account-id" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_prefix +# @dottie/validate required_if=QUEUE_DRIVER sqs +#SQS_PREFIX="" + +# @default "your-queue-name" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_queue +# @dottie/validate required_if=QUEUE_DRIVER sqs +#SQS_QUEUE="your-queue-name" + +# @default "us-east-1" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_region +# @dottie/validate required_if=QUEUE_DRIVER sqs +#SQS_REGION="us-east-1" + +################################################################################ +# session +################################################################################ + +# This option controls the default session “driver” that will be used on requests. +# +# By default, we will use the lightweight native driver but you may specify any of the other wonderful drivers provided here. +# +# Possible values: +# - "file" +# - "cookie" +# - "database" (default) +# - "apc" +# - "memcached" +# - "redis" +# - "array" +# +# @default "database" +# @dottie/validate required,oneof=file cookie database apc memcached redis array +SESSION_DRIVER="redis" + +# Here you may specify the number of minutes that you wish the session to be allowed to remain idle before it expires. +# +# If you want them to immediately expire on the browser closing, set that option. +# +# @default 86400. +# @see https://docs.pixelfed.org/technical-documentation/config/#session_lifetime +# @dottie/validate required,number +#SESSION_LIFETIME="86400" + +# Here you may change the domain of the cookie used to identify a session in your application. +# +# This will determine which domains the cookie is available to in your application. +# +# A sensible default has been set. +# +# @default the value of APP_DOMAIN, or null. +# @see https://docs.pixelfed.org/technical-documentation/config/#session_domain +# @dottie/validate required,hostname +#SESSION_DOMAIN="${APP_DOMAIN}" + +################################################################################ +# horizon +################################################################################ + +# This prefix will be used when storing all Horizon data in Redis. +# +# You may modify the prefix when you are running multiple installations +# of Horizon on the same server so that they don’t have problems. +# +# @default "horizon-" +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_prefix +# @dottie/validate required +#HORIZON_PREFIX="horizon-" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_darkmode +# @dottie/validate required,boolean +#HORIZON_DARKMODE="false" + +# This value (in MB) describes the maximum amount of memory (in MB) the Horizon worker +# may consume before it is terminated and restarted. +# +# You should set this value according to the resources available to your server. +# +# @default "64" +# @dottie/validate required,number +#HORIZON_MEMORY_LIMIT="64" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_balance_strategy +# @dottie/validate required +#HORIZON_BALANCE_STRATEGY="auto" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_min_processes +# @dottie/validate required,number +#HORIZON_MIN_PROCESSES="1" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_max_processes +# @dottie/validate required,number +#HORIZON_MAX_PROCESSES="20" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_memory +# @dottie/validate required,number +#HORIZON_SUPERVISOR_MEMORY="64" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_tries +# @dottie/validate required,number +#HORIZON_SUPERVISOR_TRIES="3" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_nice +# @dottie/validate required,number +#HORIZON_SUPERVISOR_NICE="0" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_timeout +# @dottie/validate required,number +#HORIZON_SUPERVISOR_TIMEOUT="300" + +################################################################################ +# docker shared +################################################################################ + +# A random 32-character string to be used as an encryption key. +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# ! NOTE: This will be auto-generated by Docker during bootstrap +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# +# This key is used by the Illuminate encrypter service and should be set to a random, +# 32 character string, otherwise these encrypted strings will not be safe. +# +# @see https://docs.pixelfed.org/technical-documentation/config/#app_key +# @dottie/validate required APP_KEY= -## General Settings -APP_NAME="Pixelfed Prod" -APP_ENV=production -APP_DEBUG=false -APP_URL=https://real.domain -APP_DOMAIN="real.domain" -ADMIN_DOMAIN="real.domain" -SESSION_DOMAIN="real.domain" +# Prefix for container names (without any dash at the end) +# @dottie/validate required +DOCKER_ALL_CONTAINER_NAME_PREFIX="${APP_DOMAIN}" -OPEN_REGISTRATION=true -ENFORCE_EMAIL_VERIFICATION=false -PF_MAX_USERS=1000 -OAUTH_ENABLED=true +# How often Docker health check should run for all services +# +# Can be overridden by individual [DOCKER_*_HEALTHCHECK_INTERVAL] settings further down +# +# @default "10s" +# @dottie/validate required +DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL="10s" -APP_TIMEZONE=UTC -APP_LOCALE=en +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will *all* data +# will be stored (data, config, overrides) +# +# @default "./docker-compose-state" +# @dottie/validate required,dir +DOCKER_ALL_HOST_ROOT_PATH="./docker-compose-state" -## Pixelfed Tweaks -LIMIT_ACCOUNT_SIZE=true -MAX_ACCOUNT_SIZE=1000000 -MAX_PHOTO_SIZE=15000 -MAX_AVATAR_SIZE=2000 -MAX_CAPTION_LENGTH=500 -MAX_BIO_LENGTH=125 -MAX_NAME_LENGTH=30 -MAX_ALBUM_LENGTH=4 -IMAGE_QUALITY=80 -PF_OPTIMIZE_IMAGES=true -PF_OPTIMIZE_VIDEOS=true -ADMIN_ENV_EDITOR=false -ACCOUNT_DELETION=true -ACCOUNT_DELETE_AFTER=false -MAX_LINKS_PER_POST=0 +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their data +# +# @default "${DOCKER_ALL_HOST_ROOT_PATH}/data" +# @dottie/validate required,dir +DOCKER_ALL_HOST_DATA_ROOT_PATH="${DOCKER_ALL_HOST_ROOT_PATH:?error}/data" -## Instance -#INSTANCE_DESCRIPTION= -INSTANCE_PUBLIC_HASHTAGS=false -#INSTANCE_CONTACT_EMAIL= -INSTANCE_PUBLIC_LOCAL_TIMELINE=false -#BANNED_USERNAMES= -STORIES_ENABLED=false -RESTRICTED_INSTANCE=false +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their confguration +# +# @default "${DOCKER_ALL_HOST_ROOT_PATH}/config" +# @dottie/validate required,dir +DOCKER_ALL_HOST_CONFIG_ROOT_PATH="${DOCKER_ALL_HOST_ROOT_PATH:?error}/config" -## Mail -MAIL_DRIVER=log -MAIL_HOST=smtp.mailtrap.io -MAIL_PORT=2525 -MAIL_FROM_ADDRESS="pixelfed@example.com" -MAIL_FROM_NAME="Pixelfed" -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store overrides +# +# @default "${DOCKER_ALL_HOST_ROOT_PATH}/overrides" +# @dottie/validate required,dir +DOCKER_APP_HOST_OVERRIDES_PATH="${DOCKER_ALL_HOST_ROOT_PATH:?error}/overrides" -## Databases (MySQL) -DB_CONNECTION=mysql -DB_DATABASE=pixelfed_prod -DB_HOST=db -DB_PASSWORD=pixelfed_db_pass -DB_PORT=3306 -DB_USERNAME=pixelfed -# pass the same values to the db itself -MYSQL_DATABASE=pixelfed_prod -MYSQL_PASSWORD=pixelfed_db_pass -MYSQL_RANDOM_ROOT_PASSWORD=true -MYSQL_USER=pixelfed +# Set timezone used by *all* containers - these must be in sync. +# +# ! Do not edit your timezone once the service is running - or things will break! +# +# @see https://www.php.net/manual/en/timezones.php +# @dottie/validate required,timezone +TZ="${APP_TIMEZONE}" -## Databases (Postgres) -#DB_CONNECTION=pgsql -#DB_HOST=postgres -#DB_PORT=5432 -#DB_DATABASE=pixelfed -#DB_USERNAME=postgres -#DB_PASSWORD=postgres +################################################################################ +# docker app +################################################################################ -## Cache (Redis) -REDIS_CLIENT=phpredis -REDIS_SCHEME=tcp -REDIS_HOST=redis -REDIS_PASSWORD=redis_password -REDIS_PORT=6379 -REDIS_DATABASE=0 +# The docker tag prefix to use for pulling images, can be one of +# +# * latest +# * +# * staging +# * edge +# * branch- +# * pr- +# +# Combined with [DOCKER_APP_RUNTIME] and [PHP_VERSION] configured +# elsewhere in this file, the final Docker tag is computed. +# @dottie/validate required +DOCKER_APP_RELEASE="branch-jippi-fork" -HORIZON_PREFIX="horizon-" +# The PHP version to use for [web] and [worker] container +# +# Any version published on https://hub.docker.com/_/php should work +# +# Example: +# +# * 8.1 +# * 8.2 +# * 8.2.14 +# * latest +# +# Do *NOT* use the full Docker tag (e.g. "8.3.2RC1-fpm-bullseye") +# *only* the version part. The rest of the full tag is derived from +# the [DOCKER_APP_RUNTIME] and [PHP_DEBIAN_RELEASE] settings +# @dottie/validate required +DOCKER_APP_PHP_VERSION="8.2" -## EXPERIMENTS -EXP_LC=false -EXP_REC=false -EXP_LOOPS=false +# The container runtime to use. +# +# @see https://docs.pixelfed.org/running-pixelfed/docker/runtimes.html +# @dottie/validate required,oneof=apache nginx fpm +DOCKER_APP_RUNTIME="apache" -## ActivityPub Federation -ACTIVITY_PUB=false -AP_REMOTE_FOLLOW=false -AP_SHAREDINBOX=false -AP_INBOX=false -AP_OUTBOX=false -ATOM_FEEDS=true -NODEINFO=true -WEBFINGER=true +# The Debian release variant to use of the [php] Docker image +# +# Examlpe: [bookworm] or [bullseye] +# @dottie/validate required,oneof=bookworm bullseye +DOCKER_APP_DEBIAN_RELEASE="bullseye" -## S3 -FILESYSTEM_CLOUD=s3 -PF_ENABLE_CLOUD=false -#AWS_ACCESS_KEY_ID= -#AWS_SECRET_ACCESS_KEY= -#AWS_DEFAULT_REGION= -#AWS_BUCKET= -#AWS_URL= -#AWS_ENDPOINT= -#AWS_USE_PATH_STYLE_ENDPOINT=false +# The [php] Docker image base type +# +# @see https://docs.pixelfed.org/running-pixelfed/docker/runtimes.html +# @dottie/validate required,oneof=apache fpm cli +DOCKER_APP_BASE_TYPE="apache" -## Horizon -HORIZON_DARKMODE=false +# Image to pull the Pixelfed Docker images from. +# +# Example values: +# +# * "ghcr.io/pixelfed/pixelfed" to pull from GitHub +# * "pixelfed/pixelfed" to pull from DockerHub +# * "your/fork" to pull from a custom fork +# +# @dottie/validate required +DOCKER_APP_IMAGE="ghcr.io/jippi/pixelfed" -## COSTAR - Confirm Object Sentiment Transform and Reduce -PF_COSTAR_ENABLED=false +# Pixelfed version (image tag) to pull from the registry. +# +# @see https://github.com/pixelfed/pixelfed/pkgs/container/pixelfed +# @dottie/validate required +DOCKER_APP_TAG="${DOCKER_APP_RELEASE:?error}-${DOCKER_APP_RUNTIME:?error}-${DOCKER_APP_PHP_VERSION:?error}" -# Media -MEDIA_EXIF_DATABASE=false +# Path (on host system) where the [app] + [worker] container will write +# its [storage] data (e.g uploads/images/profile pictures etc.). +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +# @dottie/validate required,dir +DOCKER_APP_HOST_STORAGE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/storage" -## Logging -LOG_CHANNEL=stderr +# Path (on host system) where the [app] + [worker] container will write +# its [cache] data. +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +# @dottie/validate required,dir +DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/cache" -## Image -IMAGE_DRIVER=imagick +# Automatically run "One-time setup tasks" commands. +# +# If you are migrating to this docker-compose setup or have manually run the "One time setup" +# tasks (https://docs.pixelfed.org/running-pixelfed/installation/#setting-up-services) +# you can set this to "0" to prevent them from running. +# +# Otherwise, leave it at "1" to have them run *once*. +# @dottie/validate required,boolean +#DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS="1" -## Broadcasting: log driver for local development -BROADCAST_DRIVER=log +# A space-seperated list of paths (inside the container) to *recursively* [chown] +# to the container user/group id (UID/GID) in case of permission issues. +# +# ! You should *not* leave this on permanently, at it can significantly slow down startup +# ! time for the container, and during normal operations there should never be permission +# ! issues. Please report a bug if you see behavior requiring this to be permanently on +# +# Example: "/var/www/storage /var/www/bootstrap/cache" +# @dottie/validate required +#DOCKER_APP_ENSURE_OWNERSHIP_PATHS="" -## Cache -CACHE_DRIVER=redis +# Enable Docker Entrypoint debug mode (will call [set -x] in bash scripts) +# by setting this to "1" +# @dottie/validate required,boolean +#DOCKER_APP_ENTRYPOINT_DEBUG="0" -## Purify -RESTRICT_HTML_TYPES=true +# Show the "diff" when applying templating to files +# +# @default "1" +# @dottie/validate required,boolean +#DOCKER_APP_ENTRYPOINT_SHOW_TEMPLATE_DIFF="1" -## Queue -QUEUE_DRIVER=redis +# Docker entrypoints that should be skipped on startup +# @default "" +#ENTRYPOINT_SKIP_SCRIPTS="" -## Session -SESSION_DRIVER=redis +# List of extra APT packages (separated by space) to install when building +# locally using [docker compose build]. +# +# @see https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @dottie/validate required +#DOCKER_APP_APT_PACKAGES_EXTRA="" -## Trusted Proxy -TRUST_PROXIES="*" +# List of *extra* PECL extensions (separated by space) to install when +# building locally using [docker compose build]. +# +# @see https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @dottie/validate required +#DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA="" -## Passport -#PASSPORT_PRIVATE_KEY= -#PASSPORT_PUBLIC_KEY= +# List of *extra* PHP extensions (separated by space) to install when +# building locally using [docker compose build]. +# +# @see https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @dottie/validate required +#DOCKER_APP_PHP_EXTENSIONS_EXTRA="" + +# @default "128M" +# @see https://www.php.net/manual/en/ini.core.php#ini.memory-limit +# @dottie/validate required +#DOCKER_APP_PHP_MEMORY_LIMIT="128M" + +# @default "E_ALL & ~E_DEPRECATED & ~E_STRICT" +# @see http://php.net/error-reporting +# @dottie/validate required +#DOCKER_APP_PHP_ERROR_REPORTING="E_ALL & ~E_DEPRECATED & ~E_STRICT" + +# @default "off" +# @see http://php.net/display-errors +# @dottie/validate required,oneof=on off +#DOCKER_APP_PHP_DISPLAY_ERRORS="off" + +# Enables the opcode cache. +# +# When disabled, code is not optimised or cached. +# +# @default "1" +# @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.enable +# @dottie/validate required,oneof=0 1 +#DOCKER_APP_PHP_OPCACHE_ENABLE="1" + +# If enabled, OPcache will check for updated scripts every [opcache.revalidate_freq] seconds. +# +# When this directive is disabled, you must reset OPcache manually via opcache_reset(), +# opcache_invalidate() or by restarting the Web server for changes to the filesystem to take effect. +# +# @default "0" +# @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.validate-timestamps +# @dottie/validate required,oneof=0 1 +#DOCKER_APP_PHP_OPCACHE_VALIDATE_TIMESTAMPS="0" + +# How often to check script timestamps for updates, in seconds. +# 0 will result in OPcache checking for updates on every request. +# +# @default "2" +# @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.revalidate-freq +# @dottie/validate required,oneof=0 1 2 +#DOCKER_APP_PHP_OPCACHE_REVALIDATE_FREQ="2" + +# When doing [docker compose build], should the frontend be built in the Dockerfile? +# If set to "0" the included pre-compiled frontend will be used. +# +# @default "0" +# @dottie/validate required,oneof=0 1 +#DOCKER_APP_BUILD_FRONTEND="0" + +################################################################################ +# docker redis +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [redis] service +#DOCKER_REDIS_PROFILE= + +# Redis version to use as Docker tag +# +# @see https://hub.docker.com/_/redis +# @dottie/validate required +DOCKER_REDIS_VERSION="7.2" + +# Path (on host system) where the [redis] container will store its data +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +# @dottie/validate required,dir +DOCKER_REDIS_HOST_DATA_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/redis" + +# Port that Redis will listen on *outside* the container (e.g. the host machine) +# @dottie/validate required,number +DOCKER_REDIS_HOST_PORT="${REDIS_PORT:?error}" + +# The filename that Redis should store its config file within +# +# NOTE: The file *MUST* exists (even empty) before enabling this setting! +# +# Use a command like [touch "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/redis/redis.conf"] to create it. +# +# @default "" +# @dottie/validate required +#DOCKER_REDIS_CONFIG_FILE="/etc/redis/redis.conf" +# How often Docker health check should run for [redis] service +# +# @default "10s" +# @dottie/validate required +DOCKER_REDIS_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" + +################################################################################ +# docker db +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [db] service +#DOCKER_DB_PROFILE= + +# Docker image for the DB service +# @dottie/validate required +DOCKER_DB_IMAGE="mariadb:${DB_VERSION}" + +# Command to pass to the [db] server container +# @dottie/validate required +DOCKER_DB_COMMAND="--default-authentication-plugin=mysql_native_password" + +# Path (on host system) where the [db] container will store its data +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +# @dottie/validate required,dir +DOCKER_DB_HOST_DATA_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/db" + +# Path (inside the container) where the [db] will store its data. +# +# Path MUST be absolute. +# +# For MySQL this should be [/var/lib/mysql] +# For PostgreSQL this should be [/var/lib/postgresql/data] +# @dottie/validate required +DOCKER_DB_CONTAINER_DATA_PATH="/var/lib/mysql" + +# Port that the database will listen on *OUTSIDE* the container (e.g. the host machine) +# +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +# @dottie/validate required,number +DOCKER_DB_HOST_PORT="${DB_PORT:?error}" + +# Port that the database will listen on *INSIDE* the container +# +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +# @dottie/validate required,number +DOCKER_DB_CONTAINER_PORT="${DB_PORT:?error}" + +# root password for the database. By default uses DB_PASSWORD +# but can be changed in situations where you are migrating +# to the included docker-compose and have a different password +# set already +# +# @dottie/validate required +DOCKER_DB_ROOT_PASSWORD="${DB_PASSWORD:?error}" + +# How often Docker health check should run for [db] service +# @dottie/validate required +DOCKER_DB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" + +################################################################################ +# docker web +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [web] service +#DOCKER_WEB_PROFILE="" + +# Port to expose [web] container will listen on *outside* the container (e.g. the host machine) for *HTTP* traffic only +# @dottie/validate required,number +DOCKER_WEB_PORT_EXTERNAL_HTTP="8080" + +# How often Docker health check should run for [web] service +# @dottie/validate required +DOCKER_WEB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" + +################################################################################ +# docker worker +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [worker] service +#DOCKER_WORKER_PROFILE="" + +# How often Docker health check should run for [worker] service +# @dottie/validate required +DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" + +################################################################################ +# docker proxy +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [proxy] and [proxy-acme] service +#DOCKER_PROXY_PROFILE= + +# Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service +#DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE:-}" + +# The version of nginx-proxy to use +# +# @see https://hub.docker.com/r/nginxproxy/nginx-proxy +# @dottie/validate required +DOCKER_PROXY_VERSION="1.4" + +# How often Docker health check should run for [proxy] service +# @dottie/validate required +DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" + +# Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTP traffic +# @dottie/validate required,number +DOCKER_PROXY_HOST_PORT_HTTP="80" + +# Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTPS traffic +# @dottie/validate required,number +DOCKER_PROXY_HOST_PORT_HTTPS="443" + +# Path to the Docker socket on the *host* +# @dottie/validate required,file +DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH="/var/run/docker.sock" + +# The host to request LetsEncrypt certificate for +# @dottie/validate required,fqdn +DOCKER_PROXY_LETSENCRYPT_HOST="${APP_DOMAIN}" + +# The e-mail to use for Lets Encrypt certificate requests. +# @dottie/validate required,email +DOCKER_PROXY_LETSENCRYPT_EMAIL="${INSTANCE_CONTACT_EMAIL:?error}" + +# Lets Encrypt staging/test servers for certificate requests. +# +# Setting this to any value will change to letsencrypt test servers. +#DOCKER_PROXY_LETSENCRYPT_TEST="1" diff --git a/.env.example b/.env.example index d4d7228d1..79ce65337 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,7 @@ OPEN_REGISTRATION="false" ENFORCE_EMAIL_VERIFICATION="false" PF_MAX_USERS="1000" OAUTH_ENABLED="true" +ENABLE_CONFIG_CACHE=true # Media Configuration PF_OPTIMIZE_IMAGES="true" diff --git a/.env.testing b/.env.testing index 258d8d740..63209d91b 100644 --- a/.env.testing +++ b/.env.testing @@ -1,3 +1,5 @@ +# shellcheck disable=SC2034,SC2148 + APP_NAME="Pixelfed Test" APP_ENV=local APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E= @@ -62,8 +64,8 @@ CS_BLOCKED_DOMAINS='example.org,example.net,example.com' CS_CW_DOMAINS='example.org,example.net,example.com' CS_UNLISTED_DOMAINS='example.org,example.net,example.com' -## Optional +## Optional #HORIZON_DARKMODE=false # Horizon theme darkmode -#HORIZON_EMBED=false # Single Docker Container mode +#HORIZON_EMBED=false # Single Docker Container mode ENABLE_CONFIG_CACHE=false diff --git a/.gitattributes b/.gitattributes index 967315dd3..25c1b1b65 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,10 @@ *.scss linguist-vendored *.js linguist-vendored CHANGELOG.md export-ignore + +# Collapse diffs for generated files: +public/**/*.js text -diff +public/**/*.json text -diff +public/**/*.css text -diff +public/img/* binary -diff +public/fonts/* binary -diff diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml deleted file mode 100644 index 34f31cf08..000000000 --- a/.github/workflows/build-docker.yml +++ /dev/null @@ -1,125 +0,0 @@ ---- -name: Build Docker image - -on: - workflow_dispatch: - push: - branches: - - dev - tags: - - '*' - pull_request: - paths: - - .github/workflows/build-docker.yml - - contrib/docker/Dockerfile.apache - - contrib/docker/Dockerfile.fpm -permissions: - contents: read - -jobs: - build-docker-apache: - runs-on: ubuntu-latest - - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Docker Lint - uses: hadolint/hadolint-action@v3.0.0 - with: - dockerfile: contrib/docker/Dockerfile.apache - failure-threshold: error - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 - secrets: inherit - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - if: github.event_name != 'pull_request' - - - name: Fetch tags - uses: docker/metadata-action@v4 - secrets: inherit - id: meta - with: - images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed - flavor: | - latest=auto - suffix=-apache - tags: | - type=edge,branch=dev - type=pep440,pattern={{raw}} - type=pep440,pattern=v{{major}}.{{minor}} - type=ref,event=pr - - - name: Build and push Docker image - uses: docker/build-push-action@v3 - with: - context: . - file: contrib/docker/Dockerfile.apache - platforms: linux/amd64,linux/arm64 - builder: ${{ steps.buildx.outputs.name }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max - - build-docker-fpm: - runs-on: ubuntu-latest - - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Docker Lint - uses: hadolint/hadolint-action@v3.0.0 - with: - dockerfile: contrib/docker/Dockerfile.fpm - failure-threshold: error - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 - secrets: inherit - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - if: github.event_name != 'pull_request' - - - name: Fetch tags - uses: docker/metadata-action@v4 - secrets: inherit - id: meta - with: - images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed - flavor: | - suffix=-fpm - tags: | - type=edge,branch=dev - type=pep440,pattern={{raw}} - type=pep440,pattern=v{{major}}.{{minor}} - type=ref,event=pr - - - name: Build and push Docker image - uses: docker/build-push-action@v3 - with: - context: . - file: contrib/docker/Dockerfile.fpm - platforms: linux/amd64,linux/arm64 - builder: ${{ steps.buildx.outputs.name }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..8fdea53a1 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,230 @@ +--- +name: Docker + +on: + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch + workflow_dispatch: + + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push + push: + branches: + - dev + - staging + tags: + - "*" + + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request + pull_request: + types: + - opened + - reopened + - synchronize + +jobs: + lint: + name: hadolint + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Docker Lint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: Dockerfile + failure-threshold: error + + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + env: + SHELLCHECK_OPTS: --shell=bash --external-sources + with: + version: v0.9.0 + additional_files: "*.envsh .env .env.docker .env.example .env.testing" + + bats: + name: Bats Testing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run bats + run: docker run -v "$PWD:/var/www" bats/bats:latest /var/www/tests/bats + + build: + name: Build, Test, and Push + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + # See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs + matrix: + php_version: + - 8.2 + - 8.3 + target_runtime: + - apache + - fpm + - nginx + php_base: + - apache + - fpm + + # See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#excluding-matrix-configurations + # See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixexclude + exclude: + # targeting [apache] runtime with [fpm] base type doesn't make sense + - target_runtime: apache + php_base: fpm + + # targeting [fpm] runtime with [apache] base type doesn't make sense + - target_runtime: fpm + php_base: apache + + # targeting [nginx] runtime with [apache] base type doesn't make sense + - target_runtime: nginx + php_base: apache + + # See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-concurrency-and-the-default-behavior + concurrency: + group: docker-build-${{ github.ref }}-${{ matrix.php_base }}-${{ matrix.php_version }}-${{ matrix.target_runtime }} + cancel-in-progress: true + + permissions: + contents: read + packages: write + + env: + # Set the repo variable [DOCKER_HUB_USERNAME] to override the default + # at https://github.com///settings/variables/actions + DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'pixelfed' }} + + # Set the repo variable [DOCKER_HUB_ORGANISATION] to override the default + # at https://github.com///settings/variables/actions + DOCKER_HUB_ORGANISATION: ${{ vars.DOCKER_HUB_ORGANISATION || 'pixelfed' }} + + # Set the repo variable [DOCKER_HUB_REPO] to override the default + # at https://github.com///settings/variables/actions + DOCKER_HUB_REPO: ${{ vars.DOCKER_HUB_REPO || 'pixelfed' }} + + # For Docker Hub pushing to work, you need the secret [DOCKER_HUB_TOKEN] + # set to your Personal Access Token at https://github.com///settings/secrets/actions + # + # ! NOTE: no [login] or [push] will happen to Docker Hub until this secret is set! + HAS_DOCKER_HUB_CONFIGURED: ${{ secrets.DOCKER_HUB_TOKEN != '' }} + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + id: buildx + with: + version: v0.12.0 # *or* newer, needed for annotations to work + + # See: https://github.com/docker/login-action?tab=readme-ov-file#github-container-registry + - name: Log in to the GitHub Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # See: https://github.com/docker/login-action?tab=readme-ov-file#docker-hub + - name: Login to Docker Hub registry (conditionally) + if: ${{ env.HAS_DOCKER_HUB_CONFIGURED == true }} + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Docker meta + uses: docker/metadata-action@v5 + id: meta + with: + images: | + name=ghcr.io/${{ github.repository }},enable=true + name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }},enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }} + flavor: | + latest=auto + suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }} + tags: | + type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }} + type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }} + type=pep440,pattern={{raw}} + type=pep440,pattern=v{{major}}.{{minor}} + type=ref,event=branch,prefix=branch- + type=ref,event=pr,prefix=pr- + type=ref,event=tag + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + + - name: Docker meta (Cache) + uses: docker/metadata-action@v5 + id: cache + with: + images: | + name=ghcr.io/${{ github.repository }}-cache,enable=true + name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }}-cache,enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }} + flavor: | + latest=auto + suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }} + tags: | + type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }} + type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }} + type=pep440,pattern={{raw}} + type=pep440,pattern=v{{major}}.{{minor}} + type=ref,event=branch,prefix=branch- + type=ref,event=pr,prefix=pr- + type=ref,event=tag + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + target: ${{ matrix.target_runtime }}-runtime + platforms: linux/amd64,linux/arm64 + builder: ${{ steps.buildx.outputs.name }} + tags: ${{ steps.meta.outputs.tags }} + annotations: ${{ steps.meta.outputs.annotations }} + push: true + sbom: true + provenance: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + PHP_BASE_TYPE=${{ matrix.php_base }} + cache-from: | + type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + cache-to: | + type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + ${{ steps.cache.outputs.tags }} + + # goss validate the image + # + # See: https://github.com/goss-org/goss + - uses: e1himself/goss-installation-action@v1 + with: + version: "v0.4.4" + - name: Execute Goss tests + run: | + dgoss run \ + -v "./.env.testing:/var/www/.env" \ + -e "EXPECTED_PHP_VERSION=${{ matrix.php_version }}" \ + -e "PHP_BASE_TYPE=${{ matrix.php_base }}" \ + ${{ steps.meta.outputs.tags }} diff --git a/.gitignore b/.gitignore index 0494cee10..8a37ec621 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,31 @@ +.DS_Store +/.bash_history +/.bash_profile +/.bashrc +/.composer +/.env +/.env.dottie-backup +#/.git +/.git-credentials +/.gitconfig +#/.gitignore +/.idea +/.vagrant +/bootstrap/cache +/docker-compose-state/ +/Homestead.json +/Homestead.yaml /node_modules +/npm-debug.log /public/hot /public/storage +/public/vendor/horizon /storage/*.key +/storage/docker /vendor -/.idea -/.vscode -/.vagrant -/docker-volumes -Homestead.json -Homestead.yaml -npm-debug.log -yarn-error.log -.env -.DS_Store -.bash_profile -.bash_history -.bashrc -.gitconfig -.git-credentials -/.composer/ -/nginx.conf +/yarn-error.log +/public/build + +# Exceptions - these *MUST* be last +!/bootstrap/cache/.gitignore +!/public/vendor/horizon/.gitignore diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 000000000..27fa2ff27 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,6 @@ +ignored: + - DL3002 # warning: Last USER should not be root + - DL3008 # warning: Pin versions in apt get install. Instead of `apt-get install ` use `apt-get install =` + - DL3029 # warning: Do not use --platform flag with FROM + - SC2046 # warning: Quote this to prevent word splitting. + - SC2086 # info: Double quote to prevent globbing and word splitting. diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 000000000..cf98a0902 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD013": false, + "MD014": false +} diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 000000000..be92f81fc --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,12 @@ +# See: https://github.com/koalaman/shellcheck/blob/master/shellcheck.1.md#rc-files + +source-path=SCRIPTDIR + +# Allow opening any 'source'd file, even if not specified as input +external-sources=true + +# Turn on warnings for unquoted variables with safe values +enable=quote-safe-variables + +# Turn on warnings for unassigned uppercase variables +enable=check-unassigned-uppercase diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..128e0a295 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,14 @@ +{ + "recommendations": [ + "foxundermoon.shell-format", + "timonwong.shellcheck", + "jetmartin.bats", + "aaron-bond.better-comments", + "streetsidesoftware.code-spell-checker", + "editorconfig.editorconfig", + "github.vscode-github-actions", + "bmewburn.vscode-intelephense-client", + "redhat.vscode-yaml", + "ms-azuretools.vscode-docker" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..6446fb6f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "shellformat.useEditorConfig": true, + "[shellscript]": { + "files.eol": "\n", + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, + "[dockercompose]": { + "editor.defaultFormatter": "redhat.vscode-yaml", + "editor.autoIndent": "advanced", + }, + "yaml.schemas": { + "https://json.schemastore.org/composer": "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json" + }, + "files.associations": { + ".env": "shellscript", + ".env.*": "shellscript" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index dc2b6cb11..96d9bb00a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,261 @@ # Release Notes -## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.9...dev) +## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev) + +### OAuth +- Fix oauth oob (urn:ietf:wg:oauth:2.0:oob) support. ([8afbdb03](https://github.com/pixelfed/pixelfed/commit/8afbdb03)) + +### Updates +- Update AP helpers, reject statuses with invalid dates ([960f3849](https://github.com/pixelfed/pixelfed/commit/960f3849)) +- Update DirectMessage API, fix broken threading ([044d410c](https://github.com/pixelfed/pixelfed/commit/044d410c)) +- Update Status caption render logic ([fb8dbb95](https://github.com/pixelfed/pixelfed/commit/fb8dbb95)) +- Update ApiV1Controller, fix bookmark bug. Closes #5216 ([9f7cc52c](https://github.com/pixelfed/pixelfed/commit/9f7cc52c)) +- Update Status caption logic, stop storing duplicate html caption in db and defer to cached StatusService rendering ([9eeb7b67](https://github.com/pixelfed/pixelfed/commit/9eeb7b67)) +- Update AutolinkService, optimize lookups ([eac2c196](https://github.com/pixelfed/pixelfed/commit/eac2c196)) +- Update DirectMessageController, remove 72h limit for admins ([639df410](https://github.com/pixelfed/pixelfed/commit/639df410)) +- Update StatusService, fix newlines ([56c07b7a](https://github.com/pixelfed/pixelfed/commit/56c07b7a)) +- Update confirm email template, add plaintext link. Fixes #5375 ([45986707](https://github.com/pixelfed/pixelfed/commit/45986707)) +- Update UserVerifyEmail command ([77da9ad8](https://github.com/pixelfed/pixelfed/commit/77da9ad8)) +- Update StatusStatelessTransformer, refactor the caption field to be compliant with the MastoAPI. Fixes #5364 ([79039ba5](https://github.com/pixelfed/pixelfed/commit/79039ba5)) +- Update mailgun config, add endpoint and scheme ([271d5114](https://github.com/pixelfed/pixelfed/commit/271d5114)) +- Update search and status logic to fix postgres bugs ([8c39ef4](https://github.com/pixelfed/pixelfed/commit/8c39ef4)) +- ([](https://github.com/pixelfed/pixelfed/commit/)) + +## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev) + +### Added +- Implement Admin Domain Blocks API (Mastodon API Compatible) [ThisIsMissEm](https://github.com/ThisIsMissEm) ([#5021](https://github.com/pixelfed/pixelfed/pull/5021)) +- Authorize Interaction support (for handling remote interactions) ([4ca7c6c3](https://github.com/pixelfed/pixelfed/commit/4ca7c6c3)) +- Contact Form Admin Responses ([52cc6090](https://github.com/pixelfed/pixelfed/commit/52cc6090)) +- Profile Carousels ([8af77a3f](https://github.com/pixelfed/pixelfed/commit/8af77a3f)) +- Moderated Profiles ([39f16321](https://github.com/pixelfed/pixelfed/commit/39f16321)) + +### Federation +- Add ActiveSharedInboxService, for efficient sharedInbox caching ([1a6a3397](https://github.com/pixelfed/pixelfed/commit/1a6a3397)) +- Add MovePipeline queue jobs ([9904d05f](https://github.com/pixelfed/pixelfed/commit/9904d05f)) +- Add ActivityPub Move validator ([909a6c72](https://github.com/pixelfed/pixelfed/commit/909a6c72)) +- Add delay to move handler to allow for remote cache invalidation ([8a362c12](https://github.com/pixelfed/pixelfed/commit/8a362c12)) + +### Updates +- Update ApiV1Controller, add support for notification filter types ([f61159a1](https://github.com/pixelfed/pixelfed/commit/f61159a1)) +- Update ApiV1Dot1Controller, fix mutual api ([a8bb97b2](https://github.com/pixelfed/pixelfed/commit/a8bb97b2)) +- Update ApiV1Controller, fix /api/v1/favourits pagination ([72f68160](https://github.com/pixelfed/pixelfed/commit/72f68160)) +- Update RegisterController, update username constraints, require atleast one alpha char ([dd6e3cc2](https://github.com/pixelfed/pixelfed/commit/dd6e3cc2)) +- Update AdminUser, fix entity casting ([cb5620d4](https://github.com/pixelfed/pixelfed/commit/cb5620d4)) +- Update instance config, update network cache feed max_hours_old falloff to 90 days instead of 6 hours to allow for less active instances to have more results ([c042d135](https://github.com/pixelfed/pixelfed/commit/c042d135)) +- Update ApiV1Dot1Controller, add new single media status create endpoint ([b03f5cec](https://github.com/pixelfed/pixelfed/commit/b03f5cec)) +- Update AdminSettings component, add link to Custom CSS settings ([958daac4](https://github.com/pixelfed/pixelfed/commit/958daac4)) +- Update ApiV1Controller, fix v1/instance stats, force cast to int ([dcd95d68](https://github.com/pixelfed/pixelfed/commit/dcd95d68)) +- Update BeagleService, disable discovery if AP is disabled ([6cd1cbb4](https://github.com/pixelfed/pixelfed/commit/6cd1cbb4)) +- Update NodeinfoService, fix typo ([edad436d](https://github.com/pixelfed/pixelfed/commit/edad436d)) +- Update ActivityPubFetchService, reduce cache ttl from 1 hour to 7.5 mins and add uncached fetchRequest method ([21da2b64](https://github.com/pixelfed/pixelfed/commit/21da2b64)) +- Update UserAccountDelete command, increase sharedInbox ttl from 12h to 14d ([be02f48a](https://github.com/pixelfed/pixelfed/commit/be02f48a)) +- Update HttpSignature, add signRaw method and improve error checking ([d4cf9181](https://github.com/pixelfed/pixelfed/commit/d4cf9181)) +- Update AP helpers, add forceBanCheck param to validateUrl method ([42424028](https://github.com/pixelfed/pixelfed/commit/42424028)) +- Update layout, add og:logo ([4cc576e1](https://github.com/pixelfed/pixelfed/commit/4cc576e1)) +- Update ReblogService, fix cache sync issues ([3de8ceca](https://github.com/pixelfed/pixelfed/commit/3de8ceca)) +- Update config, allow Beagle discover service to be disabled ([de4ce3c8](https://github.com/pixelfed/pixelfed/commit/de4ce3c8)) +- Update ApiV1Dot1Controller, allow upto 5 similar push tokens ([7820b506](https://github.com/pixelfed/pixelfed/commit/7820b506)) +- Update AdminReports, add missing click handler. Fixes #5332 ([fe48b8ad](https://github.com/pixelfed/pixelfed/commit/fe48b8ad)) +- Improve media filtering by using OffscreenCanvas, if supported ([aea5392](https://github.com/pixelfed/pixelfed/commit/aea5392)) + +## [v0.12.3 (2024-07-01)](https://github.com/pixelfed/pixelfed/compare/v0.12.2...v0.12.3) + +### Updates +- Fix migrations bug ([4d1180b1](https://github.com/pixelfed/pixelfed/commit/4d1180b1)) + +## [v0.12.2 (2024-07-01)](https://github.com/pixelfed/pixelfed/compare/v0.12.1...v0.12.2) + +### Framework +- Updated to Laravel 11 (requires php 8.2+) + +### Added +- New api/v1/instance/peers API endpoint, disabled by default ([4aad1c22](https://github.com/pixelfed/pixelfed/commit/4aad1c22)) +- Added disable_embeds setting, and fix cache invalidation in other settings ([c5e7e917](https://github.com/pixelfed/pixelfed/commit/c5e7e917)) + +### Updates +- Update DirectMessageController, add 72 hour delay for new accounts before they can send a DM ([61d105fd](https://github.com/pixelfed/pixelfed/commit/61d105fd)) +- Update AdminCuratedRegisterController, increase message length from 1000 to 3000 ([9a5e3471](https://github.com/pixelfed/pixelfed/commit/9a5e3471)) +- Update ApiV1Controller, add pe (pixelfed entity) support to /api/v1/statuses/{id}/context endpoint ([d645d6ca](https://github.com/pixelfed/pixelfed/commit/d645d6ca)) +- Update Admin Curated Onboarding, add select-all/mass action operations ([b22cac94](https://github.com/pixelfed/pixelfed/commit/b22cac94)) +- Update AdminCuratedRegisterController, fix existing account approval ([cbb96cfd](https://github.com/pixelfed/pixelfed/commit/cbb96cfd)) +- Update ActivityPubFetchService, fix Friendica bug ([e4edc6f1](https://github.com/pixelfed/pixelfed/commit/e4edc6f1)) +- Update ProfileController, fix atom feed cache ttl. Fixes #5093 ([921e2965](https://github.com/pixelfed/pixelfed/commit/921e2965)) +- Update CollectionsController, add new self route ([bc2495c6](https://github.com/pixelfed/pixelfed/commit/bc2495c6)) +- Update FederationController, add webfinger support for actor uri. Fixes #5068 ([24194f7d](https://github.com/pixelfed/pixelfed/commit/24194f7d)) +- Update FetchNodeinfoPipeline, set last_fetched_at timestamp ([a7fce91e](https://github.com/pixelfed/pixelfed/commit/a7fce91e)) +- Update task scheduler, add weekly instance scan to check nodeinfo for known instances ([dc6b9f46](https://github.com/pixelfed/pixelfed/commit/dc6b9f46)) +- Update AP fetch service and domain service ([42915ff9](https://github.com/pixelfed/pixelfed/commit/42915ff9)) +- Update ApiV1Controller, add settings to verify_credentials endpoint ([3f4e0b94](https://github.com/pixelfed/pixelfed/commit/3f4e0b94)) +- Update ApiV1Controller, fix update_credentials boolean handling ([19c62aaa](https://github.com/pixelfed/pixelfed/commit/19c62aaa)) +- Update ApiV1Controller, fix cache invalidation bug in update_credentials ([d56a4108](https://github.com/pixelfed/pixelfed/commit/d56a4108)) +- Update ApiV1Controller, fix self relationship response ([28bc7aa4](https://github.com/pixelfed/pixelfed/commit/28bc7aa4)) +- Update ApiController, add pe support to like/unlike endpoints ([679ef677](https://github.com/pixelfed/pixelfed/commit/679ef677)) +- Update ApiV1Dot1Controller, fix username to id endpoint ([4d6cea9a](https://github.com/pixelfed/pixelfed/commit/4d6cea9a)) +- Update StatusController, cache AP object ([a75b89b2](https://github.com/pixelfed/pixelfed/commit/a75b89b2)) +- Update status embed, add support for album carousels ([f4898db9](https://github.com/pixelfed/pixelfed/commit/f4898db9)) +- Update profile embeds, add support for albums ([4fd156c4](https://github.com/pixelfed/pixelfed/commit/4fd156c4)) +- Update DirectMessageController, add timestamps to threads ([b24d2554](https://github.com/pixelfed/pixelfed/commit/b24d2554)) +- Update DirectMessageController, add carousel entity to threads ([96f24f33](https://github.com/pixelfed/pixelfed/commit/96f24f33)) +- Update and refactor total local post count logic, cache value and schedule updates twice daily to eliminate the perf issue on larger instances ([4f2b8ed2](https://github.com/pixelfed/pixelfed/commit/4f2b8ed2)) +- Update Media model, fix broken thumbnail/gray thumbnail bug ([e33643c2](https://github.com/pixelfed/pixelfed/commit/e33643c2)) +- Update StatusController, fix unlisted post guest/ap access bug ([83098428](https://github.com/pixelfed/pixelfed/commit/83098428)) +- Update discover, add network trending using Beagle API ([2cae8b48](https://github.com/pixelfed/pixelfed/commit/2cae8b48)) + +## [v0.12.1 (2024-05-07)](https://github.com/pixelfed/pixelfed/compare/v0.12.0...v0.12.1) + +### Updates +- Update ApiV1Dot1Controller, fix in app registration bug that prevents proper auth flow due to missing oauth scopes ([cbf996c9](https://github.com/pixelfed/pixelfed/commit/cbf996c9)) +- Update ConfigCacheService, fix database race condition and fallback to file config and enable by default ([60a62b59](https://github.com/pixelfed/pixelfed/commit/60a62b59)) + +## [v0.12.0 (2024-04-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.13...v0.12.0) + +### Updates + +- Update SoftwareUpdateService, add command to refresh latest versions ([632f2cb6](https://github.com/pixelfed/pixelfed/commit/632f2cb6)) +- Update Post.vue, fix cache bug ([3a27e637](https://github.com/pixelfed/pixelfed/commit/3a27e637)) +- Update StatusHashtagService, use more efficient cached count ([592c8412](https://github.com/pixelfed/pixelfed/commit/592c8412)) +- Update DiscoverController, handle discover hashtag redirects ([18382e8a](https://github.com/pixelfed/pixelfed/commit/18382e8a)) +- Update ApiV1Controller, use admin filter service ([94503a1c](https://github.com/pixelfed/pixelfed/commit/94503a1c)) +- Update SearchApiV2Service, use more efficient query ([cee618e8](https://github.com/pixelfed/pixelfed/commit/cee618e8)) +- Update Curated Onboarding view, fix concierge form ([15ad69f7](https://github.com/pixelfed/pixelfed/commit/15ad69f7)) +- Update AP Profile Transformer, add `suspended` attribute ([25f3fa06](https://github.com/pixelfed/pixelfed/commit/25f3fa06)) +- Update AP Profile Transformer, fix movedTo attribute ([63100fe9](https://github.com/pixelfed/pixelfed/commit/63100fe9)) +- Update AP Profile Transformer, fix suspended attributes ([2e5e68e4](https://github.com/pixelfed/pixelfed/commit/2e5e68e4)) +- Update PrivacySettings controller, add cache invalidation ([e742d595](https://github.com/pixelfed/pixelfed/commit/e742d595)) +- Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup ([853a729f](https://github.com/pixelfed/pixelfed/commit/853a729f)) +- Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable ([95199843](https://github.com/pixelfed/pixelfed/commit/95199843)) +- Update AP transformers, add DeleteActor activity ([bcce1df6](https://github.com/pixelfed/pixelfed/commit/bcce1df6)) +- Update commands, add user account delete cli command to federate account deletion ([4aa0e25f](https://github.com/pixelfed/pixelfed/commit/4aa0e25f)) +- Update web-api popular accounts route to its own method to remove the breaking oauth scope bug ([a4bc5ce3](https://github.com/pixelfed/pixelfed/commit/a4bc5ce3)) +- Update config cache ([5e4d4eff](https://github.com/pixelfed/pixelfed/commit/5e4d4eff)) +- Update Config, use config_cache ([7785a2da](https://github.com/pixelfed/pixelfed/commit/7785a2da)) +- Update ApiV1Dot1Controller, use config_cache for in-app registration ([b0cb4456](https://github.com/pixelfed/pixelfed/commit/b0cb4456)) +- Update captcha, use config_cache helper ([8a89e3c9](https://github.com/pixelfed/pixelfed/commit/8a89e3c9)) +- Update custom emoji, add config_cache support ([481314cd](https://github.com/pixelfed/pixelfed/commit/481314cd)) +- Update ProfileController, fix permalink redirect bug ([75081e60](https://github.com/pixelfed/pixelfed/commit/75081e60)) +- Update admin css, use font-display:swap for nucleo icons ([8a0c456e](https://github.com/pixelfed/pixelfed/commit/8a0c456e)) +- Update PixelfedDirectoryController, fix boolean cast bug ([f08aab22](https://github.com/pixelfed/pixelfed/commit/f08aab22)) +- Update PixelfedDirectoryController, use cached stats ([f2f2a809](https://github.com/pixelfed/pixelfed/commit/f2f2a809)) +- Update AdminDirectoryController, fix type casting ([ad506e90](https://github.com/pixelfed/pixelfed/commit/ad506e90)) +- Update image pipeline, use config_cache ([a72188a7](https://github.com/pixelfed/pixelfed/commit/a72188a7)) +- Update cloud storage, use config_cache ([665581d8](https://github.com/pixelfed/pixelfed/commit/665581d8)) +- Update pixelfed.max_album_length, use config_cache ([fecbe189](https://github.com/pixelfed/pixelfed/commit/fecbe189)) +- Update media_types, use config_cache ([d670de17](https://github.com/pixelfed/pixelfed/commit/d670de17)) +- Update landing settings, use config_cache ([40478f25](https://github.com/pixelfed/pixelfed/commit/40478f25)) +- Update activitypub setting, use config_cache ([5071aaf4](https://github.com/pixelfed/pixelfed/commit/5071aaf4)) +- Update oauth setting, use config_cache ([ce228f7f](https://github.com/pixelfed/pixelfed/commit/ce228f7f)) +- Update stories config, use config_cache ([d1adb109](https://github.com/pixelfed/pixelfed/commit/d1adb109)) +- Update ig import, use config_cache ([da0e0ffa](https://github.com/pixelfed/pixelfed/commit/da0e0ffa)) +- Update autospam config, use config_cache ([a76cb5f4](https://github.com/pixelfed/pixelfed/commit/a76cb5f4)) +- Update app.name config, use config_cache ([911446c0](https://github.com/pixelfed/pixelfed/commit/911446c0)) +- Update UserObserver, fix type casting ([949e9979](https://github.com/pixelfed/pixelfed/commit/949e9979)) +- Update user_filters, use config_cache ([6ce513f8](https://github.com/pixelfed/pixelfed/commit/6ce513f8)) +- Update filesystems config, add to config_cache ([087b2791](https://github.com/pixelfed/pixelfed/commit/087b2791)) +- Update web-admin routes, add setting api routes ([828a456f](https://github.com/pixelfed/pixelfed/commit/828a456f)) +- Update hashtag component ([cee979ed](https://github.com/pixelfed/pixelfed/commit/cee979ed)) +- Update AdminReadMore component, add .prevent to click action ([704e7b12](https://github.com/pixelfed/pixelfed/commit/704e7b12)) +- Update admin dashboard, add admin settings partials ([eb487123](https://github.com/pixelfed/pixelfed/commit/eb487123)) +- Update admin settings, refactor to vue component ([674e560f](https://github.com/pixelfed/pixelfed/commit/674e560f)) +- Update ConfigCacheService, encrypt keys at rest ([3628b462](https://github.com/pixelfed/pixelfed/commit/3628b462)) +- Update RemoteFollowImportRecent, use MediaPathService ([5162c070](https://github.com/pixelfed/pixelfed/commit/5162c070)) +- Update AdminSettingsController, add user filter max limit settings ([ac1f0748](https://github.com/pixelfed/pixelfed/commit/ac1f0748)) +- Update AdminSettingsController, add AdminSettingsService ([dcc5f416](https://github.com/pixelfed/pixelfed/commit/dcc5f416)) +- Update AdminSettings component, fix user settings ([aba1e13d](https://github.com/pixelfed/pixelfed/commit/aba1e13d)) +- Update AdminInstances component ([ec2fdd61](https://github.com/pixelfed/pixelfed/commit/ec2fdd61)) +- Update AdminSettings, add max_account_size support ([2dcbc1d5](https://github.com/pixelfed/pixelfed/commit/2dcbc1d5)) +- Update AdminSettings, use better validation for user integer settings ([d946afcc](https://github.com/pixelfed/pixelfed/commit/d946afcc)) +- Update spa sass, fix timestamp dark mode bug ([4147f7c5](https://github.com/pixelfed/pixelfed/commit/4147f7c5)) +- Update relationships view, fix unfollow hashtag bug. Fixes #5008 ([8c693640](https://github.com/pixelfed/pixelfed/commit/8c693640)) +- Update PrivacySettings controller, refresh RelationshipService when unmute/unblocking ([b7322b68](https://github.com/pixelfed/pixelfed/commit/b7322b68)) +- Update ApiV1Controller, improve refresh relations logic when (un)muting or (un)blocking ([b8e96a5f](https://github.com/pixelfed/pixelfed/commit/b8e96a5f)) +- Update context menu, add mute/block/unfollow actions and update relationship store accordingly ([81d1e0fd](https://github.com/pixelfed/pixelfed/commit/81d1e0fd)) +- Update docker env, fix config_cache. Fixes #5033 ([858fcbf6](https://github.com/pixelfed/pixelfed/commit/858fcbf6)) +- Update UnfollowPipeline, fix follower count cache bug ([6bdf73de](https://github.com/pixelfed/pixelfed/commit/6bdf73de)) +- Update VideoPresenter component, add webkit-playsinline attribute to video element to prevent the full screen video player ([ad032916](https://github.com/pixelfed/pixelfed/commit/ad032916)) +- Update VideoPlayer component, add playsinline attribute to video element ([8af23607](https://github.com/pixelfed/pixelfed/commit/8af23607)) +- Update StatusController, refactor status embeds ([9a7acc12](https://github.com/pixelfed/pixelfed/commit/9a7acc12)) +- Update ProfileController, refactor profile embeds ([8b8b1ffc](https://github.com/pixelfed/pixelfed/commit/8b8b1ffc)) +- Update profile embed view, fix height bug ([65166570](https://github.com/pixelfed/pixelfed/commit/65166570)) +- Update CustomEmojiService, only return local emoji ([7f8bba44](https://github.com/pixelfed/pixelfed/commit/7f8bba44)) +- Update Like model, increase max likes per day from 500 to 1500 ([4223119f](https://github.com/pixelfed/pixelfed/commit/4223119f)) + +## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13) + +### Features + +- Account Migrations ([#4968](https://github.com/pixelfed/pixelfed/pull/4968)) ([4a6be6212](https://github.com/pixelfed/pixelfed/pull/4968/commits/4a6be6212)) +- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf)) +- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4)) +- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e)) +- Improved Docker Support ([#4844](https://github.com/pixelfed/pixelfed/pull/4844)) ([d92cf7f](https://github.com/pixelfed/pixelfed/commit/d92cf7f)) + +### Updates + +- Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad)) +- Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4)) +- Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623)) +- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9)) +- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e)) +- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d)) +- Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641)) +- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239)) +- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4)) +- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235)) +- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8)) +- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac)) +- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c)) +- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e)) +- Update Inbox, fix flag validation condition, allow profile reports ([402a4607](https://github.com/pixelfed/pixelfed/commit/402a4607)) +- Update AccountTransformer, fix follower/following count visibility bug ([542d1106](https://github.com/pixelfed/pixelfed/commit/542d1106)) +- Update ProfileMigration model, add target relation ([3f053997](https://github.com/pixelfed/pixelfed/commit/3f053997)) +- Update ApiV1Controller, update Notifications endpoint to filter notifications with missing activities ([a933615b](https://github.com/pixelfed/pixelfed/commit/a933615b)) +- Update ApiV1Controller, fix public timeline scope, properly support both local + remote parameters ([d6eac655](https://github.com/pixelfed/pixelfed/commit/d6eac655)) +- Update ApiV1Controller, handle public feed parameter bug to gracefully fallback to min_id=1 when max_id=0 ([e3826c58](https://github.com/pixelfed/pixelfed/commit/e3826c58)) +- Update ApiV1Controller, fix hashtag feed to include private posts from accounts you follow or your own, and your own unlisted posts ([3b5500b3](https://github.com/pixelfed/pixelfed/commit/3b5500b3)) +- Update checkpoint view, improve input autocomplete. Fixes ([#4959](https://github.com/pixelfed/pixelfed/pull/4959)) ([d18824e7](https://github.com/pixelfed/pixelfed/commit/d18824e7)) +- Update navbar.vue, removes the 50px limit ([#4969](https://github.com/pixelfed/pixelfed/pull/4969)) ([7fd5599](https://github.com/pixelfed/pixelfed/commit/7fd5599)) +- Update ComposeModal.vue, add an informative UI error message when trying to create a mixed media album ([#4886](https://github.com/pixelfed/pixelfed/pull/4886)) ([fd4f41a](https://github.com/pixelfed/pixelfed/commit/fd4f41a)) + +## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) + +### Features +- Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a)) +- Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988)) + +### Updates + +- Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3)) +- Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc)) +- Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3)) +- Update ActivityPubFetchService, enforce stricter Content-Type validation ([1232cfc8](https://github.com/pixelfed/pixelfed/commit/1232cfc8)) +- Update status view, fix unlisted/private scope bug ([0f3ca194](https://github.com/pixelfed/pixelfed/commit/0f3ca194)) + +## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11) + +### Fixes +- Fix api endpoints ([fd7f5dbb](https://github.com/pixelfed/pixelfed/commit/fd7f5dbb)) + +## [v0.11.10 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.9...v0.11.10) ### Added - Resilient Media Storage ([#4665](https://github.com/pixelfed/pixelfed/pull/4665)) ([fb1deb6](https://github.com/pixelfed/pixelfed/commit/fb1deb6)) - Video WebP2P ([#4713](https://github.com/pixelfed/pixelfed/pull/4713)) ([0405ef12](https://github.com/pixelfed/pixelfed/commit/0405ef12)) - Added user:2fa command to easily disable 2FA for given account ([c6408fd7](https://github.com/pixelfed/pixelfed/commit/c6408fd7)) - Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde)) +- Added S3 command to rewrite media urls ([5b3a5610](https://github.com/pixelfed/pixelfed/commit/5b3a5610)) +- Experimental home feed ([#4752](https://github.com/pixelfed/pixelfed/pull/4752)) ([c39b9afb](https://github.com/pixelfed/pixelfed/commit/c39b9afb)) +- Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6)) +- Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7)) +- Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46)) +- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac)) +- Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59)) +- Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1)) +- Added S3 IG Import Media Storage support ([#4891](https://github.com/pixelfed/pixelfed/pull/4891)) ([081360b9](https://github.com/pixelfed/pixelfed/commit/081360b9)) ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) - Update AP Helpers, consume actor `indexable` attribute ([fbdcdd9d](https://github.com/pixelfed/pixelfed/commit/fbdcdd9d)) -- ([](https://github.com/pixelfed/pixelfed/commit/)) ### Updates - Update FollowerService, add forget method to RelationshipService call to reduce load when mass purging ([347e4f59](https://github.com/pixelfed/pixelfed/commit/347e4f59)) @@ -40,7 +284,62 @@ - Update ApiV1Dot1Controller, allow iar rate limits to be configurable ([28a80803](https://github.com/pixelfed/pixelfed/commit/28a80803)) - Update ApiV1Dot1Controller, add domain to iar redirect ([1f82d47c](https://github.com/pixelfed/pixelfed/commit/1f82d47c)) - Update ApiV1Dot1Controller, add configurable app confirm rate limit ttl ([4c6a0719](https://github.com/pixelfed/pixelfed/commit/4c6a0719)) -- ([](https://github.com/pixelfed/pixelfed/commit/)) +- Update LikePipeline, dispatch to feed queue. Fixes ([#4723](https://github.com/pixelfed/pixelfed/issues/4723)) ([da510089](https://github.com/pixelfed/pixelfed/commit/da510089)) +- Update AccountImport ([5a2d7e3e](https://github.com/pixelfed/pixelfed/commit/5a2d7e3e)) +- Update ImportPostController, fix IG bug with missing spaces between hashtags ([9c24157a](https://github.com/pixelfed/pixelfed/commit/9c24157a)) +- Update ApiV1Controller, fix mutes in home feed ([ddc21714](https://github.com/pixelfed/pixelfed/commit/ddc21714)) +- Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79)) +- Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c)) +- Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271)) +- Update hashtag following ([015b1b80](https://github.com/pixelfed/pixelfed/commit/015b1b80)) +- Update IncrementPostCount job, prevent overlap ([b2c9cc23](https://github.com/pixelfed/pixelfed/commit/b2c9cc23)) +- Update HashtagFollowService, fix cache invalidation bug ([84f4e885](https://github.com/pixelfed/pixelfed/commit/84f4e885)) +- Update Experimental Home Feed, fix remote posts, shares and reblogs ([c6a6b3ae](https://github.com/pixelfed/pixelfed/commit/c6a6b3ae)) +- Update HashtagService, improve count perf ([3327a008](https://github.com/pixelfed/pixelfed/commit/3327a008)) +- Update StatusHashtagService, remove problematic cache layer ([e5401f85](https://github.com/pixelfed/pixelfed/commit/e5401f85)) +- Update HomeFeedPipeline, fix tag filtering ([f105f4e8](https://github.com/pixelfed/pixelfed/commit/f105f4e8)) +- Update HashtagService, reduce cached_count cache ttl ([15f29f7d](https://github.com/pixelfed/pixelfed/commit/15f29f7d)) +- Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic ([287f903b](https://github.com/pixelfed/pixelfed/commit/287f903b)) +- Update StoryApiV1Controller, add self-carousel endpoint. Fixes ([#4352](https://github.com/pixelfed/pixelfed/issues/4352)) ([bcb88d5b](https://github.com/pixelfed/pixelfed/commit/bcb88d5b)) +- Update FollowServiceWarmCache, use more efficient query ([fe9b4c5a](https://github.com/pixelfed/pixelfed/commit/fe9b4c5a)) +- Update HomeFeedPipeline, observe mutes/blocks during fanout ([8548294c](https://github.com/pixelfed/pixelfed/commit/8548294c)) +- Update FederationController, add proper following/follower counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96)) +- Update FederationController, add proper statuses counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96)) +- Update Inbox handler, fix missing object_url and uri fields for direct statuses ([a0157fce](https://github.com/pixelfed/pixelfed/commit/a0157fce)) +- Update DirectMessageController, deliver direct delete activities to user inbox instead of sharedInbox ([d848792a](https://github.com/pixelfed/pixelfed/commit/d848792a)) +- Update DirectMessageController, dispatch deliver and delete actions to the job queue ([7f462a80](https://github.com/pixelfed/pixelfed/commit/7f462a80)) +- Update Inbox, improve story attribute collection ([06bee36c](https://github.com/pixelfed/pixelfed/commit/06bee36c)) +- Update DirectMessageController, dispatch local deletes to pipeline ([98186564](https://github.com/pixelfed/pixelfed/commit/98186564)) +- Update StatusPipeline, fix Direct and Story notification deletion ([4c95306f](https://github.com/pixelfed/pixelfed/commit/4c95306f)) +- Update Notifications.vue, fix deprecated DM action links for story activities ([4c3823b0](https://github.com/pixelfed/pixelfed/commit/4c3823b0)) +- Update ComposeModal, fix missing alttext post state ([0a068119](https://github.com/pixelfed/pixelfed/commit/0a068119)) +- Update PhotoAlbumPresenter.vue, fix fullscreen mode ([822e9888](https://github.com/pixelfed/pixelfed/commit/822e9888)) +- Update Timeline.vue, improve CHT pagination ([9c43e7e2](https://github.com/pixelfed/pixelfed/commit/9c43e7e2)) +- Update HomeFeedPipeline, fix StatusService validation ([041c0135](https://github.com/pixelfed/pixelfed/commit/041c0135)) +- Update Inbox, improve tombstone query efficiency ([759a4393](https://github.com/pixelfed/pixelfed/commit/759a4393)) +- Update AccountService, add setLastActive method ([ebbd98e7](https://github.com/pixelfed/pixelfed/commit/ebbd98e7)) +- Update ApiV1Controller, set last_active_at ([b6419545](https://github.com/pixelfed/pixelfed/commit/b6419545)) +- Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a)) +- Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc)) +- Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1)) +- Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c)) +- Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168)) +- Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171)) +- Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a)) +- Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b)) +- Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04)) +- Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381)) +- Update AP helpers, refactor post count decrement logic ([b81ae577](https://github.com/pixelfed/pixelfed/commit/b81ae577)) +- Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c)) +- Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389)) +- Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3)) +- Update NodeinfoService, disable redirects ([240e6bbe](https://github.com/pixelfed/pixelfed/commit/240e6bbe)) +- Update Instance model, add entity casts ([289cad47](https://github.com/pixelfed/pixelfed/commit/289cad47)) +- Update FetchNodeinfoPipeline, use more efficient dispatch ([ac01f51a](https://github.com/pixelfed/pixelfed/commit/ac01f51a)) +- Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade)) +- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3)) +- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac)) +- Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! ([4d02d6f1](https://github.com/pixelfed/pixelfed/commit/4d02d6f1)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..69a06cf87 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,18 @@ +# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +* @dansup + +# Docker related files +.editorconfig @jippi @dansup +.env @jippi @dansup +.env.* @jippi @dansup +.hadolint.yaml @jippi @dansup +.shellcheckrc @jippi @dansup +/.github/ @jippi @dansup +/docker/ @jippi @dansup +/tests/ @jippi @dansup +docker-compose.migrate.yml @jippi @dansup +docker-compose.yml @jippi @dansup +goss.yaml @jippi @dansup diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..3f27c84a2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,364 @@ +# syntax=docker/dockerfile:1 +# See https://hub.docker.com/r/docker/dockerfile + +####################################################### +# Configuration +####################################################### + +# See: https://github.com/mlocati/docker-php-extension-installer +ARG DOCKER_PHP_EXTENSION_INSTALLER_VERSION="2.1.80" + +# See: https://github.com/composer/composer +ARG COMPOSER_VERSION="2.6" + +# See: https://nginx.org/ +ARG NGINX_VERSION="1.25.3" + +# See: https://github.com/ddollar/forego +ARG FOREGO_VERSION="0.17.2" + +# See: https://github.com/hairyhenderson/gomplate +ARG GOMPLATE_VERSION="v3.11.6" + +# See: https://github.com/jippi/dottie +ARG DOTTIE_VERSION="v0.9.5" + +### +# PHP base configuration +### + +# See: https://hub.docker.com/_/php/tags +ARG PHP_VERSION="8.1" + +# See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants +ARG PHP_BASE_TYPE="apache" +ARG PHP_DEBIAN_RELEASE="bullseye" + +ARG RUNTIME_UID=33 # often called 'www-data' +ARG RUNTIME_GID=33 # often called 'www-data' + +# APT extra packages +ARG APT_PACKAGES_EXTRA= + +# Extensions installed via [pecl install] +# ! NOTE: imagick is installed from [master] branch on GitHub due to 8.3 bug on ARM that haven't +# ! been released yet (after +10 months)! +# ! See: https://github.com/Imagick/imagick/pull/641 +ARG PHP_PECL_EXTENSIONS="redis https://codeload.github.com/Imagick/imagick/tar.gz/28f27044e435a2b203e32675e942eb8de620ee58" +ARG PHP_PECL_EXTENSIONS_EXTRA= + +# Extensions installed via [docker-php-ext-install] +ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd" +ARG PHP_EXTENSIONS_EXTRA="" +ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite" + +# GPG key for nginx apt repository +ARG NGINX_GPGKEY="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62" + +# GPP key path for nginx apt repository +ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg" + +####################################################### +# Docker "copy from" images +####################################################### + +# Composer docker image from Docker Hub +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +FROM composer:${COMPOSER_VERSION} AS composer-image + +# php-extension-installer image from Docker Hub +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +FROM mlocati/php-extension-installer:${DOCKER_PHP_EXTENSION_INSTALLER_VERSION} AS php-extension-installer + +# nginx webserver from Docker Hub. +# Used to copy some docker-entrypoint files for [nginx-runtime] +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +FROM nginx:${NGINX_VERSION} AS nginx-image + +# Forego is a Procfile "runner" that makes it trival to run multiple +# processes under a simple init / PID 1 process. +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +# +# See: https://github.com/nginx-proxy/forego +FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image + +# Dottie makes working with .env files easier and safer +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +# +# See: https://github.com/jippi/dottie +FROM ghcr.io/jippi/dottie:${DOTTIE_VERSION} AS dottie-image + +# gomplate-image grabs the gomplate binary from GitHub releases +# +# It's in its own layer so it can be fetched in parallel with other build steps +FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image + +ARG TARGETARCH +ARG TARGETOS +ARG GOMPLATE_VERSION + +RUN set -ex \ + && curl \ + --silent \ + --show-error \ + --location \ + --output /usr/local/bin/gomplate \ + https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS}-${TARGETARCH} \ + && chmod +x /usr/local/bin/gomplate \ + && /usr/local/bin/gomplate --version + +####################################################### +# Base image +####################################################### + +FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base + +ARG BUILDKIT_SBOM_SCAN_STAGE="true" + +ARG APT_PACKAGES_EXTRA +ARG PHP_DEBIAN_RELEASE +ARG PHP_VERSION +ARG RUNTIME_GID +ARG RUNTIME_UID +ARG TARGETPLATFORM + +ENV DEBIAN_FRONTEND="noninteractive" + +# Ensure we run all scripts through 'bash' rather than 'sh' +SHELL ["/bin/bash", "-c"] + +# Set www-data to be RUNTIME_UID/RUNTIME_GID +RUN groupmod --gid ${RUNTIME_GID} www-data \ + && usermod --uid ${RUNTIME_UID} --gid ${RUNTIME_GID} www-data + +RUN set -ex \ + && mkdir -pv /var/www/ \ + && chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www + +WORKDIR /var/www/ + +ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA} + +# Install and configure base layer +COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh + +RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + /docker/install/base.sh + +####################################################### +# PHP: extensions +####################################################### + +FROM base AS php-extensions + +ARG PHP_DEBIAN_RELEASE +ARG PHP_EXTENSIONS +ARG PHP_EXTENSIONS_DATABASE +ARG PHP_EXTENSIONS_EXTRA +ARG PHP_PECL_EXTENSIONS +ARG PHP_PECL_EXTENSIONS_EXTRA +ARG PHP_VERSION +ARG TARGETPLATFORM + +COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ + +COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh + +RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/tmp/pear \ + --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + PHP_EXTENSIONS=${PHP_EXTENSIONS} \ + PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} \ + PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} \ + PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} \ + PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} \ + /docker/install/php-extensions.sh + +####################################################### +# Node: Build frontend +####################################################### + +# NOTE: Since the nodejs build is CPU architecture agnostic, +# we only want to build once and cache it for other architectures. +# We force the (CPU) [--platform] here to be architecture +# of the "builder"/"server" and not the *target* CPU architecture +# (e.g.) building the ARM version of Pixelfed on AMD64. +FROM --platform=${BUILDARCH} node:lts AS frontend-build + +ARG BUILDARCH +ARG BUILD_FRONTEND=0 +ARG RUNTIME_UID +ARG RUNTIME_GID + +ARG NODE_ENV=production +ENV NODE_ENV=$NODE_ENV + +WORKDIR /var/www/ + +SHELL [ "/usr/bin/bash", "-c" ] + +# Install NPM dependencies +RUN --mount=type=cache,id=pixelfed-node-${BUILDARCH},sharing=locked,target=/tmp/cache \ + --mount=type=bind,source=package.json,target=/var/www/package.json \ + --mount=type=bind,source=package-lock.json,target=/var/www/package-lock.json \ +< "$NGINX_GPGKEY_PATH" \ + && echo "deb [signed-by=${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ ${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends nginx=${NGINX_VERSION}* + +# copy docker entrypoints from the *real* nginx image directly +COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/ +COPY docker/nginx/root / +COPY docker/nginx/Procfile . + +STOPSIGNAL SIGQUIT + +CMD ["forego", "start", "-r"] diff --git a/README.md b/README.md index 7c0117a52..e2a90535e 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,10 @@ We would like to extend our thanks to the following sponsors for funding Pixelfe - [NLnet Foundation](https://nlnet.nl) and [NGI0 Discovery](https://nlnet.nl/discovery/), part of the [Next Generation Internet](https://ngi.eu) initiative. + +

This project is supported by:

+

+ + + +

diff --git a/app/Auth/BearerTokenResponse.php b/app/Auth/BearerTokenResponse.php index 79cd97c85..0e1aa8a19 100644 --- a/app/Auth/BearerTokenResponse.php +++ b/app/Auth/BearerTokenResponse.php @@ -18,8 +18,7 @@ class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerToke protected function getExtraParams(AccessTokenEntityInterface $accessToken) { return [ - 'created_at' => time(), - 'scope' => 'read write follow push' + 'created_at' => time(), ]; } } diff --git a/app/Console/Commands/AccountPostCountStatUpdate.php b/app/Console/Commands/AccountPostCountStatUpdate.php new file mode 100644 index 000000000..6d5ba00a6 --- /dev/null +++ b/app/Console/Commands/AccountPostCountStatUpdate.php @@ -0,0 +1,57 @@ +count(); + if($statusCount != $acct['statuses_count']) { + $profile = Profile::find($id); + if(!$profile) { + AccountStatService::removeFromPostCount($id); + continue; + } + $profile->status_count = $statusCount; + $profile->save(); + AccountService::del($id); + } + AccountStatService::removeFromPostCount($id); + } + return; + } +} diff --git a/app/Console/Commands/AddUserDomainBlock.php b/app/Console/Commands/AddUserDomainBlock.php new file mode 100644 index 000000000..6d5c192bf --- /dev/null +++ b/app/Console/Commands/AddUserDomainBlock.php @@ -0,0 +1,106 @@ +validateDomain($domain); + if(!$domain || empty($domain)) { + $this->error('Invalid domain'); + return; + } + $this->processBlocks($domain); + return; + } + + protected function validateDomain($domain) + { + if(!strpos($domain, '.')) { + return; + } + + if(str_starts_with($domain, 'https://')) { + $domain = str_replace('https://', '', $domain); + } + + if(str_starts_with($domain, 'http://')) { + $domain = str_replace('http://', '', $domain); + } + + $domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST)); + + $valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE); + if(!$valid) { + return; + } + + if($domain === config('pixelfed.domain.app')) { + $this->error('Invalid domain'); + return; + } + + $confirmed = confirm('Are you sure you want to block ' . $domain . '?'); + if(!$confirmed) { + return; + } + + return $domain; + } + + protected function processBlocks($domain) + { + DefaultDomainBlock::updateOrCreate([ + 'domain' => $domain + ]); + progress( + label: 'Updating user domain blocks...', + steps: User::lazyById(500), + callback: fn ($user) => $this->performTask($user, $domain), + ); + } + + protected function performTask($user, $domain) + { + if(!$user->profile_id || $user->delete_after) { + return; + } + + if($user->status != null && $user->status != 'disabled') { + return; + } + + UserDomainBlock::updateOrCreate([ + 'profile_id' => $user->profile_id, + 'domain' => $domain + ]); + } +} diff --git a/app/Console/Commands/AvatarStorage.php b/app/Console/Commands/AvatarStorage.php index 054802f42..a6bb70e3d 100644 --- a/app/Console/Commands/AvatarStorage.php +++ b/app/Console/Commands/AvatarStorage.php @@ -82,7 +82,7 @@ class AvatarStorage extends Command $this->line(' '); - if(config_cache('pixelfed.cloud_storage')) { + if((bool) config_cache('pixelfed.cloud_storage')) { $this->info('✅ - Cloud storage configured'); $this->line(' '); } @@ -92,7 +92,7 @@ class AvatarStorage extends Command $this->line(' '); } - if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) { + if((bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) { $disk = Storage::disk(config_cache('filesystems.cloud')); $exists = $disk->exists('cache/avatars/default.jpg'); $state = $exists ? '✅' : '❌'; @@ -100,7 +100,7 @@ class AvatarStorage extends Command $this->info($msg); } - $options = config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ? + $options = (bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ? [ 'Cancel', 'Upload default avatar to cloud', @@ -164,7 +164,7 @@ class AvatarStorage extends Command protected function uploadAvatarsToCloud() { - if(!config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) { + if(!(bool) config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) { $this->error('Enable cloud storage and avatar cloud storage to perform this action'); return; } @@ -213,7 +213,7 @@ class AvatarStorage extends Command return; } - if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) { + if((bool) config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) { $this->error('You have cloud storage disabled and local avatar storage disabled, we cannot refetch avatars.'); return; } diff --git a/app/Console/Commands/AvatarStorageDeepClean.php b/app/Console/Commands/AvatarStorageDeepClean.php index 5840142f5..6f773bd42 100644 --- a/app/Console/Commands/AvatarStorageDeepClean.php +++ b/app/Console/Commands/AvatarStorageDeepClean.php @@ -44,7 +44,7 @@ class AvatarStorageDeepClean extends Command $this->line(' '); $storage = [ - 'cloud' => boolval(config_cache('pixelfed.cloud_storage')), + 'cloud' => (bool) config_cache('pixelfed.cloud_storage'), 'local' => boolval(config_cache('federation.avatars.store_local')) ]; diff --git a/app/Console/Commands/CaptchaToggleCommand.php b/app/Console/Commands/CaptchaToggleCommand.php new file mode 100644 index 000000000..e4f43f528 --- /dev/null +++ b/app/Console/Commands/CaptchaToggleCommand.php @@ -0,0 +1,52 @@ +error('Cloud storage not enabled. Exiting...'); return; } + if(!$this->confirm('Are you sure you want to proceed?')) { + return; + } + $limit = $this->option('limit'); $hugeMode = $this->option('huge'); diff --git a/app/Console/Commands/DeleteRemoteProfile.php b/app/Console/Commands/DeleteRemoteProfile.php new file mode 100644 index 000000000..e5fb741a1 --- /dev/null +++ b/app/Console/Commands/DeleteRemoteProfile.php @@ -0,0 +1,51 @@ + strlen($value) > 2 + ? Profile::whereNotNull('domain')->where('username', 'like', $value.'%')->pluck('username', 'id')->all() + : [] + ); + $profile = Profile::whereNotNull('domain')->find($id); + + if (! $profile) { + $this->error('Could not find profile.'); + exit; + } + + $confirmed = confirm('Are you sure you want to delete '.$profile->username.'\'s account? This action cannot be reversed.'); + DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('adelete'); + $this->info('Dispatched delete job, it may take a few minutes...'); + exit; + } +} diff --git a/app/Console/Commands/DeleteUserDomainBlock.php b/app/Console/Commands/DeleteUserDomainBlock.php new file mode 100644 index 000000000..405b6fe76 --- /dev/null +++ b/app/Console/Commands/DeleteUserDomainBlock.php @@ -0,0 +1,96 @@ +validateDomain($domain); + if(!$domain || empty($domain)) { + $this->error('Invalid domain'); + return; + } + $this->processUnblocks($domain); + return; + } + + protected function validateDomain($domain) + { + if(!strpos($domain, '.')) { + return; + } + + if(str_starts_with($domain, 'https://')) { + $domain = str_replace('https://', '', $domain); + } + + if(str_starts_with($domain, 'http://')) { + $domain = str_replace('http://', '', $domain); + } + + $domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST)); + + $valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE); + if(!$valid) { + return; + } + + if($domain === config('pixelfed.domain.app')) { + return; + } + + $confirmed = confirm('Are you sure you want to unblock ' . $domain . '?'); + if(!$confirmed) { + return; + } + + return $domain; + } + + protected function processUnblocks($domain) + { + DefaultDomainBlock::whereDomain($domain)->delete(); + if(!UserDomainBlock::whereDomain($domain)->count()) { + $this->info('No results found!'); + return; + } + progress( + label: 'Updating user domain blocks...', + steps: UserDomainBlock::whereDomain($domain)->lazyById(500), + callback: fn ($domainBlock) => $this->performTask($domainBlock), + ); + } + + protected function performTask($domainBlock) + { + $domainBlock->deleteQuietly(); + } +} diff --git a/app/Console/Commands/FetchMissingMediaMimeType.php b/app/Console/Commands/FetchMissingMediaMimeType.php index 16aeb5f59..27ae23e4c 100644 --- a/app/Console/Commands/FetchMissingMediaMimeType.php +++ b/app/Console/Commands/FetchMissingMediaMimeType.php @@ -2,11 +2,11 @@ namespace App\Console\Commands; -use Illuminate\Console\Command; use App\Media; -use Illuminate\Support\Facades\Http; use App\Services\MediaService; use App\Services\StatusService; +use Illuminate\Console\Command; +use Illuminate\Support\Facades\Http; class FetchMissingMediaMimeType extends Command { @@ -29,20 +29,20 @@ class FetchMissingMediaMimeType extends Command */ public function handle() { - foreach(Media::whereNotNull(['remote_url', 'status_id'])->whereNull('mime')->lazyByIdDesc(50, 'id') as $media) { + foreach (Media::whereNotNull(['remote_url', 'status_id'])->whereNull('mime')->lazyByIdDesc(50, 'id') as $media) { $res = Http::retry(2, 100, throw: false)->head($media->remote_url); - if(!$res->successful()) { + if (! $res->successful()) { continue; } - if(!in_array($res->header('content-type'), explode(',',config('pixelfed.media_types')))) { + if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) { continue; } $media->mime = $res->header('content-type'); - if($res->hasHeader('content-length')) { + if ($res->hasHeader('content-length')) { $media->size = $res->header('content-length'); } @@ -50,7 +50,7 @@ class FetchMissingMediaMimeType extends Command MediaService::del($media->status_id); StatusService::del($media->status_id); - $this->info('mid:'.$media->id . ' (' . $res->header('content-type') . ':' . $res->header('content-length') . ' bytes)'); + $this->info('mid:'.$media->id.' ('.$res->header('content-type').':'.$res->header('content-length').' bytes)'); } } } diff --git a/app/Console/Commands/FixMediaDriver.php b/app/Console/Commands/FixMediaDriver.php index c743d6c64..a20b0574e 100644 --- a/app/Console/Commands/FixMediaDriver.php +++ b/app/Console/Commands/FixMediaDriver.php @@ -37,7 +37,7 @@ class FixMediaDriver extends Command return Command::SUCCESS; } - if(config_cache('pixelfed.cloud_storage') == false) { + if((bool) config_cache('pixelfed.cloud_storage') == false) { $this->error('Cloud storage not enabled, exiting...'); return Command::SUCCESS; } diff --git a/app/Console/Commands/HashtagCachedCountUpdate.php b/app/Console/Commands/HashtagCachedCountUpdate.php new file mode 100644 index 000000000..49f354e2b --- /dev/null +++ b/app/Console/Commands/HashtagCachedCountUpdate.php @@ -0,0 +1,57 @@ +option('limit'); + $tags = Hashtag::whereNull('cached_count')->limit($limit)->get(); + $count = count($tags); + if(!$count) { + return; + } + + $bar = $this->output->createProgressBar($count); + $bar->start(); + + foreach($tags as $tag) { + $count = DB::table('status_hashtags')->whereHashtagId($tag->id)->count(); + if(!$count) { + $tag->cached_count = 0; + $tag->saveQuietly(); + $bar->advance(); + continue; + } + $tag->cached_count = $count; + $tag->saveQuietly(); + $bar->advance(); + } + $bar->finish(); + $this->line(' '); + return; + } +} diff --git a/app/Console/Commands/HashtagRelatedGenerate.php b/app/Console/Commands/HashtagRelatedGenerate.php new file mode 100644 index 000000000..26fdb8b52 --- /dev/null +++ b/app/Console/Commands/HashtagRelatedGenerate.php @@ -0,0 +1,94 @@ + 'Which hashtag should we generate related tags for?', + ]; + } + + /** + * Execute the console command. + */ + public function handle() + { + $tag = $this->argument('tag'); + $hashtag = Hashtag::whereName($tag)->orWhere('slug', $tag)->first(); + if(!$hashtag) { + $this->error('Hashtag not found, aborting...'); + exit; + } + + $exists = HashtagRelated::whereHashtagId($hashtag->id)->exists(); + + if($exists) { + $confirmed = confirm('Found existing related tags, do you want to regenerate them?'); + if(!$confirmed) { + $this->error('Aborting...'); + exit; + } + } + + $this->info('Looking up #' . $tag . '...'); + + $tags = StatusHashtag::whereHashtagId($hashtag->id)->count(); + if(!$tags || $tags < 100) { + $this->error('Not enough posts found to generate related hashtags!'); + exit; + } + + $this->info('Found ' . $tags . ' posts that use that hashtag'); + $related = collect(HashtagRelatedService::fetchRelatedTags($tag)); + + $selected = multiselect( + label: 'Which tags do you want to generate?', + options: $related->pluck('name'), + required: true, + ); + + $filtered = $related->filter(fn($i) => in_array($i['name'], $selected))->all(); + $agg_score = $related->filter(fn($i) => in_array($i['name'], $selected))->sum('related_count'); + + HashtagRelated::updateOrCreate([ + 'hashtag_id' => $hashtag->id, + ], [ + 'related_tags' => array_values($filtered), + 'agg_score' => $agg_score, + 'last_calculated_at' => now() + ]); + + $this->info('Finished!'); + } +} diff --git a/app/Console/Commands/ImportEmojis.php b/app/Console/Commands/ImportEmojis.php new file mode 100644 index 000000000..77a0c29a4 --- /dev/null +++ b/app/Console/Commands/ImportEmojis.php @@ -0,0 +1,118 @@ +argument('path'); + + if (!file_exists($path) || !mime_content_type($path) == 'application/x-tar') { + $this->error('Path does not exist or is not a tarfile'); + return Command::FAILURE; + } + + $imported = 0; + $skipped = 0; + $failed = 0; + + $tar = new \PharData($path); + $tar->decompress(); + + foreach (new \RecursiveIteratorIterator($tar) as $entry) { + $this->line("Processing {$entry->getFilename()}"); + if (!$entry->isFile() || !$this->isImage($entry) || !$this->isEmoji($entry->getPathname())) { + $failed++; + continue; + } + + $filename = pathinfo($entry->getFilename(), PATHINFO_FILENAME); + $extension = pathinfo($entry->getFilename(), PATHINFO_EXTENSION); + + // Skip macOS shadow files + if (str_starts_with($filename, '._')) { + continue; + } + + $shortcode = implode('', [ + $this->option('prefix'), + $filename, + $this->option('suffix'), + ]); + + $customEmoji = CustomEmoji::whereShortcode($shortcode)->first(); + + if ($customEmoji && !$this->option('overwrite')) { + $skipped++; + continue; + } + + $emoji = $customEmoji ?? new CustomEmoji(); + $emoji->shortcode = $shortcode; + $emoji->domain = config('pixelfed.domain.app'); + $emoji->disabled = $this->option('disabled'); + $emoji->save(); + + $fileName = $emoji->id . '.' . $extension; + Storage::putFileAs('public/emoji', $entry->getPathname(), $fileName); + $emoji->media_path = 'emoji/' . $fileName; + $emoji->save(); + $imported++; + Cache::forget('pf:custom_emoji'); + } + + $this->line("Imported: {$imported}"); + $this->line("Skipped: {$skipped}"); + $this->line("Failed: {$failed}"); + + //delete file + unlink(str_replace('.tar.gz', '.tar', $path)); + + return Command::SUCCESS; + } + + private function isImage($file) + { + $image = getimagesize($file->getPathname()); + return $image !== false; + } + + private function isEmoji($filename) + { + $allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp']; + $mimeType = mime_content_type($filename); + + return in_array($mimeType, $allowedMimeTypes); + } +} diff --git a/app/Console/Commands/ImportUploadMediaToCloudStorage.php b/app/Console/Commands/ImportUploadMediaToCloudStorage.php new file mode 100644 index 000000000..bf23794c9 --- /dev/null +++ b/app/Console/Commands/ImportUploadMediaToCloudStorage.php @@ -0,0 +1,54 @@ +error('Aborted. Cloud storage is not enabled for IG imports.'); + return; + } + + $limit = $this->option('limit'); + + $progress = progress(label: 'Migrating import media', steps: $limit); + + $progress->start(); + + $posts = ImportPost::whereUploadedToS3(false)->take($limit)->get(); + + foreach($posts as $post) { + ImportMediaToCloudPipeline::dispatch($post)->onQueue('low'); + $progress->advance(); + } + + $progress->finish(); + } +} diff --git a/app/Console/Commands/InstanceManager.php b/app/Console/Commands/InstanceManager.php new file mode 100644 index 000000000..a495d9617 --- /dev/null +++ b/app/Console/Commands/InstanceManager.php @@ -0,0 +1,298 @@ +recalculateStats(); + break; + + case 'Unlisted Instances': + return $this->viewUnlistedInstances(); + break; + + case 'Banned Instances': + return $this->viewBannedInstances(); + break; + + case 'Unlist Instance': + return $this->unlistInstance(); + break; + + case 'Ban Instance': + return $this->banInstance(); + break; + + case 'Unban Instance': + return $this->unbanInstance(); + break; + + case 'Relist Instance': + return $this->relistInstance(); + break; + } + } + + protected function recalculateStats() + { + $instanceCount = Instance::count(); + $confirmed = confirm('Do you want to recalculate stats for all ' . $instanceCount . ' instances?'); + if(!$confirmed) { + $this->error('Aborting...'); + exit; + } + + $users = progress( + label: 'Updating instance stats...', + steps: Instance::all(), + callback: fn ($instance) => $this->updateInstanceStats($instance), + ); + } + + protected function updateInstanceStats($instance) + { + FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg'); + } + + protected function unlistInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereUnlisted(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to unlist this instance?'); + if(!$confirmed) { + $this->error('Aborting instance unlisting'); + exit; + } + + $instance->unlisted = true; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully unlisted ' . $instance->domain . '!'); + exit; + } + + protected function relistInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereUnlisted(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to re-list this instance?'); + if(!$confirmed) { + $this->error('Aborting instance re-listing'); + exit; + } + + $instance->unlisted = false; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully re-listed ' . $instance->domain . '!'); + exit; + } + + protected function banInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereBanned(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to ban this instance?'); + if(!$confirmed) { + $this->error('Aborting instance ban'); + exit; + } + + $instance->banned = true; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully banned ' . $instance->domain . '!'); + exit; + } + + protected function unbanInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereBanned(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to unban this instance?'); + if(!$confirmed) { + $this->error('Aborting instance unban'); + exit; + } + + $instance->banned = false; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully un-banned ' . $instance->domain . '!'); + exit; + } + + protected function viewBannedInstances() + { + $data = Instance::whereBanned(true) + ->get(['domain', 'user_count', 'status_count']) + ->map(function($d) { + return [ + 'domain' => $d->domain, + 'user_count' => number_format($d->user_count), + 'status_count' => number_format($d->status_count), + ]; + }) + ->toArray(); + table( + ['Domain', 'User Count', 'Status Count'], + $data + ); + } + + protected function viewUnlistedInstances() + { + $data = Instance::whereUnlisted(true) + ->get(['domain', 'user_count', 'status_count', 'banned']) + ->map(function($d) { + return [ + 'domain' => $d->domain, + 'user_count' => number_format($d->user_count), + 'status_count' => number_format($d->status_count), + 'banned' => $d->banned ? '✅' : null + ]; + }) + ->toArray(); + table( + ['Domain', 'User Count', 'Status Count', 'Banned'], + $data + ); + } +} diff --git a/app/Console/Commands/InstanceUpdateTotalLocalPosts.php b/app/Console/Commands/InstanceUpdateTotalLocalPosts.php new file mode 100644 index 000000000..d44236a51 --- /dev/null +++ b/app/Console/Commands/InstanceUpdateTotalLocalPosts.php @@ -0,0 +1,79 @@ +checkForCache(); + if (! $cached) { + $this->initCache(); + + return; + } + $cache = $this->getCached(); + if (! $cache || ! isset($cache['count'])) { + $this->error('Problem fetching cache'); + + return; + } + $this->updateAndCache(); + Cache::forget('api:nodeinfo'); + + } + + protected function checkForCache() + { + return Storage::exists('total_local_posts.json'); + } + + protected function initCache() + { + $count = DB::table('statuses')->whereNull(['url', 'deleted_at'])->count(); + $res = [ + 'count' => $count, + ]; + Storage::put('total_local_posts.json', json_encode($res, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + ConfigCacheService::put('instance.stats.total_local_posts', $res['count']); + } + + protected function getCached() + { + return Storage::json('total_local_posts.json'); + } + + protected function updateAndCache() + { + $count = DB::table('statuses')->whereNull(['url', 'deleted_at'])->count(); + $res = [ + 'count' => $count, + ]; + Storage::put('total_local_posts.json', json_encode($res, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + ConfigCacheService::put('instance.stats.total_local_posts', $res['count']); + + } +} diff --git a/app/Console/Commands/MediaCloudUrlRewrite.php b/app/Console/Commands/MediaCloudUrlRewrite.php new file mode 100644 index 000000000..367c22d1f --- /dev/null +++ b/app/Console/Commands/MediaCloudUrlRewrite.php @@ -0,0 +1,140 @@ + 'The old S3 domain', + 'newDomain' => 'The new S3 domain' + ]; + } + /** + * The console command description. + * + * @var string + */ + protected $description = 'Rewrite S3 media urls from local users'; + + /** + * Execute the console command. + */ + public function handle() + { + $this->preflightCheck(); + $this->bootMessage(); + $this->confirmCloudUrl(); + } + + protected function preflightCheck() + { + if(!(bool) config_cache('pixelfed.cloud_storage')) { + $this->info('Error: Cloud storage is not enabled!'); + $this->error('Aborting...'); + exit; + } + } + + protected function bootMessage() + { + $this->info(' ____ _ ______ __ '); + $this->info(' / __ \(_) _____ / / __/__ ____/ / '); + $this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / '); + $this->info(' / ____/ /> info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ '); + $this->info(' '); + $this->info(' Media Cloud Url Rewrite Tool'); + $this->info(' ==='); + $this->info(' Old S3: ' . trim($this->argument('oldDomain'))); + $this->info(' New S3: ' . trim($this->argument('newDomain'))); + $this->info(' '); + } + + protected function confirmCloudUrl() + { + $disk = Storage::disk(config('filesystems.cloud'))->url('test'); + $domain = parse_url($disk, PHP_URL_HOST); + if(trim($this->argument('newDomain')) !== $domain) { + $this->error('Error: The new S3 domain you entered is not currently configured'); + exit; + } + + if(!$this->confirm('Confirm this is correct')) { + $this->error('Aborting...'); + exit; + } + + $this->updateUrls(); + } + + protected function updateUrls() + { + $this->info('Updating urls...'); + $oldDomain = trim($this->argument('oldDomain')); + $newDomain = trim($this->argument('newDomain')); + $disk = Storage::disk(config('filesystems.cloud')); + $count = Media::whereNotNull('cdn_url')->count(); + $bar = $this->output->createProgressBar($count); + $counter = 0; + $bar->start(); + foreach(Media::whereNotNull('cdn_url')->lazyById(1000, 'id') as $media) { + if(strncmp($media->media_path, 'http', 4) === 0) { + $bar->advance(); + continue; + } + $cdnHost = parse_url($media->cdn_url, PHP_URL_HOST); + if($oldDomain != $cdnHost || $newDomain == $cdnHost) { + $bar->advance(); + continue; + } + + $media->cdn_url = str_replace($oldDomain, $newDomain, $media->cdn_url); + + if($media->thumbnail_url != null) { + $thumbHost = parse_url($media->thumbnail_url, PHP_URL_HOST); + if($thumbHost == $oldDomain) { + $thumbUrl = $disk->url($media->thumbnail_path); + $media->thumbnail_url = $thumbUrl; + } + } + + if($media->optimized_url != null) { + $optiHost = parse_url($media->optimized_url, PHP_URL_HOST); + if($optiHost == $oldDomain) { + $optiUrl = str_replace($oldDomain, $newDomain, $media->optimized_url); + $media->optimized_url = $optiUrl; + } + } + + $media->save(); + $counter++; + $bar->advance(); + } + + $bar->finish(); + + $this->line(' '); + $this->info('Finished! Updated ' . $counter . ' total records!'); + $this->line(' '); + $this->info('Tip: Run `php artisan cache:clear` to purge cached urls'); + } +} diff --git a/app/Console/Commands/MediaS3GarbageCollector.php b/app/Console/Commands/MediaS3GarbageCollector.php index b6cda43c3..e66fdd2a8 100644 --- a/app/Console/Commands/MediaS3GarbageCollector.php +++ b/app/Console/Commands/MediaS3GarbageCollector.php @@ -45,7 +45,7 @@ class MediaS3GarbageCollector extends Command */ public function handle() { - $enabled = in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']); + $enabled = (bool) config_cache('pixelfed.cloud_storage'); if(!$enabled) { $this->error('Cloud storage not enabled. Exiting...'); return; diff --git a/app/Console/Commands/NotificationEpochUpdate.php b/app/Console/Commands/NotificationEpochUpdate.php new file mode 100644 index 000000000..e606b47ad --- /dev/null +++ b/app/Console/Commands/NotificationEpochUpdate.php @@ -0,0 +1,31 @@ +info('Checking Push Notification support...'); + $this->line(' '); + + $currentState = NotificationAppGatewayService::enabled(); + + if ($currentState) { + $this->info('Push Notification support is active!'); + + return; + } else { + $this->error('Push notification support is NOT active'); + + $action = select( + label: 'Do you want to force re-check?', + options: ['Yes', 'No'], + required: true + ); + + if ($action === 'Yes') { + $recheck = NotificationAppGatewayService::forceSupportRecheck(); + if ($recheck) { + $this->info('Success! Push Notifications are now active!'); + PushNotificationService::warmList('like'); + + return; + } else { + $this->error('Error, please ensure you have a valid API key.'); + $this->line(' '); + $this->line('For more info, visit https://docs.pixelfed.org/running-pixelfed/push-notifications.html'); + $this->line(' '); + + return; + } + + return; + } else { + exit; + } + + return; + } + } +} diff --git a/app/Console/Commands/SoftwareUpdateRefresh.php b/app/Console/Commands/SoftwareUpdateRefresh.php new file mode 100644 index 000000000..c486d58ce --- /dev/null +++ b/app/Console/Commands/SoftwareUpdateRefresh.php @@ -0,0 +1,37 @@ +info('Succesfully updated software versions!'); + } +} diff --git a/app/Console/Commands/TransformImports.php b/app/Console/Commands/TransformImports.php index b88401178..6b6efa6e3 100644 --- a/app/Console/Commands/TransformImports.php +++ b/app/Console/Commands/TransformImports.php @@ -2,17 +2,16 @@ namespace App\Console\Commands; -use Illuminate\Console\Command; -use App\Models\ImportPost; -use App\Services\ImportService; use App\Media; +use App\Models\ImportPost; use App\Profile; -use App\Status; -use Storage; use App\Services\AccountService; +use App\Services\ImportService; use App\Services\MediaPathService; +use App\Status; +use Illuminate\Console\Command; use Illuminate\Support\Str; -use App\Util\Lexer\Autolink; +use Storage; class TransformImports extends Command { @@ -35,23 +34,24 @@ class TransformImports extends Command */ public function handle() { - if(!config('import.instagram.enabled')) { + if (! config('import.instagram.enabled')) { return; } $ips = ImportPost::whereNull('status_id')->where('skip_missing_media', '!=', true)->take(500)->get(); - if(!$ips->count()) { + if (! $ips->count()) { return; } - foreach($ips as $ip) { + foreach ($ips as $ip) { $id = $ip->user_id; $pid = $ip->profile_id; $profile = Profile::find($pid); - if(!$profile) { + if (! $profile) { $ip->skip_missing_media = true; $ip->save(); + continue; } @@ -63,34 +63,43 @@ class TransformImports extends Command ->where('creation_day', $ip->creation_day) ->exists(); - if($exists == true) { + if ($exists == true) { $ip->skip_missing_media = true; $ip->save(); + continue; } $idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day); + if (! $idk) { + $ip->skip_missing_media = true; + $ip->save(); - if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) { + continue; + } + + if (Storage::exists('imports/'.$id.'/'.$ip->filename) === false) { ImportService::clearAttempts($profile->id); ImportService::getPostCount($profile->id, true); $ip->skip_missing_media = true; $ip->save(); + continue; } $missingMedia = false; - foreach($ip->media as $ipm) { + foreach ($ip->media as $ipm) { $fileName = last(explode('/', $ipm['uri'])); - $og = 'imports/' . $id . '/' . $fileName; - if(!Storage::exists($og)) { + $og = 'imports/'.$id.'/'.$fileName; + if (! Storage::exists($og)) { $missingMedia = true; } } - if($missingMedia === true) { + if ($missingMedia === true) { $ip->skip_missing_media = true; $ip->save(); + continue; } @@ -98,7 +107,6 @@ class TransformImports extends Command $status = new Status; $status->profile_id = $pid; $status->caption = $caption; - $status->rendered = strlen(trim($caption)) ? Autolink::create()->autolink($ip->caption) : null; $status->type = $ip->post_type; $status->scope = 'unlisted'; @@ -107,20 +115,21 @@ class TransformImports extends Command $status->created_at = now()->parse($ip->creation_date); $status->save(); - foreach($ip->media as $ipm) { + foreach ($ip->media as $ipm) { $fileName = last(explode('/', $ipm['uri'])); $ext = last(explode('.', $fileName)); $basePath = MediaPathService::get($profile); - $og = 'imports/' . $id . '/' . $fileName; - if(!Storage::exists($og)) { + $og = 'imports/'.$id.'/'.$fileName; + if (! Storage::exists($og)) { $ip->skip_missing_media = true; $ip->save(); + continue; } $size = Storage::size($og); $mime = Storage::mimeType($og); - $newFile = Str::random(40) . '.' . $ext; - $np = $basePath . '/' . $newFile; + $newFile = Str::random(40).'.'.$ext; + $np = $basePath.'/'.$newFile; Storage::move($og, $np); $media = new Media; $media->profile_id = $pid; diff --git a/app/Console/Commands/UserAccountDelete.php b/app/Console/Commands/UserAccountDelete.php new file mode 100644 index 000000000..5032676c4 --- /dev/null +++ b/app/Console/Commands/UserAccountDelete.php @@ -0,0 +1,123 @@ + strlen($value) > 0 + ? User::withTrashed()->whereStatus('deleted')->where('username', 'like', "%{$value}%")->pluck('username', 'id')->all() + : [], + ); + + $user = User::withTrashed()->find($id); + + table( + ['Username', 'Name', 'Email', 'Created'], + [[$user->username, $user->name, $user->email, $user->created_at]] + ); + + $confirmed = confirm( + label: 'Do you want to federate this account deletion?', + default: false, + yes: 'Proceed', + no: 'Cancel', + hint: 'This action is irreversible' + ); + + if (! $confirmed) { + $this->error('Aborting...'); + exit; + } + + $profile = Profile::withTrashed()->find($user->profile_id); + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($profile, new DeleteActor()); + $activity = $fractal->createData($resource)->toArray(); + + $audience = Instance::whereNotNull(['shared_inbox', 'nodeinfo_last_fetched']) + ->where('nodeinfo_last_fetched', '>', now()->subDays(14)) + ->distinct() + ->pluck('shared_inbox'); + + $payload = json_encode($activity); + + $client = new Client([ + 'timeout' => 5, + ]); + + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + + $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) { + foreach ($audience as $url) { + $headers = HttpSignature::sign($profile, $url, $activity, [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]); + yield function () use ($client, $url, $headers, $payload) { + return $client->postAsync($url, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + ], + ]); + }; + } + }; + + $pool = new Pool($client, $requests($audience), [ + 'concurrency' => 50, + 'fulfilled' => function ($response, $index) { + }, + 'rejected' => function ($reason, $index) { + }, + ]); + + $promise = $pool->promise(); + + $promise->wait(); + } +} diff --git a/app/Console/Commands/UserVerifyEmail.php b/app/Console/Commands/UserVerifyEmail.php index 3b3cac5ef..b0461ca79 100644 --- a/app/Console/Commands/UserVerifyEmail.php +++ b/app/Console/Commands/UserVerifyEmail.php @@ -5,8 +5,9 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Str; use App\User; +use Illuminate\Contracts\Console\PromptsForMissingInput; -class UserVerifyEmail extends Command +class UserVerifyEmail extends Command implements PromptsForMissingInput { /** * The name and signature of the console command. @@ -39,13 +40,19 @@ class UserVerifyEmail extends Command */ public function handle() { - $user = User::whereUsername($this->argument('username'))->first(); + $username = $this->argument('username'); + $user = User::whereUsername($username)->first(); if(!$user) { $this->error('Username not found'); return; } + if($user->email_verified_at) { + $this->error('Email already verified ' . $user->email_verified_at->diffForHumans()); + return; + } + $user->email_verified_at = now(); $user->save(); $this->info('Successfully verified email address for ' . $user->username); diff --git a/app/Console/Commands/WeeklyInstanceScan.php b/app/Console/Commands/WeeklyInstanceScan.php new file mode 100644 index 000000000..a2560c254 --- /dev/null +++ b/app/Console/Commands/WeeklyInstanceScan.php @@ -0,0 +1,47 @@ + $this->updateInstanceStats($instance), + ); + } + + protected function updateInstanceStats($instance) + { + FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg'); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 046924eb6..d5f6962f4 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -19,30 +19,39 @@ class Kernel extends ConsoleKernel /** * Define the application's command schedule. * - * @param \Illuminate\Console\Scheduling\Schedule $schedule * * @return void */ protected function schedule(Schedule $schedule) { - $schedule->command('media:optimize')->hourlyAt(40); - $schedule->command('media:gc')->hourlyAt(5); - $schedule->command('horizon:snapshot')->everyFiveMinutes(); - $schedule->command('story:gc')->everyFiveMinutes(); - $schedule->command('gc:failedjobs')->dailyAt(3); - $schedule->command('gc:passwordreset')->dailyAt('09:41'); - $schedule->command('gc:sessions')->twiceDaily(13, 23); + $schedule->command('media:optimize')->hourlyAt(40)->onOneServer(); + $schedule->command('media:gc')->hourlyAt(5)->onOneServer(); + $schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer(); + $schedule->command('story:gc')->everyFiveMinutes()->onOneServer(); + $schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer(); + $schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer(); + $schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer(); + $schedule->command('app:weekly-instance-scan')->weeklyOn(2, '4:20')->onOneServer(); - if(in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) { + if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('media.delete_local_after_cloud')) { $schedule->command('media:s3gc')->hourlyAt(15); } - if(config('import.instagram.enabled')) { - $schedule->command('app:transform-imports')->everyFourMinutes(); - $schedule->command('app:import-upload-garbage-collection')->hourlyAt(51); - $schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37); - $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32); + if (config('import.instagram.enabled')) { + $schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer(); + $schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer(); + $schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer(); + $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer(); + + if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) { + $schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer(); + } } + + $schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer(); + $schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer(); + $schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer(); + $schedule->command('app:instance-update-total-local-posts')->twiceDailyAt(1, 13, 45)->onOneServer(); } /** diff --git a/app/Contact.php b/app/Contact.php index 4d9bc56e8..2239af7d8 100644 --- a/app/Contact.php +++ b/app/Contact.php @@ -3,16 +3,31 @@ namespace App; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Str; class Contact extends Model { + protected $casts = [ + 'responded_at' => 'datetime', + ]; + public function user() { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class); } public function adminUrl() { - return url('/i/admin/messages/show/' . $this->id); + return url('/i/admin/messages/show/'.$this->id); + } + + public function userResponseUrl() + { + return url('/i/contact-admin-response/'.$this->id); + } + + public function getMessageId() + { + return $this->id.'-'.(string) Str::uuid().'@'.strtolower(config('pixelfed.domain.app', 'example.org')); } } diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 90810008b..7000ace07 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -157,7 +157,7 @@ class AccountController extends Controller $pid = $request->user()->profile_id; $count = UserFilterService::muteCount($pid); - $maxLimit = intval(config('instance.user_filters.max_user_mutes')); + $maxLimit = (int) config_cache('instance.user_filters.max_user_mutes'); abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts'); if($count == 0) { $filterCount = UserFilter::whereUserId($pid)->count(); @@ -260,7 +260,7 @@ class AccountController extends Controller ]); $pid = $request->user()->profile_id; $count = UserFilterService::blockCount($pid); - $maxLimit = intval(config('instance.user_filters.max_user_blocks')); + $maxLimit = (int) config_cache('instance.user_filters.max_user_blocks'); abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts'); if($count == 0) { $filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count(); diff --git a/app/Http/Controllers/Admin/AdminDirectoryController.php b/app/Http/Controllers/Admin/AdminDirectoryController.php index 1e4db7d2d..a5923894b 100644 --- a/app/Http/Controllers/Admin/AdminDirectoryController.php +++ b/app/Http/Controllers/Admin/AdminDirectoryController.php @@ -2,30 +2,20 @@ namespace App\Http\Controllers\Admin; -use DB, Cache; -use App\{ - DiscoverCategory, - DiscoverCategoryHashtag, - Hashtag, - Media, - Profile, - Status, - StatusHashtag, - User -}; +use App\Http\Controllers\PixelfedDirectoryController; use App\Models\ConfigCache; use App\Services\AccountService; use App\Services\ConfigCacheService; use App\Services\StatusService; -use Carbon\Carbon; +use App\Status; +use App\User; +use Cache; use Illuminate\Http\Request; -use Illuminate\Validation\Rule; -use League\ISO3166\ISO3166; -use Illuminate\Support\Str; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Validator; -use Illuminate\Support\Facades\Http; -use App\Http\Controllers\PixelfedDirectoryController; +use Illuminate\Support\Str; +use League\ISO3166\ISO3166; trait AdminDirectoryController { @@ -41,64 +31,67 @@ trait AdminDirectoryController $res['countries'] = collect((new ISO3166)->all())->pluck('name'); $res['admins'] = User::whereIsAdmin(true) ->where('2fa_enabled', true) - ->get()->map(function($user) { - return [ - 'uid' => (string) $user->id, - 'pid' => (string) $user->profile_id, - 'username' => $user->username, - 'created_at' => $user->created_at - ]; - }); + ->get()->map(function ($user) { + return [ + 'uid' => (string) $user->id, + 'pid' => (string) $user->profile_id, + 'username' => $user->username, + 'created_at' => $user->created_at, + ]; + }); $config = ConfigCache::whereK('pixelfed.directory')->first(); - if($config) { + if ($config) { $data = $config->v ? json_decode($config->v, true) : []; $res = array_merge($res, $data); } - if(empty($res['summary'])) { + if (empty($res['summary'])) { $summary = ConfigCache::whereK('app.short_description')->pluck('v'); $res['summary'] = $summary ? $summary[0] : null; } - if(isset($res['banner_image']) && !empty($res['banner_image'])) { + if (isset($res['banner_image']) && ! empty($res['banner_image'])) { $res['banner_image'] = url(Storage::url($res['banner_image'])); } - if(isset($res['favourite_posts'])) { - $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) { + if (isset($res['favourite_posts'])) { + $res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) { return StatusService::get($id); }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); } $res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : []; + $res['curated_onboarding'] = (bool) config_cache('instance.curated_registration.enabled'); $res['open_registration'] = (bool) config_cache('pixelfed.open_registration'); - $res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key')); + $res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && + (file_exists(storage_path('oauth-public.key')) || config_cache('passport.public_key')) && + (file_exists(storage_path('oauth-private.key')) || config_cache('passport.private_key')); $res['activitypub_enabled'] = (bool) config_cache('federation.activitypub.enabled'); $res['feature_config'] = [ 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), 'image_quality' => config_cache('pixelfed.image_quality'), - 'optimize_image' => config_cache('pixelfed.optimize_image'), + 'optimize_image' => (bool) config_cache('pixelfed.optimize_image'), 'max_photo_size' => config_cache('pixelfed.max_photo_size'), 'max_caption_length' => config_cache('pixelfed.max_caption_length'), 'max_altext_length' => config_cache('pixelfed.max_altext_length'), - 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'), + 'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'), 'max_account_size' => config_cache('pixelfed.max_account_size'), 'max_album_length' => config_cache('pixelfed.max_album_length'), - 'account_deletion' => config_cache('pixelfed.account_deletion'), + 'account_deletion' => (bool) config_cache('pixelfed.account_deletion'), ]; - if(config_cache('pixelfed.directory.testimonials')) { - $testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'),true)) - ->map(function($t) { + if (config_cache('pixelfed.directory.testimonials')) { + $testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true)) + ->map(function ($t) { return [ 'profile' => AccountService::get($t['profile_id']), - 'body' => $t['body'] + 'body' => $t['body'], ]; }); $res['testimonials'] = $testimonials; @@ -107,8 +100,8 @@ trait AdminDirectoryController $validator = Validator::make($res['feature_config'], [ 'media_types' => [ 'required', - function ($attribute, $value, $fail) { - if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) { + function ($attribute, $value, $fail) { + if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) { $fail('You must enable image/jpeg and image/png support.'); } }, @@ -119,12 +112,12 @@ trait AdminDirectoryController 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000', 'max_album_length' => 'required|integer|min:4|max:20', 'account_deletion' => 'required|accepted', - 'max_caption_length' => 'required|integer|min:500|max:10000' + 'max_caption_length' => 'required|integer|min:500|max:10000', ]); $res['requirements_validator'] = $validator->errors(); - $res['is_eligible'] = $res['open_registration'] && + $res['is_eligible'] = ($res['open_registration'] || $res['curated_onboarding']) && $res['oauth_enabled'] && $res['activitypub_enabled'] && count($res['requirements_validator']) === 0 && @@ -145,11 +138,11 @@ trait AdminDirectoryController foreach (new \DirectoryIterator($path) as $io) { $name = $io->getFilename(); $skip = ['vendor']; - if($io->isDot() || in_array($name, $skip)) { + if ($io->isDot() || in_array($name, $skip)) { continue; } - if($io->isDir()) { + if ($io->isDir()) { $langs->push(['code' => $name, 'name' => locale_get_display_name($name)]); } } @@ -158,25 +151,26 @@ trait AdminDirectoryController $res['primary_locale'] = config('app.locale'); $submissionState = Http::withoutVerifying() - ->post('https://pixelfed.org/api/v1/directory/check-submission', [ - 'domain' => config('pixelfed.domain.app') - ]); + ->post('https://pixelfed.org/api/v1/directory/check-submission', [ + 'domain' => config('pixelfed.domain.app'), + ]); $res['submission_state'] = $submissionState->json(); + return $res; } protected function validVal($res, $val, $count = false, $minLen = false) { - if(!isset($res[$val])) { + if (! isset($res[$val])) { return false; } - if($count) { + if ($count) { return count($res[$val]) >= $count; } - if($minLen) { + if ($minLen) { return strlen($res[$val]) >= $minLen; } @@ -193,11 +187,11 @@ trait AdminDirectoryController 'favourite_posts' => 'array|max:12', 'favourite_posts.*' => 'distinct', 'privacy_pledge' => 'sometimes', - 'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000' + 'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000', ]); $config = ConfigCache::firstOrNew([ - 'k' => 'pixelfed.directory' + 'k' => 'pixelfed.directory', ]); $res = $config->v ? json_decode($config->v, true) : []; @@ -207,27 +201,28 @@ trait AdminDirectoryController $res['contact_email'] = $request->input('contact_email'); $res['privacy_pledge'] = (bool) $request->input('privacy_pledge'); - if($request->filled('location')) { + if ($request->filled('location')) { $exists = (new ISO3166)->name($request->location); - if($exists) { + if ($exists) { $res['location'] = $request->input('location'); } } - if($request->hasFile('banner_image')) { + if ($request->hasFile('banner_image')) { collect(Storage::files('public/headers')) - ->filter(function($name) { - $protected = [ - 'public/headers/.gitignore', - 'public/headers/default.jpg', - 'public/headers/missing.png' - ]; - return !in_array($name, $protected); - }) - ->each(function($name) { - Storage::delete($name); - }); - $path = $request->file('banner_image')->store('public/headers'); + ->filter(function ($name) { + $protected = [ + 'public/headers/.gitignore', + 'public/headers/default.jpg', + 'public/headers/missing.png', + ]; + + return ! in_array($name, $protected); + }) + ->each(function ($name) { + Storage::delete($name); + }); + $path = $request->file('banner_image')->storePublicly('public/headers'); $res['banner_image'] = $path; ConfigCacheService::put('app.banner_image', url(Storage::url($path))); @@ -239,9 +234,10 @@ trait AdminDirectoryController ConfigCacheService::put('pixelfed.directory', $config->v); $updated = json_decode($config->v, true); - if(isset($updated['banner_image'])) { + if (isset($updated['banner_image'])) { $updated['banner_image'] = url(Storage::url($updated['banner_image'])); } + return $updated; } @@ -249,9 +245,10 @@ trait AdminDirectoryController { $reqs = []; $reqs['feature_config'] = [ - 'open_registration' => config_cache('pixelfed.open_registration'), + 'open_registration' => (bool) config_cache('pixelfed.open_registration'), + 'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'), 'activitypub_enabled' => config_cache('federation.activitypub.enabled'), - 'oauth_enabled' => config_cache('pixelfed.oauth_enabled'), + 'oauth_enabled' => (bool) config_cache('pixelfed.oauth_enabled'), 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), 'image_quality' => config_cache('pixelfed.image_quality'), 'optimize_image' => config_cache('pixelfed.optimize_image'), @@ -265,13 +262,14 @@ trait AdminDirectoryController ]; $validator = Validator::make($reqs['feature_config'], [ - 'open_registration' => 'required|accepted', + 'open_registration' => 'required_unless:curated_onboarding,true', + 'curated_onboarding' => 'required_unless:open_registration,true', 'activitypub_enabled' => 'required|accepted', 'oauth_enabled' => 'required|accepted', 'media_types' => [ 'required', - function ($attribute, $value, $fail) { - if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) { + function ($attribute, $value, $fail) { + if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) { $fail('You must enable image/jpeg and image/png support.'); } }, @@ -282,10 +280,10 @@ trait AdminDirectoryController 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000', 'max_album_length' => 'required|integer|min:4|max:20', 'account_deletion' => 'required|accepted', - 'max_caption_length' => 'required|integer|min:500|max:10000' + 'max_caption_length' => 'required|integer|min:500|max:10000', ]); - if(!$validator->validate()) { + if (! $validator->validate()) { return response()->json($validator->errors(), 422); } @@ -294,6 +292,7 @@ trait AdminDirectoryController $data = (new PixelfedDirectoryController())->buildListing(); $res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data); + return 200; } @@ -301,7 +300,7 @@ trait AdminDirectoryController { $bannerImage = ConfigCache::whereK('app.banner_image')->first(); $directory = ConfigCache::whereK('pixelfed.directory')->first(); - if(!$bannerImage && !$directory || empty($directory->v)) { + if (! $bannerImage && ! $directory || empty($directory->v)) { return; } $directoryArr = json_decode($directory->v, true); @@ -309,12 +308,12 @@ trait AdminDirectoryController $protected = [ 'public/headers/.gitignore', 'public/headers/default.jpg', - 'public/headers/missing.png' + 'public/headers/missing.png', ]; - if(!$path || in_array($path, $protected)) { + if (! $path || in_array($path, $protected)) { return; } - if(Storage::exists($directoryArr['banner_image'])) { + if (Storage::exists($directoryArr['banner_image'])) { Storage::delete($directoryArr['banner_image']); } @@ -325,12 +324,13 @@ trait AdminDirectoryController $bannerImage->save(); Cache::forget('api:v1:instance-data-response-v1'); ConfigCacheService::put('pixelfed.directory', $directory); + return $bannerImage->v; } public function directoryGetPopularPosts(Request $request) { - $ids = Cache::remember('admin:api:popular_posts', 86400, function() { + $ids = Cache::remember('admin:api:popular_posts', 86400, function () { return Status::whereLocal(true) ->whereScope('public') ->whereType('photo') @@ -340,21 +340,21 @@ trait AdminDirectoryController ->pluck('id'); }); - $res = $ids->map(function($id) { + $res = $ids->map(function ($id) { return StatusService::get($id); }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } public function directoryGetAddPostByIdSearch(Request $request) { $this->validate($request, [ - 'q' => 'required|integer' + 'q' => 'required|integer', ]); $id = $request->input('q'); @@ -377,11 +377,12 @@ trait AdminDirectoryController $profile_id = $request->input('profile_id'); $testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail(); $existing = collect(json_decode($testimonials->v, true)) - ->filter(function($t) use($profile_id) { + ->filter(function ($t) use ($profile_id) { return $t['profile_id'] !== $profile_id; }) ->values(); ConfigCacheService::put('pixelfed.directory.testimonials', $existing); + return $existing; } @@ -389,13 +390,13 @@ trait AdminDirectoryController { $this->validate($request, [ 'username' => 'required', - 'body' => 'required|string|min:5|max:500' + 'body' => 'required|string|min:5|max:500', ]); $user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail(); $configCache = ConfigCache::firstOrCreate([ - 'k' => 'pixelfed.directory.testimonials' + 'k' => 'pixelfed.directory.testimonials', ]); $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]); @@ -406,7 +407,7 @@ trait AdminDirectoryController $testimonials->push([ 'profile_id' => (string) $user->profile_id, 'username' => $request->input('username'), - 'body' => $request->input('body') + 'body' => $request->input('body'), ]); $configCache->v = json_encode($testimonials->toArray()); @@ -414,8 +415,9 @@ trait AdminDirectoryController ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v); $res = [ 'profile' => AccountService::get($user->profile_id), - 'body' => $request->input('body') + 'body' => $request->input('body'), ]; + return $res; } @@ -423,7 +425,7 @@ trait AdminDirectoryController { $this->validate($request, [ 'profile_id' => 'required', - 'body' => 'required|string|min:5|max:500' + 'body' => 'required|string|min:5|max:500', ]); $profile_id = $request->input('profile_id'); @@ -431,18 +433,19 @@ trait AdminDirectoryController $user = User::whereProfileId($profile_id)->firstOrFail(); $configCache = ConfigCache::firstOrCreate([ - 'k' => 'pixelfed.directory.testimonials' + 'k' => 'pixelfed.directory.testimonials', ]); $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]); - $updated = $testimonials->map(function($t) use($profile_id, $body) { - if($t['profile_id'] == $profile_id) { + $updated = $testimonials->map(function ($t) use ($profile_id, $body) { + if ($t['profile_id'] == $profile_id) { $t['body'] = $body; } + return $t; }) - ->values(); + ->values(); $configCache->v = json_encode($updated); $configCache->save(); diff --git a/app/Http/Controllers/Admin/AdminGroupsController.php b/app/Http/Controllers/Admin/AdminGroupsController.php new file mode 100644 index 000000000..45a4fd266 --- /dev/null +++ b/app/Http/Controllers/Admin/AdminGroupsController.php @@ -0,0 +1,49 @@ +groupAdminStats(); + + return view('admin.groups.home', compact('stats')); + } + + protected function groupAdminStats() + { + return Cache::remember('admin:groups:stats', 3, function () { + $res = [ + 'total' => Group::count(), + 'local' => Group::whereLocal(true)->count(), + ]; + + $res['remote'] = $res['total'] - $res['local']; + $res['categories'] = GroupCategory::count(); + $res['posts'] = GroupPost::count(); + $res['members'] = GroupMember::count(); + $res['interactions'] = GroupInteraction::count(); + $res['reports'] = GroupReport::count(); + + $res['local_30d'] = Cache::remember('admin:groups:stats:local_30d', 43200, function () { + return Group::whereLocal(true)->where('created_at', '>', now()->subMonth())->count(); + }); + + $res['remote_30d'] = Cache::remember('admin:groups:stats:remote_30d', 43200, function () { + return Group::whereLocal(false)->where('created_at', '>', now()->subMonth())->count(); + }); + + return $res; + }); + } +} diff --git a/app/Http/Controllers/Admin/AdminReportController.php b/app/Http/Controllers/Admin/AdminReportController.php index ac238f28c..bdd1c33a4 100644 --- a/app/Http/Controllers/Admin/AdminReportController.php +++ b/app/Http/Controllers/Admin/AdminReportController.php @@ -2,461 +2,471 @@ namespace App\Http\Controllers\Admin; +use App\AccountInterstitial; +use App\Http\Resources\Admin\AdminModeratedProfileResource; +use App\Http\Resources\AdminRemoteReport; +use App\Http\Resources\AdminReport; +use App\Http\Resources\AdminSpamReport; +use App\Jobs\DeletePipeline\DeleteAccountPipeline; +use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; +use App\Jobs\StatusPipeline\RemoteStatusDelete; +use App\Jobs\StatusPipeline\StatusDelete; +use App\Jobs\StoryPipeline\StoryDelete; +use App\Models\ModeratedProfile; +use App\Models\RemoteReport; +use App\Notification; +use App\Profile; +use App\Report; +use App\Services\AccountService; +use App\Services\ModLogService; +use App\Services\NetworkTimelineService; +use App\Services\NotificationService; +use App\Services\PublicTimelineService; +use App\Services\StatusService; +use App\Status; +use App\Story; +use App\User; +use App\Util\ActivityPub\Helpers; use Cache; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redis; -use App\Services\AccountService; -use App\Services\StatusService; -use App\{ - AccountInterstitial, - Contact, - Hashtag, - Newsroom, - Notification, - OauthClient, - Profile, - Report, - Status, - Story, - User -}; -use Illuminate\Validation\Rule; -use App\Services\StoryService; -use App\Services\ModLogService; -use App\Jobs\DeletePipeline\DeleteAccountPipeline; -use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; -use App\Jobs\StatusPipeline\StatusDelete; -use App\Jobs\StatusPipeline\RemoteStatusDelete; -use App\Http\Resources\AdminReport; -use App\Http\Resources\AdminSpamReport; -use App\Services\NotificationService; -use App\Services\PublicTimelineService; -use App\Services\NetworkTimelineService; +use Storage; trait AdminReportController { - public function reports(Request $request) - { - $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; - $page = $request->input('page') ?? 1; + public function reports(Request $request) + { + $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; + $page = $request->input('page') ?? 1; - $ai = Cache::remember('admin-dash:reports:ai-count', 3600, function() { - return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(); - }); + $ai = Cache::remember('admin-dash:reports:ai-count', 3600, function () { + return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(); + }); - $spam = Cache::remember('admin-dash:reports:spam-count', 3600, function() { - return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count(); - }); + $spam = Cache::remember('admin-dash:reports:spam-count', 3600, function () { + return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count(); + }); - $mailVerifications = Redis::scard('email:manual'); + $mailVerifications = Redis::scard('email:manual'); - if($filter == 'open' && $page == 1) { - $reports = Cache::remember('admin-dash:reports:list-cache', 300, function() use($page, $filter) { - return Report::whereHas('status') - ->whereHas('reportedUser') - ->whereHas('reporter') - ->orderBy('created_at','desc') - ->when($filter, function($q, $filter) { - return $filter == 'open' ? - $q->whereNull('admin_seen') : - $q->whereNotNull('admin_seen'); - }) - ->paginate(6); - }); - } else { - $reports = Report::whereHas('status') - ->whereHas('reportedUser') - ->whereHas('reporter') - ->orderBy('created_at','desc') - ->when($filter, function($q, $filter) { - return $filter == 'open' ? - $q->whereNull('admin_seen') : - $q->whereNotNull('admin_seen'); - }) - ->paginate(6); - } + if ($filter == 'open' && $page == 1) { + $reports = Cache::remember('admin-dash:reports:list-cache', 300, function () use ($filter) { + return Report::whereHas('status') + ->whereHas('reportedUser') + ->whereHas('reporter') + ->orderBy('created_at', 'desc') + ->when($filter, function ($q, $filter) { + return $filter == 'open' ? + $q->whereNull('admin_seen') : + $q->whereNotNull('admin_seen'); + }) + ->paginate(6); + }); + } else { + $reports = Report::whereHas('status') + ->whereHas('reportedUser') + ->whereHas('reporter') + ->orderBy('created_at', 'desc') + ->when($filter, function ($q, $filter) { + return $filter == 'open' ? + $q->whereNull('admin_seen') : + $q->whereNotNull('admin_seen'); + }) + ->paginate(6); + } - return view('admin.reports.home', compact('reports', 'ai', 'spam', 'mailVerifications')); - } + return view('admin.reports.home', compact('reports', 'ai', 'spam', 'mailVerifications')); + } - public function showReport(Request $request, $id) - { - $report = Report::with('status')->findOrFail($id); - if($request->has('ref') && $request->input('ref') == 'email') { - return redirect('/i/admin/reports?tab=report&id=' . $report->id); - } - return view('admin.reports.show', compact('report')); - } + public function showReport(Request $request, $id) + { + $report = Report::with('status')->findOrFail($id); + if ($request->has('ref') && $request->input('ref') == 'email') { + return redirect('/i/admin/reports?tab=report&id='.$report->id); + } - public function appeals(Request $request) - { - $appeals = AccountInterstitial::whereNotNull('appeal_requested_at') - ->whereNull('appeal_handled_at') - ->latest() - ->paginate(6); - return view('admin.reports.appeals', compact('appeals')); - } + return view('admin.reports.show', compact('report')); + } - public function showAppeal(Request $request, $id) - { - $appeal = AccountInterstitial::whereNotNull('appeal_requested_at') - ->whereNull('appeal_handled_at') - ->findOrFail($id); - $meta = json_decode($appeal->meta); - return view('admin.reports.show_appeal', compact('appeal', 'meta')); - } + public function appeals(Request $request) + { + $appeals = AccountInterstitial::whereNotNull('appeal_requested_at') + ->whereNull('appeal_handled_at') + ->latest() + ->paginate(6); - public function spam(Request $request) - { - $this->validate($request, [ - 'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions' - ]); + return view('admin.reports.appeals', compact('appeals')); + } - $tab = $request->input('tab', 'home'); + public function showAppeal(Request $request, $id) + { + $appeal = AccountInterstitial::whereNotNull('appeal_requested_at') + ->whereNull('appeal_handled_at') + ->findOrFail($id); + $meta = json_decode($appeal->meta); - $openCount = Cache::remember('admin-dash:reports:spam-count', 3600, function() { - return AccountInterstitial::whereType('post.autospam') - ->whereNull('appeal_handled_at') - ->count(); - }); + return view('admin.reports.show_appeal', compact('appeal', 'meta')); + } - $monthlyCount = Cache::remember('admin-dash:reports:spam-count:30d', 43200, function() { - return AccountInterstitial::whereType('post.autospam') - ->where('created_at', '>', now()->subMonth()) - ->count(); - }); + public function spam(Request $request) + { + $this->validate($request, [ + 'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions', + ]); - $totalCount = Cache::remember('admin-dash:reports:spam-count:total', 43200, function() { - return AccountInterstitial::whereType('post.autospam')->count(); - }); + $tab = $request->input('tab', 'home'); - $uncategorized = Cache::remember('admin-dash:reports:spam-sync', 3600, function() { - return AccountInterstitial::whereType('post.autospam') - ->whereIsSpam(null) - ->whereNotNull('appeal_handled_at') - ->exists(); - }); + $openCount = Cache::remember('admin-dash:reports:spam-count', 3600, function () { + return AccountInterstitial::whereType('post.autospam') + ->whereNull('appeal_handled_at') + ->count(); + }); - $avg = Cache::remember('admin-dash:reports:spam-count:avg', 43200, function() { - if(config('database.default') != 'mysql') { - return 0; - } - return AccountInterstitial::selectRaw('*, count(id) as counter') - ->whereType('post.autospam') - ->groupBy('user_id') - ->get() - ->avg('counter'); - }); + $monthlyCount = Cache::remember('admin-dash:reports:spam-count:30d', 43200, function () { + return AccountInterstitial::whereType('post.autospam') + ->where('created_at', '>', now()->subMonth()) + ->count(); + }); - $avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function() { - if(config('database.default') != 'mysql') { - return "0"; - } - $seconds = AccountInterstitial::selectRaw('DATE(created_at) AS start_date, AVG(TIME_TO_SEC(TIMEDIFF(appeal_handled_at, created_at))) AS timediff')->whereType('post.autospam')->whereNotNull('appeal_handled_at')->where('created_at', '>', now()->subMonth())->get(); - if(!$seconds) { - return "0"; - } - $mins = floor($seconds->avg('timediff') / 60); + $totalCount = Cache::remember('admin-dash:reports:spam-count:total', 43200, function () { + return AccountInterstitial::whereType('post.autospam')->count(); + }); - if($mins < 60) { - return $mins . ' min(s)'; - } + $uncategorized = Cache::remember('admin-dash:reports:spam-sync', 3600, function () { + return AccountInterstitial::whereType('post.autospam') + ->whereIsSpam(null) + ->whereNotNull('appeal_handled_at') + ->exists(); + }); - if($mins < 2880) { - return floor($mins / 60) . ' hour(s)'; - } + $avg = Cache::remember('admin-dash:reports:spam-count:avg', 43200, function () { + if (config('database.default') != 'mysql') { + return 0; + } - return floor($mins / 60 / 24) . ' day(s)'; - }); - $avgCount = $totalCount && $avg ? floor($totalCount / $avg) : "0"; + return AccountInterstitial::selectRaw('*, count(id) as counter') + ->whereType('post.autospam') + ->groupBy('user_id') + ->get() + ->avg('counter'); + }); - if(in_array($tab, ['home', 'spam', 'not-spam'])) { - $appeals = AccountInterstitial::whereType('post.autospam') - ->when($tab, function($q, $tab) { - switch($tab) { - case 'home': - return $q->whereNull('appeal_handled_at'); - break; - case 'spam': - return $q->whereIsSpam(true); - break; - case 'not-spam': - return $q->whereIsSpam(false); - break; - } - }) - ->latest() - ->paginate(6); + $avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function () { + if (config('database.default') != 'mysql') { + return '0'; + } + $seconds = AccountInterstitial::selectRaw('DATE(created_at) AS start_date, AVG(TIME_TO_SEC(TIMEDIFF(appeal_handled_at, created_at))) AS timediff')->whereType('post.autospam')->whereNotNull('appeal_handled_at')->where('created_at', '>', now()->subMonth())->get(); + if (! $seconds) { + return '0'; + } + $mins = floor($seconds->avg('timediff') / 60); - if($tab !== 'home') { - $appeals = $appeals->appends(['tab' => $tab]); - } - } else { - $appeals = new class { - public function count() { - return 0; - } + if ($mins < 60) { + return $mins.' min(s)'; + } - public function render() { - return; - } - }; - } + if ($mins < 2880) { + return floor($mins / 60).' hour(s)'; + } + return floor($mins / 60 / 24).' day(s)'; + }); + $avgCount = $totalCount && $avg ? floor($totalCount / $avg) : '0'; - return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized')); - } + if (in_array($tab, ['home', 'spam', 'not-spam'])) { + $appeals = AccountInterstitial::whereType('post.autospam') + ->when($tab, function ($q, $tab) { + switch ($tab) { + case 'home': + return $q->whereNull('appeal_handled_at'); + break; + case 'spam': + return $q->whereIsSpam(true); + break; + case 'not-spam': + return $q->whereIsSpam(false); + break; + } + }) + ->latest() + ->paginate(6); - public function showSpam(Request $request, $id) - { - $appeal = AccountInterstitial::whereType('post.autospam') - ->findOrFail($id); - if($request->has('ref') && $request->input('ref') == 'email') { - return redirect('/i/admin/reports?tab=autospam&id=' . $appeal->id); - } - $meta = json_decode($appeal->meta); - return view('admin.reports.show_spam', compact('appeal', 'meta')); - } + if ($tab !== 'home') { + $appeals = $appeals->appends(['tab' => $tab]); + } + } else { + $appeals = new class + { + public function count() + { + return 0; + } - public function fixUncategorizedSpam(Request $request) - { - if(Cache::get('admin-dash:reports:spam-sync-active')) { - return redirect('/i/admin/reports/autospam'); - } + public function render() {} + }; + } - Cache::put('admin-dash:reports:spam-sync-active', 1, 900); + return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized')); + } - AccountInterstitial::chunk(500, function($reports) { - foreach($reports as $report) { - if($report->item_type != 'App\Status') { - continue; - } + public function showSpam(Request $request, $id) + { + $appeal = AccountInterstitial::whereType('post.autospam') + ->findOrFail($id); + if ($request->has('ref') && $request->input('ref') == 'email') { + return redirect('/i/admin/reports?tab=autospam&id='.$appeal->id); + } + $meta = json_decode($appeal->meta); - if($report->type != 'post.autospam') { - continue; - } + return view('admin.reports.show_spam', compact('appeal', 'meta')); + } - if($report->is_spam != null) { - continue; - } + public function fixUncategorizedSpam(Request $request) + { + if (Cache::get('admin-dash:reports:spam-sync-active')) { + return redirect('/i/admin/reports/autospam'); + } - $status = StatusService::get($report->item_id, false); - if(!$status) { - return; - } - $scope = $status['visibility']; - $report->is_spam = $scope == 'unlisted'; - $report->in_violation = $report->is_spam; - $report->severity_index = 1; - $report->save(); - } - }); + Cache::put('admin-dash:reports:spam-sync-active', 1, 900); - Cache::forget('admin-dash:reports:spam-sync'); - return redirect('/i/admin/reports/autospam'); - } + AccountInterstitial::chunk(500, function ($reports) { + foreach ($reports as $report) { + if ($report->item_type != 'App\Status') { + continue; + } - public function updateSpam(Request $request, $id) - { - $this->validate($request, [ - 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer' - ]); + if ($report->type != 'post.autospam') { + continue; + } - $action = $request->input('action'); - $appeal = AccountInterstitial::whereType('post.autospam') - ->whereNull('appeal_handled_at') - ->findOrFail($id); + if ($report->is_spam != null) { + continue; + } - $meta = json_decode($appeal->meta); - $res = ['status' => 'success']; - $now = now(); - Cache::forget('admin-dash:reports:spam-count:total'); - Cache::forget('admin-dash:reports:spam-count:30d'); + $status = StatusService::get($report->item_id, false); + if (! $status) { + return; + } + $scope = $status['visibility']; + $report->is_spam = $scope == 'unlisted'; + $report->in_violation = $report->is_spam; + $report->severity_index = 1; + $report->save(); + } + }); - if($action == 'delete-account') { - if(config('pixelfed.account_deletion') == false) { - abort(404); - } + Cache::forget('admin-dash:reports:spam-sync'); - $user = User::findOrFail($appeal->user_id); - $profile = $user->profile; + return redirect('/i/admin/reports/autospam'); + } - if($user->is_admin == true) { - $mid = $request->user()->id; - abort_if($user->id < $mid, 403); - } + public function updateSpam(Request $request, $id) + { + $this->validate($request, [ + 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer', + ]); - $ts = now()->addMonth(); - $user->status = 'delete'; - $profile->status = 'delete'; - $user->delete_after = $ts; - $profile->delete_after = $ts; - $user->save(); - $profile->save(); + $action = $request->input('action'); + $appeal = AccountInterstitial::whereType('post.autospam') + ->whereNull('appeal_handled_at') + ->findOrFail($id); - ModLogService::boot() - ->objectUid($user->id) - ->objectId($user->id) - ->objectType('App\User::class') - ->user($request->user()) - ->action('admin.user.delete') - ->accessLevel('admin') - ->save(); + $meta = json_decode($appeal->meta); + $res = ['status' => 'success']; + $now = now(); + Cache::forget('admin-dash:reports:spam-count:total'); + Cache::forget('admin-dash:reports:spam-count:30d'); - Cache::forget('profiles:private'); - DeleteAccountPipeline::dispatch($user); - return; - } + if ($action == 'delete-account') { + if (config('pixelfed.account_deletion') == false) { + abort(404); + } - if($action == 'dismiss') { - $appeal->is_spam = true; - $appeal->appeal_handled_at = $now; - $appeal->save(); + $user = User::findOrFail($appeal->user_id); + $profile = $user->profile; - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - Cache::forget('admin-dash:reports:spam-count'); - return $res; - } + if ($user->is_admin == true) { + $mid = $request->user()->id; + abort_if($user->id < $mid, 403); + } - if($action == 'dismiss-all') { - AccountInterstitial::whereType('post.autospam') - ->whereItemType('App\Status') - ->whereNull('appeal_handled_at') - ->whereUserId($appeal->user_id) - ->update(['appeal_handled_at' => $now, 'is_spam' => true]); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - Cache::forget('admin-dash:reports:spam-count'); - return $res; - } + $ts = now()->addMonth(); + $user->status = 'delete'; + $profile->status = 'delete'; + $user->delete_after = $ts; + $profile->delete_after = $ts; + $user->save(); + $profile->save(); - if($action == 'approve-all') { - AccountInterstitial::whereType('post.autospam') - ->whereItemType('App\Status') - ->whereNull('appeal_handled_at') - ->whereUserId($appeal->user_id) - ->get() - ->each(function($report) use($meta) { - $report->is_spam = false; - $report->appeal_handled_at = now(); - $report->save(); - $status = Status::find($report->item_id); - if($status) { - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); - StatusService::del($status->id, true); - } - }); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - Cache::forget('admin-dash:reports:spam-count'); - return $res; - } + ModLogService::boot() + ->objectUid($user->id) + ->objectId($user->id) + ->objectType('App\User::class') + ->user($request->user()) + ->action('admin.user.delete') + ->accessLevel('admin') + ->save(); - if($action == 'mark-spammer') { - AccountInterstitial::whereType('post.autospam') - ->whereItemType('App\Status') - ->whereNull('appeal_handled_at') - ->whereUserId($appeal->user_id) - ->update(['appeal_handled_at' => $now, 'is_spam' => true]); + Cache::forget('profiles:private'); + DeleteAccountPipeline::dispatch($user); - $pro = Profile::whereUserId($appeal->user_id)->firstOrFail(); + return; + } - $pro->update([ - 'unlisted' => true, - 'cw' => true, - 'no_autolink' => true - ]); + if ($action == 'dismiss') { + $appeal->is_spam = true; + $appeal->appeal_handled_at = $now; + $appeal->save(); - Status::whereProfileId($pro->id) - ->get() - ->each(function($report) { - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); - StatusService::del($status->id, true); - }); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - Cache::forget('admin-dash:reports:spam-count'); - return $res; - } + return $res; + } - $status = $appeal->status; - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); + if ($action == 'dismiss-all') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereNull('appeal_handled_at') + ->whereUserId($appeal->user_id) + ->update(['appeal_handled_at' => $now, 'is_spam' => true]); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); - $appeal->is_spam = false; - $appeal->appeal_handled_at = now(); - $appeal->save(); + return $res; + } - StatusService::del($status->id); + if ($action == 'approve-all') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereNull('appeal_handled_at') + ->whereUserId($appeal->user_id) + ->get() + ->each(function ($report) use ($meta) { + $report->is_spam = false; + $report->appeal_handled_at = now(); + $report->save(); + $status = Status::find($report->item_id); + if ($status) { + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + StatusService::del($status->id, true); + } + }); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - Cache::forget('admin-dash:reports:spam-count'); + return $res; + } - return $res; - } + if ($action == 'mark-spammer') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereNull('appeal_handled_at') + ->whereUserId($appeal->user_id) + ->update(['appeal_handled_at' => $now, 'is_spam' => true]); - public function updateAppeal(Request $request, $id) - { - $this->validate($request, [ - 'action' => 'required|in:dismiss,approve' - ]); + $pro = Profile::whereUserId($appeal->user_id)->firstOrFail(); - $action = $request->input('action'); - $appeal = AccountInterstitial::whereNotNull('appeal_requested_at') - ->whereNull('appeal_handled_at') - ->findOrFail($id); + $pro->update([ + 'unlisted' => true, + 'cw' => true, + 'no_autolink' => true, + ]); - if($action == 'dismiss') { - $appeal->appeal_handled_at = now(); - $appeal->save(); - Cache::forget('admin-dash:reports:ai-count'); - return redirect('/i/admin/reports/appeals'); - } + Status::whereProfileId($pro->id) + ->get() + ->each(function ($report) { + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + StatusService::del($status->id, true); + }); - switch ($appeal->type) { - case 'post.cw': - $status = $appeal->status; - $status->is_nsfw = false; - $status->save(); - break; + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); - case 'post.unlist': - $status = $appeal->status; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); - break; + return $res; + } - default: - # code... - break; - } + $status = $appeal->status; + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); - $appeal->appeal_handled_at = now(); - $appeal->save(); - StatusService::del($status->id, true); - Cache::forget('admin-dash:reports:ai-count'); + $appeal->is_spam = false; + $appeal->appeal_handled_at = now(); + $appeal->save(); - return redirect('/i/admin/reports/appeals'); - } + StatusService::del($status->id); + + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); + + return $res; + } + + public function updateAppeal(Request $request, $id) + { + $this->validate($request, [ + 'action' => 'required|in:dismiss,approve', + ]); + + $action = $request->input('action'); + $appeal = AccountInterstitial::whereNotNull('appeal_requested_at') + ->whereNull('appeal_handled_at') + ->findOrFail($id); + + if ($action == 'dismiss') { + $appeal->appeal_handled_at = now(); + $appeal->save(); + Cache::forget('admin-dash:reports:ai-count'); + + return redirect('/i/admin/reports/appeals'); + } + + switch ($appeal->type) { + case 'post.cw': + $status = $appeal->status; + $status->is_nsfw = false; + $status->save(); + break; + + case 'post.unlist': + $status = $appeal->status; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + break; + + default: + // code... + break; + } + + $appeal->appeal_handled_at = now(); + $appeal->save(); + StatusService::del($status->id, true); + Cache::forget('admin-dash:reports:ai-count'); + + return redirect('/i/admin/reports/appeals'); + } public function updateReport(Request $request, $id) { $this->validate($request, [ - 'action' => 'required|string', + 'action' => 'required|string', ]); $action = $request->input('action'); @@ -470,7 +480,7 @@ trait AdminReportController 'ban', ]; - if (!in_array($action, $actions)) { + if (! in_array($action, $actions)) { return abort(403); } @@ -479,7 +489,7 @@ trait AdminReportController $this->handleReportAction($report, $action); Cache::forget('admin-dash:reports:list-cache'); - return response()->json(['msg'=> 'Success']); + return response()->json(['msg' => 'Success']); } public function handleReportAction(Report $report, $action) @@ -541,7 +551,7 @@ trait AdminReportController '3' => 'unlist', '4' => 'delete', '5' => 'shadowban', - '6' => 'ban' + '6' => 'ban', ]; } @@ -549,675 +559,1195 @@ trait AdminReportController { $this->validate($request, [ 'action' => 'required|integer|min:1|max:10', - 'ids' => 'required|array' + 'ids' => 'required|array', ]); $action = $this->actionMap()[$request->input('action')]; $ids = $request->input('ids'); $reports = Report::whereIn('id', $ids)->whereNull('admin_seen')->get(); - foreach($reports as $report) { + foreach ($reports as $report) { $this->handleReportAction($report, $action); } $res = [ 'message' => 'Success', - 'code' => 200 + 'code' => 200, ]; + return response()->json($res); } public function reportMailVerifications(Request $request) { - $ids = Redis::smembers('email:manual'); - $ignored = Redis::smembers('email:manual-ignored'); - $reports = []; - if($ids) { - $reports = collect($ids) - ->filter(function($id) use($ignored) { - return !in_array($id, $ignored); - }) - ->map(function($id) { - $user = User::whereProfileId($id)->first(); - if(!$user || $user->email_verified_at) { - return []; - } - $account = AccountService::get($id, true); - if(!$account) { - return []; - } - $account['email'] = $user->email; - return $account; - }) - ->filter(function($res) { - return $res && isset($res['id']); - }) - ->values(); - } - return view('admin.reports.mail_verification', compact('reports', 'ignored')); + $ids = Redis::smembers('email:manual'); + $ignored = Redis::smembers('email:manual-ignored'); + $reports = []; + if ($ids) { + $reports = collect($ids) + ->filter(function ($id) use ($ignored) { + return ! in_array($id, $ignored); + }) + ->map(function ($id) { + $user = User::whereProfileId($id)->first(); + if (! $user || $user->email_verified_at) { + return []; + } + $account = AccountService::get($id, true); + if (! $account) { + return []; + } + $account['email'] = $user->email; + + return $account; + }) + ->filter(function ($res) { + return $res && isset($res['id']); + }) + ->values(); + } + + return view('admin.reports.mail_verification', compact('reports', 'ignored')); } public function reportMailVerifyIgnore(Request $request) { - $id = $request->input('id'); - Redis::sadd('email:manual-ignored', $id); - return redirect('/i/admin/reports'); + $id = $request->input('id'); + Redis::sadd('email:manual-ignored', $id); + + return redirect('/i/admin/reports'); } public function reportMailVerifyApprove(Request $request) { - $id = $request->input('id'); - $user = User::whereProfileId($id)->firstOrFail(); - Redis::srem('email:manual', $id); - Redis::srem('email:manual-ignored', $id); - $user->email_verified_at = now(); - $user->save(); - return redirect('/i/admin/reports'); + $id = $request->input('id'); + $user = User::whereProfileId($id)->firstOrFail(); + Redis::srem('email:manual', $id); + Redis::srem('email:manual-ignored', $id); + $user->email_verified_at = now(); + $user->save(); + + return redirect('/i/admin/reports'); } public function reportMailVerifyClearIgnored(Request $request) { - Redis::del('email:manual-ignored'); - return [200]; + Redis::del('email:manual-ignored'); + + return [200]; } public function reportsStats(Request $request) { - $stats = [ - 'total' => Report::count(), - 'open' => Report::whereNull('admin_seen')->count(), - 'closed' => Report::whereNotNull('admin_seen')->count(), - 'autospam' => AccountInterstitial::whereType('post.autospam')->count(), - 'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(), - 'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(), - 'email_verification_requests' => Redis::scard('email:manual') - ]; - return $stats; + $stats = [ + 'total' => Report::count(), + 'open' => Report::whereNull('admin_seen')->count(), + 'closed' => Report::whereNotNull('admin_seen')->count(), + 'autospam' => AccountInterstitial::whereType('post.autospam')->count(), + 'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(), + 'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(), + 'remote_open' => RemoteReport::whereNull('action_taken_at')->count(), + 'email_verification_requests' => Redis::scard('email:manual'), + ]; + + return $stats; } public function reportsApiAll(Request $request) { - $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; + $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; - $reports = AdminReport::collection( - Report::orderBy('id','desc') - ->when($filter, function($q, $filter) { - return $filter == 'open' ? - $q->whereNull('admin_seen') : - $q->whereNotNull('admin_seen'); - }) - ->groupBy(['id', 'object_id', 'object_type', 'profile_id']) - ->cursorPaginate(6) - ->withQueryString() - ); + $reports = AdminReport::collection( + Report::orderBy('id', 'desc') + ->when($filter, function ($q, $filter) { + return $filter == 'open' ? + $q->whereNull('admin_seen') : + $q->whereNotNull('admin_seen'); + }) + ->groupBy(['id', 'object_id', 'object_type', 'profile_id']) + ->cursorPaginate(6) + ->withQueryString() + ); - return $reports; + return $reports; + } + + public function reportsApiRemote(Request $request) + { + $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; + + $reports = AdminRemoteReport::collection( + RemoteReport::orderBy('id', 'desc') + ->when($filter, function ($q, $filter) { + return $filter == 'open' ? + $q->whereNull('action_taken_at') : + $q->whereNotNull('action_taken_at'); + }) + ->cursorPaginate(6) + ->withQueryString() + ); + + return $reports; } public function reportsApiGet(Request $request, $id) { - $report = Report::findOrFail($id); - return new AdminReport($report); + $report = Report::findOrFail($id); + + return new AdminReport($report); } public function reportsApiHandle(Request $request) { - $this->validate($request, [ - 'object_id' => 'required', - 'object_type' => 'required', - 'id' => 'required', - 'action' => 'required|in:ignore,nsfw,unlist,private,delete', - 'action_type' => 'required|in:post,profile' - ]); + $this->validate($request, [ + 'object_id' => 'required', + 'object_type' => 'required', + 'id' => 'required', + 'action' => 'required|in:ignore,nsfw,unlist,private,delete,delete-all', + 'action_type' => 'required|in:post,profile,story', + ]); - $report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id')); + $report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id')); - if($request->input('action_type') === 'profile') { - return $this->reportsHandleProfileAction($report, $request->input('action')); - } else if($request->input('action_type') === 'post') { - return $this->reportsHandleStatusAction($report, $request->input('action')); - } + if ($request->input('action_type') === 'profile') { + return $this->reportsHandleProfileAction($report, $request->input('action')); + } elseif ($request->input('action_type') === 'post') { + return $this->reportsHandleStatusAction($report, $request->input('action')); + } elseif ($request->input('action_type') === 'story') { + return $this->reportsHandleStoryAction($report, $request->input('action')); + } - return $report; + return $report; + } + + protected function reportsHandleStoryAction($report, $action) + { + switch ($action) { + case 'ignore': + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); + + return [200]; + break; + + case 'delete': + $profile = Profile::find($report->reported_profile_id); + $story = Story::whereProfileId($profile->id)->find($report->object_id); + + abort_if(! $story, 400, 'Invalid or missing story'); + + $story->active = false; + $story->save(); + + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($report->object_id) + ->objectType('App\Story::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'delete', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); + StoryDelete::dispatch($story)->onQueue('story'); + + return [200]; + break; + + case 'delete-all': + $profile = Profile::find($report->reported_profile_id); + $stories = Story::whereProfileId($profile->id)->whereActive(true)->get(); + + abort_if(! $stories || ! $stories->count(), 400, 'Invalid or missing stories'); + + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($report->object_id) + ->objectType('App\Story::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'delete-all', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); + + Report::where('reported_profile_id', $profile->id) + ->whereObjectType('App\Story') + ->whereNull('admin_seen') + ->update([ + 'admin_seen' => now(), + ]); + $stories->each(function ($story) { + StoryDelete::dispatch($story)->onQueue('story'); + }); + + return [200]; + break; + } } protected function reportsHandleProfileAction($report, $action) { - switch($action) { - case 'ignore': - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + switch ($action) { + case 'ignore': + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - case 'nsfw': - if($report->object_type === 'App\Profile') { - $profile = Profile::find($report->object_id); - } else if($report->object_type === 'App\Status') { - $status = Status::find($report->object_id); - if(!$status) { - return [200]; - } - $profile = Profile::find($status->profile_id); - } + return [200]; + break; - if(!$profile) { - return; - } + case 'nsfw': + if ($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } elseif ($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if (! $status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } - abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); + if (! $profile) { + return; + } - $profile->cw = true; - $profile->save(); + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); - foreach(Status::whereProfileId($profile->id)->cursor() as $status) { - $status->is_nsfw = true; - $status->save(); - StatusService::del($status->id); - PublicTimelineService::rem($status->id); - } + $profile->cw = true; + $profile->save(); - ModLogService::boot() - ->objectUid($profile->id) - ->objectId($profile->id) - ->objectType('App\Profile::class') - ->user(request()->user()) - ->action('admin.user.moderate') - ->metadata([ - 'action' => 'cw', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + if ($profile->remote_url) { + ModeratedProfile::updateOrCreate([ + 'profile_url' => $profile->remote_url, + 'profile_id' => $profile->id, + ], [ + 'is_nsfw' => true, + 'domain' => $profile->domain, + ]); + } - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'nsfw' => true, - 'admin_seen' => now() - ]); - return [200]; - break; + foreach (Status::whereProfileId($profile->id)->cursor() as $status) { + $status->is_nsfw = true; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } - case 'unlist': - if($report->object_type === 'App\Profile') { - $profile = Profile::find($report->object_id); - } else if($report->object_type === 'App\Status') { - $status = Status::find($report->object_id); - if(!$status) { - return [200]; - } - $profile = Profile::find($status->profile_id); - } + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'cw', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - if(!$profile) { - return; - } + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'nsfw' => true, + 'admin_seen' => now(), + ]); - abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); + return [200]; + break; - $profile->unlisted = true; - $profile->save(); + case 'unlist': + if ($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } elseif ($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if (! $status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } - foreach(Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) { - $status->scope = 'unlisted'; - $status->visibility = 'unlisted'; - $status->save(); - StatusService::del($status->id); - PublicTimelineService::rem($status->id); - } + if (! $profile) { + return; + } - ModLogService::boot() - ->objectUid($profile->id) - ->objectId($profile->id) - ->objectType('App\Profile::class') - ->user(request()->user()) - ->action('admin.user.moderate') - ->metadata([ - 'action' => 'unlisted', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + $profile->unlisted = true; + $profile->save(); - case 'private': - if($report->object_type === 'App\Profile') { - $profile = Profile::find($report->object_id); - } else if($report->object_type === 'App\Status') { - $status = Status::find($report->object_id); - if(!$status) { - return [200]; - } - $profile = Profile::find($status->profile_id); - } + if ($profile->remote_url) { + ModeratedProfile::updateOrCreate([ + 'profile_url' => $profile->remote_url, + 'profile_id' => $profile->id, + ], [ + 'is_unlisted' => true, + 'domain' => $profile->domain, + ]); + } - if(!$profile) { - return; - } + foreach (Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) { + $status->scope = 'unlisted'; + $status->visibility = 'unlisted'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } - abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'unlisted', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - $profile->unlisted = true; - $profile->save(); + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - foreach(Status::whereProfileId($profile->id)->cursor() as $status) { - $status->scope = 'private'; - $status->visibility = 'private'; - $status->save(); - StatusService::del($status->id); - PublicTimelineService::rem($status->id); - } + return [200]; + break; - ModLogService::boot() - ->objectUid($profile->id) - ->objectId($profile->id) - ->objectType('App\Profile::class') - ->user(request()->user()) - ->action('admin.user.moderate') - ->metadata([ - 'action' => 'private', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + case 'private': + if ($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } elseif ($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if (! $status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + if (! $profile) { + return; + } - case 'delete': - if(config('pixelfed.account_deletion') == false) { - abort(404); - } + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); - if($report->object_type === 'App\Profile') { - $profile = Profile::find($report->object_id); - } else if($report->object_type === 'App\Status') { - $status = Status::find($report->object_id); - if(!$status) { - return [200]; - } - $profile = Profile::find($status->profile_id); - } + $profile->unlisted = true; + $profile->save(); - if(!$profile) { - return; - } + if ($profile->remote_url) { + ModeratedProfile::updateOrCreate([ + 'profile_url' => $profile->remote_url, + 'profile_id' => $profile->id, + ], [ + 'is_unlisted' => true, + 'domain' => $profile->domain, + ]); + } - abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account.'); + foreach (Status::whereProfileId($profile->id)->cursor() as $status) { + $status->scope = 'private'; + $status->visibility = 'private'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } - $ts = now()->addMonth(); + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'private', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - if($profile->user_id) { - $user = $profile->user; - abort_if($user->is_admin, 403, 'You cannot delete admin accounts.'); - $user->status = 'delete'; - $user->delete_after = $ts; - $user->save(); - } + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - $profile->status = 'delete'; - $profile->delete_after = $ts; - $profile->save(); + return [200]; + break; - ModLogService::boot() - ->objectUid($profile->id) - ->objectId($profile->id) - ->objectType('App\Profile::class') - ->user(request()->user()) - ->action('admin.user.delete') - ->accessLevel('admin') - ->save(); + case 'delete': + if (config('pixelfed.account_deletion') == false) { + abort(404); + } - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); + if ($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } elseif ($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if (! $status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } - if($profile->user_id) { - DB::table('oauth_access_tokens')->whereUserId($user->id)->delete(); - DB::table('oauth_auth_codes')->whereUserId($user->id)->delete(); - $user->email = $user->id; - $user->password = ''; - $user->status = 'delete'; - $user->save(); - $profile->status = 'delete'; - $profile->delete_after = now()->addMonth(); - $profile->save(); - AccountService::del($profile->id); - DeleteAccountPipeline::dispatch($user)->onQueue('high'); - } else { - $profile->status = 'delete'; - $profile->delete_after = now()->addMonth(); - $profile->save(); - AccountService::del($profile->id); - DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high'); - } - return [200]; - break; - } + if (! $profile) { + return; + } + + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account.'); + + $ts = now()->addMonth(); + + if ($profile->remote_url) { + ModeratedProfile::updateOrCreate([ + 'profile_url' => $profile->remote_url, + 'profile_id' => $profile->id, + ], [ + 'is_banned' => true, + 'domain' => $profile->domain, + ]); + } + + if ($profile->user_id) { + $user = $profile->user; + abort_if($user->is_admin, 403, 'You cannot delete admin accounts.'); + $user->status = 'delete'; + $user->delete_after = $ts; + $user->save(); + } + + $profile->status = 'delete'; + $profile->delete_after = $ts; + $profile->save(); + + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.delete') + ->accessLevel('admin') + ->save(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); + + if ($profile->user_id) { + DB::table('oauth_access_tokens')->whereUserId($user->id)->delete(); + DB::table('oauth_auth_codes')->whereUserId($user->id)->delete(); + $user->email = $user->id; + $user->password = ''; + $user->status = 'delete'; + $user->save(); + $profile->status = 'delete'; + $profile->delete_after = now()->addMonth(); + $profile->save(); + AccountService::del($profile->id); + DeleteAccountPipeline::dispatch($user)->onQueue('high'); + } else { + $profile->status = 'delete'; + $profile->delete_after = now()->addMonth(); + $profile->save(); + AccountService::del($profile->id); + DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high'); + } + + return [200]; + break; + } } protected function reportsHandleStatusAction($report, $action) { - switch($action) { - case 'ignore': - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + switch ($action) { + case 'ignore': + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - case 'nsfw': - $status = Status::find($report->object_id); + return [200]; + break; - if(!$status) { - return [200]; - } + case 'nsfw': + $status = Status::find($report->object_id); - abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); - $status->is_nsfw = true; - $status->save(); - StatusService::del($status->id); + if (! $status) { + return [200]; + } - ModLogService::boot() - ->objectUid($status->profile_id) - ->objectId($status->profile_id) - ->objectType('App\Status::class') - ->user(request()->user()) - ->action('admin.status.moderate') - ->metadata([ - 'action' => 'cw', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); + $status->is_nsfw = true; + $status->save(); + StatusService::del($status->id); - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'nsfw' => true, - 'admin_seen' => now() - ]); - return [200]; - break; + ModLogService::boot() + ->objectUid($status->profile_id) + ->objectId($status->profile_id) + ->objectType('App\Status::class') + ->user(request()->user()) + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'cw', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - case 'private': - $status = Status::find($report->object_id); + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'nsfw' => true, + 'admin_seen' => now(), + ]); - if(!$status) { - return [200]; - } + return [200]; + break; - abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); + case 'private': + $status = Status::find($report->object_id); - $status->scope = 'private'; - $status->visibility = 'private'; - $status->save(); - StatusService::del($status->id); - PublicTimelineService::rem($status->id); + if (! $status) { + return [200]; + } - ModLogService::boot() - ->objectUid($status->profile_id) - ->objectId($status->profile_id) - ->objectType('App\Status::class') - ->user(request()->user()) - ->action('admin.status.moderate') - ->metadata([ - 'action' => 'private', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + $status->scope = 'private'; + $status->visibility = 'private'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); - case 'unlist': - $status = Status::find($report->object_id); + ModLogService::boot() + ->objectUid($status->profile_id) + ->objectId($status->profile_id) + ->objectType('App\Status::class') + ->user(request()->user()) + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'private', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - if(!$status) { - return [200]; - } + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); + return [200]; + break; - if($status->scope === 'public') { - $status->scope = 'unlisted'; - $status->visibility = 'unlisted'; - $status->save(); - StatusService::del($status->id); - PublicTimelineService::rem($status->id); - } + case 'unlist': + $status = Status::find($report->object_id); - ModLogService::boot() - ->objectUid($status->profile_id) - ->objectId($status->profile_id) - ->objectType('App\Status::class') - ->user(request()->user()) - ->action('admin.status.moderate') - ->metadata([ - 'action' => 'unlist', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + if (! $status) { + return [200]; + } - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); - case 'delete': - $status = Status::find($report->object_id); + if ($status->scope === 'public') { + $status->scope = 'unlisted'; + $status->visibility = 'unlisted'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } - if(!$status) { - return [200]; - } + ModLogService::boot() + ->objectUid($status->profile_id) + ->objectId($status->profile_id) + ->objectType('App\Status::class') + ->user(request()->user()) + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'unlist', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - $profile = $status->profile; + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account post.'); + return [200]; + break; - StatusService::del($status->id); + case 'delete': + $status = Status::find($report->object_id); - if($profile->user_id != null && $profile->domain == null) { - PublicTimelineService::del($status->id); - StatusDelete::dispatch($status)->onQueue('high'); - } else { - NetworkTimelineService::del($status->id); - RemoteStatusDelete::dispatch($status)->onQueue('high'); - } + if (! $status) { + return [200]; + } - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); + $profile = $status->profile; - return [200]; - break; - } + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account post.'); + + StatusService::del($status->id); + + if ($profile->user_id != null && $profile->domain == null) { + PublicTimelineService::del($status->id); + StatusDelete::dispatch($status)->onQueue('high'); + } else { + NetworkTimelineService::del($status->id); + RemoteStatusDelete::dispatch($status)->onQueue('high'); + } + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); + + return [200]; + break; + } } public function reportsApiSpamAll(Request $request) { - $tab = $request->input('tab', 'home'); + $tab = $request->input('tab', 'home'); - $appeals = AdminSpamReport::collection( - AccountInterstitial::orderBy('id', 'desc') - ->whereType('post.autospam') - ->whereNull('appeal_handled_at') - ->cursorPaginate(6) - ->withQueryString() - ); + $appeals = AdminSpamReport::collection( + AccountInterstitial::orderBy('id', 'desc') + ->whereType('post.autospam') + ->whereNull('appeal_handled_at') + ->cursorPaginate(6) + ->withQueryString() + ); - return $appeals; + return $appeals; } public function reportsApiSpamHandle(Request $request) { - $this->validate($request, [ - 'id' => 'required', - 'action' => 'required|in:mark-read,mark-not-spam,mark-all-read,mark-all-not-spam,delete-profile', - ]); + $this->validate($request, [ + 'id' => 'required', + 'action' => 'required|in:mark-read,mark-not-spam,mark-all-read,mark-all-not-spam,delete-profile', + ]); - $action = $request->input('action'); + $action = $request->input('action'); - abort_if( - $action === 'delete-profile' && - !config('pixelfed.account_deletion'), - 404, - "Cannot delete profile, account_deletion is disabled.\n\n Set `ACCOUNT_DELETION=true` in .env and re-cache config." - ); + abort_if( + $action === 'delete-profile' && + ! config('pixelfed.account_deletion'), + 404, + "Cannot delete profile, account_deletion is disabled.\n\n Set `ACCOUNT_DELETION=true` in .env and re-cache config." + ); - $report = AccountInterstitial::with('user') - ->whereType('post.autospam') - ->whereNull('appeal_handled_at') - ->findOrFail($request->input('id')); + $report = AccountInterstitial::with('user') + ->whereType('post.autospam') + ->whereNull('appeal_handled_at') + ->findOrFail($request->input('id')); - $this->reportsHandleSpamAction($report, $action); - Cache::forget('admin-dash:reports:spam-count'); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $report->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $report->user->profile_id); - return [$action, $report]; + $this->reportsHandleSpamAction($report, $action); + Cache::forget('admin-dash:reports:spam-count'); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$report->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$report->user->profile_id); + + return [$action, $report]; } public function reportsHandleSpamAction($appeal, $action) { - $meta = json_decode($appeal->meta); + $meta = json_decode($appeal->meta); - if($action == 'mark-read') { - $appeal->is_spam = true; - $appeal->appeal_handled_at = now(); - $appeal->save(); - PublicTimelineService::del($appeal->item_id); - } + if ($action == 'mark-read') { + $appeal->is_spam = true; + $appeal->appeal_handled_at = now(); + $appeal->save(); + PublicTimelineService::del($appeal->item_id); + } - if($action == 'mark-not-spam') { - $status = $appeal->status; - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); + if ($action == 'mark-not-spam') { + $status = $appeal->status; + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); - $appeal->is_spam = false; - $appeal->appeal_handled_at = now(); - $appeal->save(); + $appeal->is_spam = false; + $appeal->appeal_handled_at = now(); + $appeal->save(); - Notification::whereAction('autospam.warning') - ->whereProfileId($appeal->user->profile_id) - ->get() - ->each(function($n) use($appeal) { - NotificationService::del($appeal->user->profile_id, $n->id); - $n->forceDelete(); - }); + Notification::whereAction('autospam.warning') + ->whereProfileId($appeal->user->profile_id) + ->get() + ->each(function ($n) use ($appeal) { + NotificationService::del($appeal->user->profile_id, $n->id); + $n->forceDelete(); + }); - StatusService::del($status->id); - StatusService::get($status->id); - if($status->in_reply_to_id == null && $status->reblog_of_id == null) { - PublicTimelineService::add($status->id); - } - } + StatusService::del($status->id); + StatusService::get($status->id); + if ($status->in_reply_to_id == null && $status->reblog_of_id == null) { + PublicTimelineService::add($status->id); + } + } - if($action == 'mark-all-read') { - AccountInterstitial::whereType('post.autospam') - ->whereItemType('App\Status') - ->whereNull('appeal_handled_at') - ->whereUserId($appeal->user_id) - ->update([ - 'appeal_handled_at' => now(), - 'is_spam' => true - ]); - } + if ($action == 'mark-all-read') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereNull('appeal_handled_at') + ->whereUserId($appeal->user_id) + ->update([ + 'appeal_handled_at' => now(), + 'is_spam' => true, + ]); + } - if($action == 'mark-all-not-spam') { - AccountInterstitial::whereType('post.autospam') - ->whereItemType('App\Status') - ->whereUserId($appeal->user_id) - ->get() - ->each(function($report) use($meta) { - $report->is_spam = false; - $report->appeal_handled_at = now(); - $report->save(); - $status = Status::find($report->item_id); - if($status) { - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); - StatusService::del($status->id); - } - Notification::whereAction('autospam.warning') - ->whereProfileId($report->user->profile_id) - ->get() - ->each(function($n) use($report) { - NotificationService::del($report->user->profile_id, $n->id); - $n->forceDelete(); - }); - }); - } + if ($action == 'mark-all-not-spam') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereUserId($appeal->user_id) + ->get() + ->each(function ($report) use ($meta) { + $report->is_spam = false; + $report->appeal_handled_at = now(); + $report->save(); + $status = Status::find($report->item_id); + if ($status) { + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + StatusService::del($status->id); + } + Notification::whereAction('autospam.warning') + ->whereProfileId($report->user->profile_id) + ->get() + ->each(function ($n) use ($report) { + NotificationService::del($report->user->profile_id, $n->id); + $n->forceDelete(); + }); + }); + } - if($action == 'delete-profile') { - $user = User::findOrFail($appeal->user_id); - $profile = $user->profile; + if ($action == 'delete-profile') { + $user = User::findOrFail($appeal->user_id); + $profile = $user->profile; - if($user->is_admin == true) { - $mid = request()->user()->id; - abort_if($user->id < $mid, 403, 'You cannot delete an admin account.'); - } + if ($user->is_admin == true) { + $mid = request()->user()->id; + abort_if($user->id < $mid, 403, 'You cannot delete an admin account.'); + } - $ts = now()->addMonth(); - $user->status = 'delete'; - $profile->status = 'delete'; - $user->delete_after = $ts; - $profile->delete_after = $ts; - $user->save(); - $profile->save(); + $ts = now()->addMonth(); + $user->status = 'delete'; + $profile->status = 'delete'; + $user->delete_after = $ts; + $profile->delete_after = $ts; + $user->save(); + $profile->save(); - $appeal->appeal_handled_at = now(); - $appeal->save(); + $appeal->appeal_handled_at = now(); + $appeal->save(); - ModLogService::boot() - ->objectUid($user->id) - ->objectId($user->id) - ->objectType('App\User::class') - ->user(request()->user()) - ->action('admin.user.delete') - ->accessLevel('admin') - ->save(); + ModLogService::boot() + ->objectUid($user->id) + ->objectId($user->id) + ->objectType('App\User::class') + ->user(request()->user()) + ->action('admin.user.delete') + ->accessLevel('admin') + ->save(); - Cache::forget('profiles:private'); - DeleteAccountPipeline::dispatch($user); - } + Cache::forget('profiles:private'); + DeleteAccountPipeline::dispatch($user); + } } public function reportsApiSpamGet(Request $request, $id) { - $report = AccountInterstitial::findOrFail($id); - return new AdminSpamReport($report); + $report = AccountInterstitial::findOrFail($id); + + return new AdminSpamReport($report); + } + + public function reportsApiRemoteHandle(Request $request) + { + $this->validate($request, [ + 'id' => 'required|exists:remote_reports,id', + 'action' => 'required|in:mark-read,cw-posts,unlist-posts,delete-posts,private-posts,mark-all-read-by-domain,mark-all-read-by-username,cw-all-posts,private-all-posts,unlist-all-posts', + ]); + + $report = RemoteReport::findOrFail($request->input('id')); + $user = User::whereProfileId($report->account_id)->first(); + $ogPublicStatuses = []; + $ogUnlistedStatuses = []; + $ogNonCwStatuses = []; + + switch ($request->input('action')) { + case 'mark-read': + $report->action_taken_at = now(); + $report->save(); + break; + case 'mark-all-read-by-domain': + RemoteReport::whereInstanceId($report->instance_id)->update(['action_taken_at' => now()]); + break; + case 'cw-posts': + $statuses = Status::find($report->status_ids); + foreach ($statuses as $status) { + if ($report->account_id != $status->profile_id) { + continue; + } + if (! $status->is_nsfw) { + $ogNonCwStatuses[] = $status->id; + } + $status->is_nsfw = true; + $status->saveQuietly(); + StatusService::del($status->id); + } + $report->action_taken_at = now(); + $report->save(); + break; + case 'cw-all-posts': + foreach (Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) { + if ($status->is_nsfw || $status->reblog_of_id) { + continue; + } + if (! $status->is_nsfw) { + $ogNonCwStatuses[] = $status->id; + } + $status->is_nsfw = true; + $status->saveQuietly(); + StatusService::del($status->id); + } + break; + case 'unlist-posts': + $statuses = Status::find($report->status_ids); + foreach ($statuses as $status) { + if ($report->account_id != $status->profile_id) { + continue; + } + if ($status->scope === 'public') { + $ogPublicStatuses[] = $status->id; + $status->scope = 'unlisted'; + $status->visibility = 'unlisted'; + $status->saveQuietly(); + StatusService::del($status->id); + } + } + $report->action_taken_at = now(); + $report->save(); + break; + case 'unlist-all-posts': + foreach (Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) { + if ($status->visibility !== 'public' || $status->reblog_of_id) { + continue; + } + $ogPublicStatuses[] = $status->id; + $status->visibility = 'unlisted'; + $status->scope = 'unlisted'; + $status->saveQuietly(); + StatusService::del($status->id); + } + break; + case 'private-posts': + $statuses = Status::find($report->status_ids); + foreach ($statuses as $status) { + if ($report->account_id != $status->profile_id) { + continue; + } + if (in_array($status->scope, ['public', 'unlisted', 'private'])) { + if ($status->scope === 'public') { + $ogPublicStatuses[] = $status->id; + } + $status->scope = 'private'; + $status->visibility = 'private'; + $status->saveQuietly(); + StatusService::del($status->id); + } + } + $report->action_taken_at = now(); + $report->save(); + break; + case 'private-all-posts': + foreach (Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) { + if (! in_array($status->visibility, ['public', 'unlisted']) || $status->reblog_of_id) { + continue; + } + if ($status->visibility === 'public') { + $ogPublicStatuses[] = $status->id; + } elseif ($status->visibility === 'unlisted') { + $ogUnlistedStatuses[] = $status->id; + } + $status->visibility = 'private'; + $status->scope = 'private'; + $status->saveQuietly(); + StatusService::del($status->id); + } + break; + case 'delete-posts': + $statuses = Status::find($report->status_ids); + foreach ($statuses as $status) { + if ($report->account_id != $status->profile_id) { + continue; + } + StatusDelete::dispatch($status); + } + $report->action_taken_at = now(); + $report->save(); + break; + case 'mark-all-read-by-username': + RemoteReport::whereNull('action_taken_at')->whereAccountId($report->account_id)->update(['action_taken_at' => now()]); + break; + + default: + abort(404); + break; + } + + if ($ogPublicStatuses && count($ogPublicStatuses)) { + Storage::disk('local')->put('mod-log-cache/'.$report->account_id.'/'.now()->format('Y-m-d').'-og-public-statuses.json', json_encode($ogPublicStatuses, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + + if ($ogNonCwStatuses && count($ogNonCwStatuses)) { + Storage::disk('local')->put('mod-log-cache/'.$report->account_id.'/'.now()->format('Y-m-d').'-og-noncw-statuses.json', json_encode($ogNonCwStatuses, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + + if ($ogUnlistedStatuses && count($ogUnlistedStatuses)) { + Storage::disk('local')->put('mod-log-cache/'.$report->account_id.'/'.now()->format('Y-m-d').'-og-unlisted-statuses.json', json_encode($ogUnlistedStatuses, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + + ModLogService::boot() + ->user(request()->user()) + ->objectUid($user ? $user->id : null) + ->objectId($report->id) + ->objectType('App\Report::class') + ->action('admin.report.moderate') + ->metadata([ + 'action' => $request->input('action'), + 'duration_active' => now()->parse($report->created_at)->diffForHumans(), + ]) + ->accessLevel('admin') + ->save(); + + if ($report->status_ids) { + foreach ($report->status_ids as $sid) { + RemoteReport::whereNull('action_taken_at') + ->whereJsonContains('status_ids', [$sid]) + ->update(['action_taken_at' => now()]); + } + } + + return [200]; + } + + public function getModeratedProfiles(Request $request) + { + $this->validate($request, [ + 'search' => 'sometimes|string|min:3|max:120', + ]); + + if ($request->filled('search')) { + $query = '%'.$request->input('search').'%'; + $profiles = DB::table('moderated_profiles') + ->join('profiles', 'moderated_profiles.profile_id', '=', 'profiles.id') + ->where('profiles.username', 'LIKE', $query) + ->select('moderated_profiles.*', 'profiles.username') + ->orderByDesc('moderated_profiles.id') + ->cursorPaginate(10); + + return AdminModeratedProfileResource::collection($profiles); + } + $profiles = ModeratedProfile::orderByDesc('id')->cursorPaginate(10); + + return AdminModeratedProfileResource::collection($profiles); + } + + public function getModeratedProfile(Request $request) + { + $this->validate($request, [ + 'id' => 'required', + ]); + + $profile = ModeratedProfile::findOrFail($request->input('id')); + + return new AdminModeratedProfileResource($profile); + } + + public function exportModeratedProfiles(Request $request) + { + return response()->streamDownload(function () { + $profiles = ModeratedProfile::get(); + $res = AdminModeratedProfileResource::collection($profiles); + echo json_encode([ + '_pixelfed_export' => true, + 'meta' => [ + 'ns' => 'https://pixelfed.org', + 'origin' => config('pixelfed.domain.app'), + 'date' => now()->format('c'), + 'type' => 'moderated-profiles', + 'version' => "1.0" + ], + 'data' => $res + ], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + }, 'data-export.json'); + } + + public function deleteModeratedProfile(Request $request) + { + $this->validate($request, [ + 'id' => 'required', + ]); + + $profile = ModeratedProfile::findOrFail($request->input('id')); + + ModLogService::boot() + ->objectUid($profile->profile_id) + ->objectId($profile->id) + ->objectType('App\Models\ModeratedProfile::class') + ->user(request()->user()) + ->action('admin.moderated-profiles.delete') + ->metadata([ + 'profile_url' => $profile->profile_url, + 'profile_id' => $profile->profile_id, + 'domain' => $profile->domain, + 'note' => $profile->note, + 'is_banned' => $profile->is_banned, + 'is_nsfw' => $profile->is_nsfw, + 'is_unlisted' => $profile->is_unlisted, + 'is_noautolink' => $profile->is_noautolink, + 'is_nodms' => $profile->is_nodms, + 'is_notrending' => $profile->is_notrending, + ]) + ->accessLevel('admin') + ->save(); + + $profile->delete(); + + return ['status' => 200, 'message' => 'Successfully deleted moderated profile!']; + } + + public function updateModeratedProfile(Request $request) + { + $this->validate($request, [ + 'id' => 'required|exists:moderated_profiles', + 'note' => 'sometimes|nullable|string|max:500', + 'is_banned' => 'required|boolean', + 'is_noautolink' => 'required|boolean', + 'is_nodms' => 'required|boolean', + 'is_notrending' => 'required|boolean', + 'is_nsfw' => 'required|boolean', + 'is_unlisted' => 'required|boolean', + ]); + + $fields = [ + 'note', + 'is_banned', + 'is_noautolink', + 'is_nodms', + 'is_notrending', + 'is_nsfw', + 'is_unlisted', + ]; + + $profile = ModeratedProfile::findOrFail($request->input('id')); + $profile->update($request->only($fields)); + + ModLogService::boot() + ->objectUid($profile->profile_id) + ->objectId($profile->id) + ->objectType('App\Models\ModeratedProfile::class') + ->user(request()->user()) + ->action('admin.moderated-profiles.update') + ->metadata($request->only($fields)) + ->accessLevel('admin') + ->save(); + + return [200]; + } + + public function createModeratedProfile(Request $request) + { + $this->validate($request, [ + 'url' => 'required|url|starts_with:https://', + ]); + + $url = $request->input('url'); + $host = parse_url($url, PHP_URL_HOST); + + abort_if($host === config('pixelfed.domain.app'), 400, 'You cannot add local users!'); + + $exists = ModeratedProfile::whereProfileUrl($url)->exists(); + abort_if($exists, 400, 'Moderated profile already exists!'); + + $profile = Profile::whereRemoteUrl($url)->first(); + + if ($profile) { + $rec = ModeratedProfile::updateOrCreate([ + 'profile_id' => $profile->id, + ], [ + 'profile_url' => $profile->remote_url, + 'domain' => $profile->domain, + ]); + + ModLogService::boot() + ->objectUid($rec->profile_id) + ->objectId($rec->id) + ->objectType('App\Models\ModeratedProfile::class') + ->user(request()->user()) + ->action('admin.moderated-profiles.create') + ->metadata([ + 'profile_existed' => true, + ]) + ->accessLevel('admin') + ->save(); + + return $rec; + } + + $remoteSearch = Helpers::profileFetch($url); + + if ($remoteSearch) { + $rec = ModeratedProfile::updateOrCreate([ + 'profile_id' => $remoteSearch->id, + ], [ + 'profile_url' => $remoteSearch->remote_url, + 'domain' => $remoteSearch->domain, + ]); + + ModLogService::boot() + ->objectUid($rec->profile_id) + ->objectId($rec->id) + ->objectType('App\Models\ModeratedProfile::class') + ->user(request()->user()) + ->action('admin.moderated-profiles.create') + ->metadata([ + 'profile_existed' => false, + ]) + ->accessLevel('admin') + ->save(); + + return $rec; + } + abort(400, 'Invalid account'); } } diff --git a/app/Http/Controllers/Admin/AdminSettingsController.php b/app/Http/Controllers/Admin/AdminSettingsController.php index 9d9c3dfb6..17ffd98fc 100644 --- a/app/Http/Controllers/Admin/AdminSettingsController.php +++ b/app/Http/Controllers/Admin/AdminSettingsController.php @@ -2,284 +2,888 @@ namespace App\Http\Controllers\Admin; -use Artisan, Cache, DB; -use Illuminate\Http\Request; -use Carbon\Carbon; -use App\{Comment, Like, Media, Page, Profile, Report, Status, User}; -use App\Models\InstanceActor; -use App\Http\Controllers\Controller; -use App\Util\Lexer\PrettyNumber; use App\Models\ConfigCache; +use App\Models\InstanceActor; +use App\Page; +use App\Profile; use App\Services\AccountService; +use App\Services\AdminSettingsService; use App\Services\ConfigCacheService; +use App\Services\FilesystemService; +use App\User; use App\Util\Site\Config; -use Illuminate\Support\Str; +use Artisan; +use Cache; +use DB; +use Illuminate\Http\Request; trait AdminSettingsController { - public function settings(Request $request) - { - $cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage'); - $cloud_disk = config('filesystems.cloud'); - $cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret')); - $types = explode(',', ConfigCacheService::get('pixelfed.media_types')); - $rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null; - $jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types); - $png = in_array('image/png', $types); - $gif = in_array('image/gif', $types); - $mp4 = in_array('video/mp4', $types); - $webp = in_array('image/webp', $types); + public function settings(Request $request) + { + $cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage'); + $cloud_disk = config('filesystems.cloud'); + $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret')); + $types = explode(',', ConfigCacheService::get('pixelfed.media_types')); + $rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null; + $jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types); + $png = in_array('image/png', $types); + $gif = in_array('image/gif', $types); + $mp4 = in_array('video/mp4', $types); + $webp = in_array('image/webp', $types); - $availableAdmins = User::whereIsAdmin(true)->get(); - $currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null; + $availableAdmins = User::whereIsAdmin(true)->get(); + $currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null; + $openReg = (bool) config_cache('pixelfed.open_registration'); + $curOnboarding = (bool) config_cache('instance.curated_registration.enabled'); + $regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed'); + $accountMigration = (bool) config_cache('federation.migration'); - // $system = [ - // 'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')), - // 'max_upload_size' => ini_get('post_max_size'), - // 'image_driver' => config('image.driver'), - // 'image_driver_loaded' => extension_loaded(config('image.driver')) - // ]; + return view('admin.settings.home', compact( + 'jpeg', + 'png', + 'gif', + 'mp4', + 'webp', + 'rules', + 'cloud_storage', + 'cloud_disk', + 'cloud_ready', + 'availableAdmins', + 'currentAdmin', + 'regState', + 'accountMigration' + )); + } - return view('admin.settings.home', compact( - 'jpeg', - 'png', - 'gif', - 'mp4', - 'webp', - 'rules', - 'cloud_storage', - 'cloud_disk', - 'cloud_ready', - 'availableAdmins', - 'currentAdmin' - // 'system' - )); - } + public function settingsHomeStore(Request $request) + { + $this->validate($request, [ + 'name' => 'nullable|string', + 'short_description' => 'nullable', + 'long_description' => 'nullable', + 'max_photo_size' => 'nullable|integer|min:1', + 'max_album_length' => 'nullable|integer|min:1|max:100', + 'image_quality' => 'nullable|integer|min:1|max:100', + 'type_jpeg' => 'nullable', + 'type_png' => 'nullable', + 'type_gif' => 'nullable', + 'type_mp4' => 'nullable', + 'type_webp' => 'nullable', + 'admin_account_id' => 'nullable', + 'regs' => 'required|in:open,filtered,closed', + 'account_migration' => 'nullable', + 'rule_delete' => 'sometimes', + ]); - public function settingsHomeStore(Request $request) - { - $this->validate($request, [ - 'name' => 'nullable|string', - 'short_description' => 'nullable', - 'long_description' => 'nullable', - 'max_photo_size' => 'nullable|integer|min:1', - 'max_album_length' => 'nullable|integer|min:1|max:100', - 'image_quality' => 'nullable|integer|min:1|max:100', - 'type_jpeg' => 'nullable', - 'type_png' => 'nullable', - 'type_gif' => 'nullable', - 'type_mp4' => 'nullable', - 'type_webp' => 'nullable', - 'admin_account_id' => 'nullable', - ]); + $orb = false; + $cob = false; + switch ($request->input('regs')) { + case 'open': + $orb = true; + $cob = false; + break; - if($request->filled('admin_account_id')) { - ConfigCacheService::put('instance.admin.pid', $request->admin_account_id); - Cache::forget('api:v1:instance-data:contact'); - Cache::forget('api:v1:instance-data-response-v1'); - } - if($request->filled('rule_delete')) { - $index = (int) $request->input('rule_delete'); - $rules = ConfigCacheService::get('app.rules'); - $json = json_decode($rules, true); - if(!$rules || empty($json)) { - return; - } - unset($json[$index]); - $json = json_encode(array_values($json)); - ConfigCacheService::put('app.rules', $json); - Cache::forget('api:v1:instance-data:rules'); - Cache::forget('api:v1:instance-data-response-v1'); - return 200; - } + case 'filtered': + $orb = false; + $cob = true; + break; - $media_types = explode(',', config_cache('pixelfed.media_types')); - $media_types_original = $media_types; + case 'closed': + $orb = false; + $cob = false; + break; + } - $mimes = [ - 'type_jpeg' => 'image/jpeg', - 'type_png' => 'image/png', - 'type_gif' => 'image/gif', - 'type_mp4' => 'video/mp4', - 'type_webp' => 'image/webp', - ]; + ConfigCacheService::put('pixelfed.open_registration', (bool) $orb); + ConfigCacheService::put('instance.curated_registration.enabled', (bool) $cob); - foreach ($mimes as $key => $value) { - if($request->input($key) == 'on') { - if(!in_array($value, $media_types)) { - array_push($media_types, $value); - } - } else { - $media_types = array_diff($media_types, [$value]); - } - } + if ($request->filled('admin_account_id')) { + ConfigCacheService::put('instance.admin.pid', $request->admin_account_id); + Cache::forget('api:v1:instance-data:contact'); + Cache::forget('api:v1:instance-data-response-v1'); + } + if ($request->filled('rule_delete')) { + $index = (int) $request->input('rule_delete'); + $rules = ConfigCacheService::get('app.rules'); + $json = json_decode($rules, true); + if (! $rules || empty($json)) { + return; + } + unset($json[$index]); + $json = json_encode(array_values($json)); + ConfigCacheService::put('app.rules', $json); + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); - if($media_types !== $media_types_original) { - ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types))); - } + return 200; + } - $keys = [ - 'name' => 'app.name', - 'short_description' => 'app.short_description', - 'long_description' => 'app.description', - 'max_photo_size' => 'pixelfed.max_photo_size', - 'max_album_length' => 'pixelfed.max_album_length', - 'image_quality' => 'pixelfed.image_quality', - 'account_limit' => 'pixelfed.max_account_size', - 'custom_css' => 'uikit.custom.css', - 'custom_js' => 'uikit.custom.js', - 'about_title' => 'about.title' - ]; + $media_types = explode(',', config_cache('pixelfed.media_types')); + $media_types_original = $media_types; - foreach ($keys as $key => $value) { - $cc = ConfigCache::whereK($value)->first(); - $val = $request->input($key); - if($cc && $cc->v != $val) { - ConfigCacheService::put($value, $val); - } else if(!empty($val)) { - ConfigCacheService::put($value, $val); - } - } + $mimes = [ + 'type_jpeg' => 'image/jpeg', + 'type_png' => 'image/png', + 'type_gif' => 'image/gif', + 'type_mp4' => 'video/mp4', + 'type_webp' => 'image/webp', + ]; - $bools = [ - 'activitypub' => 'federation.activitypub.enabled', - 'open_registration' => 'pixelfed.open_registration', - 'mobile_apis' => 'pixelfed.oauth_enabled', - 'stories' => 'instance.stories.enabled', - 'ig_import' => 'pixelfed.import.instagram.enabled', - 'spam_detection' => 'pixelfed.bouncer.enabled', - 'require_email_verification' => 'pixelfed.enforce_email_verification', - 'enforce_account_limit' => 'pixelfed.enforce_account_limit', - 'show_custom_css' => 'uikit.show_custom.css', - 'show_custom_js' => 'uikit.show_custom.js', - 'cloud_storage' => 'pixelfed.cloud_storage', - 'account_autofollow' => 'account.autofollow', - 'show_directory' => 'instance.landing.show_directory', - 'show_explore_feed' => 'instance.landing.show_explore', - ]; + foreach ($mimes as $key => $value) { + if ($request->input($key) == 'on') { + if (! in_array($value, $media_types)) { + array_push($media_types, $value); + } + } else { + $media_types = array_diff($media_types, [$value]); + } + } - foreach ($bools as $key => $value) { - $active = $request->input($key) == 'on'; + if ($media_types !== $media_types_original) { + ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types))); + } - if($key == 'activitypub' && $active && !InstanceActor::exists()) { - Artisan::call('instance:actor'); - } + $keys = [ + 'name' => 'app.name', + 'short_description' => 'app.short_description', + 'long_description' => 'app.description', + 'max_photo_size' => 'pixelfed.max_photo_size', + 'max_album_length' => 'pixelfed.max_album_length', + 'image_quality' => 'pixelfed.image_quality', + 'account_limit' => 'pixelfed.max_account_size', + 'custom_css' => 'uikit.custom.css', + 'custom_js' => 'uikit.custom.js', + 'about_title' => 'about.title', + ]; - if( $key == 'mobile_apis' && - $active && - !file_exists(storage_path('oauth-public.key')) && - !file_exists(storage_path('oauth-private.key')) - ) { - Artisan::call('passport:keys'); - Artisan::call('route:cache'); - } + foreach ($keys as $key => $value) { + $cc = ConfigCache::whereK($value)->first(); + $val = $request->input($key); + if ($cc && $cc->v != $val) { + ConfigCacheService::put($value, $val); + } elseif (! empty($val)) { + ConfigCacheService::put($value, $val); + } + } - if(config_cache($value) !== $active) { - ConfigCacheService::put($value, (bool) $active); - } - } + $bools = [ + 'activitypub' => 'federation.activitypub.enabled', + // 'open_registration' => 'pixelfed.open_registration', + 'mobile_apis' => 'pixelfed.oauth_enabled', + 'stories' => 'instance.stories.enabled', + 'ig_import' => 'pixelfed.import.instagram.enabled', + 'spam_detection' => 'pixelfed.bouncer.enabled', + 'require_email_verification' => 'pixelfed.enforce_email_verification', + 'enforce_account_limit' => 'pixelfed.enforce_account_limit', + 'show_custom_css' => 'uikit.show_custom.css', + 'show_custom_js' => 'uikit.show_custom.js', + 'cloud_storage' => 'pixelfed.cloud_storage', + 'account_autofollow' => 'account.autofollow', + 'show_directory' => 'instance.landing.show_directory', + 'show_explore_feed' => 'instance.landing.show_explore', + 'account_migration' => 'federation.migration', + ]; - if($request->filled('new_rule')) { - $rules = ConfigCacheService::get('app.rules'); - $val = $request->input('new_rule'); - if(!$rules) { - ConfigCacheService::put('app.rules', json_encode([$val])); - } else { - $json = json_decode($rules, true); - $json[] = $val; - ConfigCacheService::put('app.rules', json_encode(array_values($json))); - } - Cache::forget('api:v1:instance-data:rules'); - Cache::forget('api:v1:instance-data-response-v1'); - } + foreach ($bools as $key => $value) { + $active = $request->input($key) == 'on'; - if($request->filled('account_autofollow_usernames')) { - $usernames = explode(',', $request->input('account_autofollow_usernames')); - $names = []; + if ($key == 'activitypub' && $active && ! InstanceActor::exists()) { + Artisan::call('instance:actor'); + } - foreach($usernames as $n) { - $p = Profile::whereUsername($n)->first(); - if(!$p) { - continue; - } - array_push($names, $p->username); - } + if ($key == 'mobile_apis' && + $active && + ! file_exists(storage_path('oauth-public.key')) && + ! config_cache('passport.public_key') && + ! file_exists(storage_path('oauth-private.key')) && + ! config_cache('passport.private_key') + ) { + Artisan::call('passport:keys'); + Artisan::call('route:cache'); + } - ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); - } + if (config_cache($value) !== $active) { + ConfigCacheService::put($value, (bool) $active); + } + } - Cache::forget(Config::CACHE_KEY); + if ($request->filled('new_rule')) { + $rules = ConfigCacheService::get('app.rules'); + $val = $request->input('new_rule'); + if (! $rules) { + ConfigCacheService::put('app.rules', json_encode([$val])); + } else { + $json = json_decode($rules, true); + $json[] = $val; + ConfigCacheService::put('app.rules', json_encode(array_values($json))); + } + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + } - return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!'); - } + if ($request->filled('account_autofollow_usernames')) { + $usernames = explode(',', $request->input('account_autofollow_usernames')); + $names = []; - public function settingsBackups(Request $request) - { - $path = storage_path('app/'.config('app.name')); - $files = is_dir($path) ? new \DirectoryIterator($path) : []; - return view('admin.settings.backups', compact('files')); - } + foreach ($usernames as $n) { + $p = Profile::whereUsername($n)->first(); + if (! $p) { + continue; + } + array_push($names, $p->username); + } - public function settingsMaintenance(Request $request) - { - return view('admin.settings.maintenance'); - } + ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); + } - public function settingsStorage(Request $request) - { - $storage = []; - return view('admin.settings.storage', compact('storage')); - } + Cache::forget(Config::CACHE_KEY); - public function settingsFeatures(Request $request) - { - return view('admin.settings.features'); - } + return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!'); + } - public function settingsPages(Request $request) - { - $pages = Page::orderByDesc('updated_at')->paginate(10); - return view('admin.pages.home', compact('pages')); - } + public function settingsBackups(Request $request) + { + $path = storage_path('app/'.config('app.name')); + $files = is_dir($path) ? new \DirectoryIterator($path) : []; - public function settingsPageEdit(Request $request) - { - return view('admin.pages.edit'); - } + return view('admin.settings.backups', compact('files')); + } - public function settingsSystem(Request $request) - { - $sys = [ - 'pixelfed' => config('pixelfed.version'), - 'php' => phpversion(), - 'laravel' => app()->version(), - ]; - switch (config('database.default')) { - case 'pgsql': - $exp = DB::raw('select version();'); - $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); - $sys['database'] = [ - 'name' => 'Postgres', - 'version' => explode(' ', DB::select($expQuery)[0]->version)[1] - ]; - break; + public function settingsMaintenance(Request $request) + { + return view('admin.settings.maintenance'); + } - case 'mysql': - $exp = DB::raw('select version()'); - $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); - $sys['database'] = [ - 'name' => 'MySQL', - 'version' => DB::select($expQuery)[0]->{'version()'} - ]; - break; + public function settingsStorage(Request $request) + { + $storage = []; - default: - $sys['database'] = [ - 'name' => 'Unknown', - 'version' => '?' - ]; - break; - } - return view('admin.settings.system', compact('sys')); - } + return view('admin.settings.storage', compact('storage')); + } + + public function settingsFeatures(Request $request) + { + return view('admin.settings.features'); + } + + public function settingsPages(Request $request) + { + $pages = Page::orderByDesc('updated_at')->paginate(10); + + return view('admin.pages.home', compact('pages')); + } + + public function settingsPageEdit(Request $request) + { + return view('admin.pages.edit'); + } + + public function settingsSystem(Request $request) + { + $sys = [ + 'pixelfed' => config('pixelfed.version'), + 'php' => phpversion(), + 'laravel' => app()->version(), + ]; + switch (config('database.default')) { + case 'pgsql': + $exp = DB::raw('select version();'); + $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); + $sys['database'] = [ + 'name' => 'Postgres', + 'version' => explode(' ', DB::select($expQuery)[0]->version)[1], + ]; + break; + + case 'mysql': + $exp = DB::raw('select version()'); + $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); + $sys['database'] = [ + 'name' => 'MySQL', + 'version' => DB::select($expQuery)[0]->{'version()'}, + ]; + break; + + default: + $sys['database'] = [ + 'name' => 'Unknown', + 'version' => '?', + ]; + break; + } + + return view('admin.settings.system', compact('sys')); + } + + public function settingsApiFetch(Request $request) + { + $cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage'); + $cloud_disk = config('filesystems.cloud'); + $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret')); + $types = explode(',', ConfigCacheService::get('pixelfed.media_types')); + $rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : []; + $jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types); + $png = in_array('image/png', $types); + $gif = in_array('image/gif', $types); + $mp4 = in_array('video/mp4', $types); + $webp = in_array('image/webp', $types); + + $availableAdmins = User::whereIsAdmin(true)->get(); + $currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null; + $openReg = (bool) config_cache('pixelfed.open_registration'); + $curOnboarding = (bool) config_cache('instance.curated_registration.enabled'); + $regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed'); + $accountMigration = (bool) config_cache('federation.migration'); + $autoFollow = config_cache('account.autofollow_usernames'); + if (strlen($autoFollow) > 3) { + $autoFollow = explode(',', $autoFollow); + } + + $res = AdminSettingsService::getAll(); + + return response()->json($res); + } + + public function settingsApiRulesAdd(Request $request) + { + $this->validate($request, [ + 'rule' => 'required|string|min:5|max:1000', + ]); + + $rules = ConfigCacheService::get('app.rules'); + $val = $request->input('rule'); + if (! $rules) { + ConfigCacheService::put('app.rules', json_encode([$val])); + } else { + $json = json_decode($rules, true); + $count = count($json); + if ($count >= 30) { + return response()->json(['message' => 'Max rules limit reached, you can set up to 30 rules at a time.'], 400); + } + $json[] = $val; + ConfigCacheService::put('app.rules', json_encode(array_values($json))); + } + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + + return [$val]; + } + + public function settingsApiRulesDelete(Request $request) + { + $this->validate($request, [ + 'rule' => 'required|string', + ]); + + $rules = ConfigCacheService::get('app.rules'); + $val = $request->input('rule'); + + if (! $rules) { + return []; + } else { + $json = json_decode($rules, true); + $idx = array_search($val, $json); + if ($idx !== false) { + unset($json[$idx]); + $json = array_values($json); + } + ConfigCacheService::put('app.rules', json_encode(array_values($json))); + } + + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + + return response()->json($json); + } + + public function settingsApiRulesDeleteAll(Request $request) + { + $rules = ConfigCacheService::get('app.rules'); + + if (! $rules) { + return []; + } else { + ConfigCacheService::put('app.rules', json_encode([])); + } + + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + + return response()->json([]); + } + + public function settingsApiAutofollowDelete(Request $request) + { + $this->validate($request, [ + 'username' => 'required|string', + ]); + + $username = $request->input('username'); + $names = []; + $existing = config_cache('account.autofollow_usernames'); + if ($existing) { + $names = explode(',', $existing); + } + + if (in_array($username, $names)) { + $key = array_search($username, $names); + if ($key !== false) { + unset($names[$key]); + } + } + ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); + + return response()->json(['accounts' => array_values($names)]); + } + + public function settingsApiAutofollowAdd(Request $request) + { + $this->validate($request, [ + 'username' => 'required|string', + ]); + + $username = $request->input('username'); + $names = []; + $existing = config_cache('account.autofollow_usernames'); + if ($existing) { + $names = explode(',', $existing); + } + + if ($existing && count($names)) { + if (count($names) >= 5) { + return response()->json(['message' => 'You can only add up to 5 accounts to be autofollowed.'], 400); + } + if (in_array(strtolower($username), array_map('strtolower', $names))) { + return response()->json(['message' => 'User already exists, please try again.'], 400); + } + } + + $p = User::whereUsername($username)->whereNull('status')->first(); + if (! $p || in_array($p->username, $names)) { + abort(404); + } + array_push($names, $p->username); + ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); + + return response()->json(['accounts' => array_values($names)]); + } + + public function settingsApiUpdateType(Request $request, $type) + { + abort_unless(in_array($type, [ + 'posts', + 'platform', + 'home', + 'landing', + 'branding', + 'media', + 'users', + 'storage', + ]), 400); + + switch ($type) { + case 'home': + return $this->settingsApiUpdateHomeType($request); + break; + + case 'landing': + return $this->settingsApiUpdateLandingType($request); + break; + + case 'posts': + return $this->settingsApiUpdatePostsType($request); + break; + + case 'platform': + return $this->settingsApiUpdatePlatformType($request); + break; + + case 'branding': + return $this->settingsApiUpdateBrandingType($request); + break; + + case 'media': + return $this->settingsApiUpdateMediaType($request); + break; + + case 'users': + return $this->settingsApiUpdateUsersType($request); + break; + + case 'storage': + return $this->settingsApiUpdateStorageType($request); + break; + + default: + abort(404); + break; + } + } + + public function settingsApiUpdateHomeType($request) + { + $this->validate($request, [ + 'registration_status' => 'required|in:open,filtered,closed', + 'cloud_storage' => 'required', + 'activitypub_enabled' => 'required', + 'authorized_fetch' => 'required', + 'account_migration' => 'required', + 'mobile_apis' => 'required', + 'stories' => 'required', + 'instagram_import' => 'required', + 'autospam_enabled' => 'required', + ]); + + $regStatus = $request->input('registration_status'); + ConfigCacheService::put('pixelfed.open_registration', $regStatus === 'open'); + ConfigCacheService::put('instance.curated_registration.enabled', $regStatus === 'filtered'); + $cloudStorage = $request->boolean('cloud_storage'); + if ($cloudStorage !== (bool) config_cache('pixelfed.cloud_storage')) { + if (! $cloudStorage) { + ConfigCacheService::put('pixelfed.cloud_storage', false); + } else { + $cloud_disk = config('filesystems.cloud'); + $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret')); + if (! $cloud_ready) { + return redirect()->back()->withErrors(['cloud_storage' => 'Must configure cloud storage before enabling!']); + } else { + ConfigCacheService::put('pixelfed.cloud_storage', true); + } + } + } + ConfigCacheService::put('federation.activitypub.authorized_fetch', $request->boolean('authorized_fetch')); + ConfigCacheService::put('federation.activitypub.enabled', $request->boolean('activitypub_enabled')); + ConfigCacheService::put('federation.migration', $request->boolean('account_migration')); + ConfigCacheService::put('pixelfed.oauth_enabled', $request->boolean('mobile_apis')); + ConfigCacheService::put('instance.stories.enabled', $request->boolean('stories')); + ConfigCacheService::put('pixelfed.import.instagram.enabled', $request->boolean('instagram_import')); + ConfigCacheService::put('pixelfed.bouncer.enabled', $request->boolean('autospam_enabled')); + + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Cache::forget('api:v1:instance-data:contact'); + Config::refresh(); + + return $request->all(); + } + + public function settingsApiUpdateLandingType($request) + { + $this->validate($request, [ + 'current_admin' => 'required', + 'show_directory' => 'required', + 'show_explore' => 'required', + ]); + + ConfigCacheService::put('instance.admin.pid', $request->input('current_admin')); + ConfigCacheService::put('instance.landing.show_directory', $request->boolean('show_directory')); + ConfigCacheService::put('instance.landing.show_explore', $request->boolean('show_explore')); + + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Cache::forget('api:v1:instance-data:contact'); + Config::refresh(); + + return $request->all(); + } + + public function settingsApiUpdateMediaType($request) + { + $this->validate($request, [ + 'image_quality' => 'required|integer|min:1|max:100', + 'max_album_length' => 'required|integer|min:1|max:20', + 'max_photo_size' => 'required|integer|min:100|max:50000', + 'media_types' => 'required', + 'optimize_image' => 'required', + 'optimize_video' => 'required', + ]); + + $mediaTypes = $request->input('media_types'); + $mediaArray = explode(',', $mediaTypes); + foreach ($mediaArray as $mediaType) { + if (! in_array($mediaType, ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4'])) { + return redirect()->back()->withErrors(['media_types' => 'Invalid media type']); + } + } + + ConfigCacheService::put('pixelfed.media_types', $request->input('media_types')); + ConfigCacheService::put('pixelfed.image_quality', $request->input('image_quality')); + ConfigCacheService::put('pixelfed.max_album_length', $request->input('max_album_length')); + ConfigCacheService::put('pixelfed.max_photo_size', $request->input('max_photo_size')); + ConfigCacheService::put('pixelfed.optimize_image', $request->boolean('optimize_image')); + ConfigCacheService::put('pixelfed.optimize_video', $request->boolean('optimize_video')); + + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Cache::forget('api:v1:instance-data:contact'); + Config::refresh(); + + return $request->all(); + } + + public function settingsApiUpdateBrandingType($request) + { + $this->validate($request, [ + 'name' => 'required', + 'short_description' => 'required', + 'long_description' => 'required', + ]); + + ConfigCacheService::put('app.name', $request->input('name')); + ConfigCacheService::put('app.short_description', $request->input('short_description')); + ConfigCacheService::put('app.description', $request->input('long_description')); + + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Cache::forget('api:v1:instance-data:contact'); + Config::refresh(); + + return $request->all(); + } + + public function settingsApiUpdatePostsType($request) + { + $this->validate($request, [ + 'max_caption_length' => 'required|integer|min:5|max:10000', + 'max_altext_length' => 'required|integer|min:5|max:40000', + ]); + + ConfigCacheService::put('pixelfed.max_caption_length', $request->input('max_caption_length')); + ConfigCacheService::put('pixelfed.max_altext_length', $request->input('max_altext_length')); + $res = [ + 'max_caption_length' => $request->input('max_caption_length'), + 'max_altext_length' => $request->input('max_altext_length'), + ]; + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + + return $res; + } + + public function settingsApiUpdatePlatformType($request) + { + $this->validate($request, [ + 'allow_app_registration' => 'required', + 'app_registration_rate_limit_attempts' => 'required|integer|min:1', + 'app_registration_rate_limit_decay' => 'required|integer|min:1', + 'app_registration_confirm_rate_limit_attempts' => 'required|integer|min:1', + 'app_registration_confirm_rate_limit_decay' => 'required|integer|min:1', + 'allow_post_embeds' => 'required', + 'allow_profile_embeds' => 'required', + 'captcha_enabled' => 'required', + 'captcha_on_login' => 'required_if_accepted:captcha_enabled', + 'captcha_on_register' => 'required_if_accepted:captcha_enabled', + 'captcha_secret' => 'required_if_accepted:captcha_enabled', + 'captcha_sitekey' => 'required_if_accepted:captcha_enabled', + 'custom_emoji_enabled' => 'required', + ]); + + ConfigCacheService::put('pixelfed.allow_app_registration', $request->boolean('allow_app_registration')); + ConfigCacheService::put('pixelfed.app_registration_rate_limit_attempts', $request->input('app_registration_rate_limit_attempts')); + ConfigCacheService::put('pixelfed.app_registration_rate_limit_decay', $request->input('app_registration_rate_limit_decay')); + ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_attempts', $request->input('app_registration_confirm_rate_limit_attempts')); + ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_decay', $request->input('app_registration_confirm_rate_limit_decay')); + ConfigCacheService::put('instance.embed.post', $request->boolean('allow_post_embeds')); + ConfigCacheService::put('instance.embed.profile', $request->boolean('allow_profile_embeds')); + ConfigCacheService::put('federation.custom_emoji.enabled', $request->boolean('custom_emoji_enabled')); + $captcha = $request->boolean('captcha_enabled'); + if ($captcha) { + $secret = $request->input('captcha_secret'); + $sitekey = $request->input('captcha_sitekey'); + if (config_cache('captcha.secret') != $secret && strpos($secret, '*') === false) { + ConfigCacheService::put('captcha.secret', $secret); + } + if (config_cache('captcha.sitekey') != $sitekey && strpos($sitekey, '*') === false) { + ConfigCacheService::put('captcha.sitekey', $sitekey); + } + ConfigCacheService::put('captcha.active.login', $request->boolean('captcha_on_login')); + ConfigCacheService::put('captcha.active.register', $request->boolean('captcha_on_register')); + ConfigCacheService::put('captcha.triggers.login.enabled', $request->boolean('captcha_on_login')); + ConfigCacheService::put('captcha.enabled', true); + } else { + ConfigCacheService::put('captcha.enabled', false); + } + $res = [ + 'allow_app_registration' => $request->boolean('allow_app_registration'), + 'app_registration_rate_limit_attempts' => $request->input('app_registration_rate_limit_attempts'), + 'app_registration_rate_limit_decay' => $request->input('app_registration_rate_limit_decay'), + 'app_registration_confirm_rate_limit_attempts' => $request->input('app_registration_confirm_rate_limit_attempts'), + 'app_registration_confirm_rate_limit_decay' => $request->input('app_registration_confirm_rate_limit_decay'), + 'allow_post_embeds' => $request->boolean('allow_post_embeds'), + 'allow_profile_embeds' => $request->boolean('allow_profile_embeds'), + 'captcha_enabled' => $request->boolean('captcha_enabled'), + 'captcha_on_login' => $request->boolean('captcha_on_login'), + 'captcha_on_register' => $request->boolean('captcha_on_register'), + 'captcha_secret' => $request->input('captcha_secret'), + 'captcha_sitekey' => $request->input('captcha_sitekey'), + 'custom_emoji_enabled' => $request->boolean('custom_emoji_enabled'), + ]; + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + + return $res; + } + + public function settingsApiUpdateUsersType($request) + { + $this->validate($request, [ + 'require_email_verification' => 'required', + 'enforce_account_limit' => 'required', + 'max_account_size' => 'required|integer|min:50000', + 'admin_autofollow' => 'required', + 'admin_autofollow_accounts' => 'sometimes', + 'max_user_blocks' => 'required|integer|min:0|max:5000', + 'max_user_mutes' => 'required|integer|min:0|max:5000', + 'max_domain_blocks' => 'required|integer|min:0|max:5000', + ]); + + $adminAutofollow = $request->boolean('admin_autofollow'); + $adminAutofollowAccounts = $request->input('admin_autofollow_accounts'); + if ($adminAutofollow) { + if ($request->filled('admin_autofollow_accounts')) { + $names = []; + $existing = config_cache('account.autofollow_usernames'); + if ($existing) { + $names = explode(',', $existing); + foreach (array_map('strtolower', $adminAutofollowAccounts) as $afc) { + if (in_array(strtolower($afc), array_map('strtolower', $names))) { + continue; + } + $names[] = $afc; + } + } else { + $names = $adminAutofollowAccounts; + } + if (! $names || count($names) == 0) { + return response()->json(['message' => 'You need to assign autofollow accounts before you can enable it.'], 400); + } + if (count($names) > 5) { + return response()->json(['message' => 'You can only add up to 5 accounts to be autofollowed.'.json_encode($names)], 400); + } + $autofollows = User::whereIn('username', $names)->whereNull('status')->pluck('username'); + $adminAutofollowAccounts = $autofollows->implode(','); + ConfigCacheService::put('account.autofollow_usernames', $adminAutofollowAccounts); + } else { + return response()->json(['message' => 'You need to assign autofollow accounts before you can enable it.'], 400); + } + } + + ConfigCacheService::put('pixelfed.enforce_email_verification', $request->boolean('require_email_verification')); + ConfigCacheService::put('pixelfed.enforce_account_limit', $request->boolean('enforce_account_limit')); + ConfigCacheService::put('pixelfed.max_account_size', $request->input('max_account_size')); + ConfigCacheService::put('account.autofollow', $request->boolean('admin_autofollow')); + ConfigCacheService::put('instance.user_filters.max_user_blocks', (int) $request->input('max_user_blocks')); + ConfigCacheService::put('instance.user_filters.max_user_mutes', (int) $request->input('max_user_mutes')); + ConfigCacheService::put('instance.user_filters.max_domain_blocks', (int) $request->input('max_domain_blocks')); + $res = [ + 'require_email_verification' => $request->boolean('require_email_verification'), + 'enforce_account_limit' => $request->boolean('enforce_account_limit'), + 'admin_autofollow' => $request->boolean('admin_autofollow'), + 'admin_autofollow_accounts' => $adminAutofollowAccounts, + 'max_user_blocks' => $request->input('max_user_blocks'), + 'max_user_mutes' => $request->input('max_user_mutes'), + 'max_domain_blocks' => $request->input('max_domain_blocks'), + ]; + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + + return $res; + } + + public function settingsApiUpdateStorageType($request) + { + $this->validate($request, [ + 'primary_disk' => 'required|in:local,cloud', + 'update_disk' => 'sometimes', + 'disk_config' => 'required_if_accepted:update_disk', + 'disk_config.driver' => 'required|in:s3,spaces', + 'disk_config.key' => 'required', + 'disk_config.secret' => 'required', + 'disk_config.region' => 'required', + 'disk_config.bucket' => 'required', + 'disk_config.visibility' => 'required', + 'disk_config.endpoint' => 'required', + 'disk_config.url' => 'nullable', + ]); + + ConfigCacheService::put('pixelfed.cloud_storage', $request->input('primary_disk') === 'cloud'); + $res = [ + 'primary_disk' => $request->input('primary_disk'), + ]; + if ($request->has('update_disk')) { + $res['disk_config'] = $request->input('disk_config'); + $changes = []; + $dkey = $request->input('disk_config.driver') === 's3' ? 'filesystems.disks.s3.' : 'filesystems.disks.spaces.'; + $key = $request->input('disk_config.key'); + $ckey = null; + $secret = $request->input('disk_config.secret'); + $csecret = null; + $region = $request->input('disk_config.region'); + $bucket = $request->input('disk_config.bucket'); + $visibility = $request->input('disk_config.visibility'); + $url = $request->input('disk_config.url'); + $endpoint = $request->input('disk_config.endpoint'); + if (strpos($key, '*') === false && $key != config_cache($dkey.'key')) { + array_push($changes, 'key'); + } else { + $ckey = config_cache($dkey.'key'); + } + if (strpos($secret, '*') === false && $secret != config_cache($dkey.'secret')) { + array_push($changes, 'secret'); + } else { + $csecret = config_cache($dkey.'secret'); + } + if ($region != config_cache($dkey.'region')) { + array_push($changes, 'region'); + } + if ($bucket != config_cache($dkey.'bucket')) { + array_push($changes, 'bucket'); + } + if ($visibility != config_cache($dkey.'visibility')) { + array_push($changes, 'visibility'); + } + if ($url != config_cache($dkey.'url')) { + array_push($changes, 'url'); + } + if ($endpoint != config_cache($dkey.'endpoint')) { + array_push($changes, 'endpoint'); + } + + if ($changes && count($changes)) { + $isValid = FilesystemService::getVerifyCredentials( + $ckey ?? $key, + $csecret ?? $secret, + $region, + $bucket, + $endpoint, + ); + if (! $isValid) { + return response()->json(['error' => true, 's3_vce' => true, 'message' => "
The S3/Spaces credentials you provided are invalid, or the bucket does not have the proper permissions.

Please check all fields and try again.

Any cloud storage configuration changes you made have NOT been saved due to invalid credentials."], 400); + } + } + $res['changes'] = json_encode($changes); + } + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + Cache::forget('api:v2:instance-data-response-v2'); + Config::refresh(); + + return $res; + } } diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index e54908a41..b769f3c8e 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -2,562 +2,668 @@ namespace App\Http\Controllers; -use App\{ - AccountInterstitial, - Contact, - Hashtag, - Instance, - Newsroom, - OauthClient, - Profile, - Report, - Status, - StatusHashtag, - Story, - User -}; -use DB, Cache, Storage; -use Carbon\Carbon; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Redis; -use App\Http\Controllers\Admin\{ - AdminAutospamController, - AdminDirectoryController, - AdminDiscoverController, - AdminHashtagsController, - AdminInstanceController, - AdminReportController, - // AdminGroupsController, - AdminMediaController, - AdminSettingsController, - // AdminStorageController, - AdminSupportController, - AdminUserController -}; -use Illuminate\Validation\Rule; -use App\Services\AdminStatsService; +use App\Contact; +use App\Http\Controllers\Admin\AdminAutospamController; +use App\Http\Controllers\Admin\AdminDirectoryController; +use App\Http\Controllers\Admin\AdminDiscoverController; +use App\Http\Controllers\Admin\AdminHashtagsController; +use App\Http\Controllers\Admin\AdminInstanceController; +use App\Http\Controllers\Admin\AdminMediaController; +use App\Http\Controllers\Admin\AdminReportController; +use App\Http\Controllers\Admin\AdminSettingsController; +use App\Http\Controllers\Admin\AdminUserController; +use App\Instance; +use App\Mail\AdminMessageResponse; +use App\Models\CustomEmoji; +use App\Newsroom; +use App\OauthClient; +use App\Profile; use App\Services\AccountService; +use App\Services\AdminStatsService; +use App\Services\ConfigCacheService; use App\Services\StatusService; use App\Services\StoryService; -use App\Models\CustomEmoji; +use App\Status; +use App\Story; +use App\User; +use Cache; +use DB; +use Illuminate\Http\Request; +use Illuminate\Validation\Rule; +use Mail; +use Storage; class AdminController extends Controller { - use AdminReportController, - AdminAutospamController, - AdminDirectoryController, - AdminDiscoverController, - AdminHashtagsController, - // AdminGroupsController, - AdminMediaController, - AdminSettingsController, - AdminInstanceController, - // AdminStorageController, - AdminUserController; + use AdminAutospamController, + AdminDirectoryController, + AdminDiscoverController, + AdminHashtagsController, + AdminInstanceController, + AdminMediaController, + AdminReportController, + AdminSettingsController, + AdminUserController; - public function __construct() - { - $this->middleware('admin'); - $this->middleware('dangerzone'); - $this->middleware('twofactor'); - } + public function __construct() + { + $this->middleware('admin'); + $this->middleware('dangerzone'); + $this->middleware('twofactor'); + } - public function home() - { - return view('admin.home'); - } + public function home() + { + return view('admin.home'); + } - public function stats() - { - $data = AdminStatsService::get(); - return view('admin.stats', compact('data')); - } + public function customCss() + { + return view('admin.settings.customcss'); + } - public function getStats() - { - return AdminStatsService::summary(); - } + public function saveCustomCss(Request $request) + { + $this->validate($request, [ + 'css' => 'sometimes|max:5000', + 'show' => 'sometimes', + ]); + ConfigCacheService::put('uikit.custom.css', $request->input('css')); + ConfigCacheService::put('uikit.show_custom.css', $request->boolean('show')); - public function getAccounts() - { - $users = User::orderByDesc('id')->cursorPaginate(10); + return view('admin.settings.customcss'); + } - $res = [ - "next_page_url" => $users->nextPageUrl(), - "data" => $users->map(function($user) { - $account = AccountService::get($user->profile_id, true); - if(!$account) { - return [ - "id" => $user->profile_id, - "username" => $user->username, - "status" => "deleted", - "avatar" => "/storage/avatars/default.jpg", - "created_at" => $user->created_at - ]; - } - $account['user_id'] = $user->id; - return $account; - }) - ->filter(function($user) { - return $user; - }) - ]; - return $res; - } + public function stats() + { + $data = AdminStatsService::get(); - public function getPosts() - { - $posts = DB::table('statuses') - ->orderByDesc('id') - ->cursorPaginate(10); + return view('admin.stats', compact('data')); + } - $res = [ - "next_page_url" => $posts->nextPageUrl(), - "data" => $posts->map(function($post) { - $status = StatusService::get($post->id, false); - if(!$status) { - return ["id" => $post->id, "created_at" => $post->created_at]; - } - return $status; - }) - ]; + public function getStats() + { + return AdminStatsService::summary(); + } - return $res; - } + public function getAccounts() + { + $users = User::orderByDesc('id')->cursorPaginate(10); - public function getInstances() - { - return Instance::orderByDesc('id')->cursorPaginate(10); - } + $res = [ + 'next_page_url' => $users->nextPageUrl(), + 'data' => $users->map(function ($user) { + $account = AccountService::get($user->profile_id, true); + if (! $account) { + return [ + 'id' => $user->profile_id, + 'username' => $user->username, + 'status' => 'deleted', + 'avatar' => '/storage/avatars/default.jpg', + 'created_at' => $user->created_at, + ]; + } + $account['user_id'] = $user->id; - public function statuses(Request $request) - { - $statuses = Status::orderBy('id', 'desc')->cursorPaginate(10); - $data = $statuses->map(function($status) { - return StatusService::get($status->id, false); - }) - ->filter(function($s) { - return $s; - }) - ->toArray(); - return view('admin.statuses.home', compact('statuses', 'data')); - } + return $account; + }) + ->filter(function ($user) { + return $user; + }), + ]; - public function showStatus(Request $request, $id) - { - $status = Status::findOrFail($id); + return $res; + } - return view('admin.statuses.show', compact('status')); - } + public function getPosts() + { + $posts = DB::table('statuses') + ->orderByDesc('id') + ->cursorPaginate(10); - public function profiles(Request $request) - { - $this->validate($request, [ - 'search' => 'nullable|string|max:250', - 'filter' => [ - 'nullable', - 'string', - Rule::in(['all', 'local', 'remote']) - ] - ]); - $search = $request->input('search'); - $filter = $request->input('filter'); - $limit = 12; - $profiles = Profile::select('id','username') - ->whereNull('status') - ->when($search, function($q, $search) { - return $q->where('username', 'like', "%$search%"); - })->when($filter, function($q, $filter) { - if($filter == 'local') { - return $q->whereNull('domain'); - } - if($filter == 'remote') { - return $q->whereNotNull('domain'); - } - return $q; - })->orderByDesc('id') - ->simplePaginate($limit); + $res = [ + 'next_page_url' => $posts->nextPageUrl(), + 'data' => $posts->map(function ($post) { + $status = StatusService::get($post->id, false); + if (! $status) { + return ['id' => $post->id, 'created_at' => $post->created_at]; + } - return view('admin.profiles.home', compact('profiles')); - } + return $status; + }), + ]; - public function profileShow(Request $request, $id) - { - $profile = Profile::findOrFail($id); - $user = $profile->user; - return view('admin.profiles.edit', compact('profile', 'user')); - } + return $res; + } - public function appsHome(Request $request) - { - $filter = $request->input('filter'); - if($filter == 'revoked') { - $apps = OauthClient::with('user') - ->whereNotNull('user_id') - ->whereRevoked(true) - ->orderByDesc('id') - ->paginate(10); - } else { - $apps = OauthClient::with('user') - ->whereNotNull('user_id') - ->orderByDesc('id') - ->paginate(10); - } - return view('admin.apps.home', compact('apps')); - } + public function getInstances() + { + return Instance::orderByDesc('id')->cursorPaginate(10); + } - public function messagesHome(Request $request) - { - $messages = Contact::orderByDesc('id')->paginate(10); - return view('admin.messages.home', compact('messages')); - } + public function statuses(Request $request) + { + $statuses = Status::orderBy('id', 'desc')->cursorPaginate(10); + $data = $statuses->map(function ($status) { + return StatusService::get($status->id, false); + }) + ->filter(function ($s) { + return $s; + }) + ->toArray(); - public function messagesShow(Request $request, $id) - { - $message = Contact::findOrFail($id); - return view('admin.messages.show', compact('message')); - } + return view('admin.statuses.home', compact('statuses', 'data')); + } - public function messagesMarkRead(Request $request) - { - $this->validate($request, [ - 'id' => 'required|integer|min:1' - ]); - $id = $request->input('id'); - $message = Contact::findOrFail($id); - if($message->read_at) { - return; - } - $message->read_at = now(); - $message->save(); - return; - } + public function showStatus(Request $request, $id) + { + $status = Status::findOrFail($id); - public function newsroomHome(Request $request) - { - $newsroom = Newsroom::latest()->paginate(10); - return view('admin.newsroom.home', compact('newsroom')); - } + return view('admin.statuses.show', compact('status')); + } - public function newsroomCreate(Request $request) - { - return view('admin.newsroom.create'); - } + public function profiles(Request $request) + { + $this->validate($request, [ + 'search' => 'nullable|string|max:250', + 'filter' => [ + 'nullable', + 'string', + Rule::in(['all', 'local', 'remote']), + ], + ]); + $search = $request->input('search'); + $filter = $request->input('filter'); + $limit = 12; + $profiles = Profile::select('id', 'username') + ->whereNull('status') + ->when($search, function ($q, $search) { + return $q->where('username', 'like', "%$search%"); + })->when($filter, function ($q, $filter) { + if ($filter == 'local') { + return $q->whereNull('domain'); + } + if ($filter == 'remote') { + return $q->whereNotNull('domain'); + } - public function newsroomEdit(Request $request, $id) - { - $news = Newsroom::findOrFail($id); - return view('admin.newsroom.edit', compact('news')); - } + return $q; + })->orderByDesc('id') + ->simplePaginate($limit); - public function newsroomDelete(Request $request, $id) - { - $news = Newsroom::findOrFail($id); - $news->delete(); - return redirect('/i/admin/newsroom'); - } + return view('admin.profiles.home', compact('profiles')); + } - public function newsroomUpdate(Request $request, $id) - { - $this->validate($request, [ - 'title' => 'required|string|min:1|max:100', - 'summary' => 'nullable|string|max:200', - 'body' => 'nullable|string' - ]); - $changed = false; - $changedFields = []; - $slug = str_slug($request->input('title')); - if(Newsroom::whereSlug($slug)->exists()) { - $slug = $slug . '-' . str_random(4); - } - $news = Newsroom::findOrFail($id); - $fields = [ - 'title' => 'string', - 'summary' => 'string', - 'body' => 'string', - 'category' => 'string', - 'show_timeline' => 'boolean', - 'auth_only' => 'boolean', - 'show_link' => 'boolean', - 'force_modal' => 'boolean', - 'published' => 'published' - ]; - foreach($fields as $field => $type) { - switch ($type) { - case 'string': - if($request->{$field} != $news->{$field}) { - if($field == 'title') { - $news->slug = $slug; - } - $news->{$field} = $request->{$field}; - $changed = true; - array_push($changedFields, $field); - } - break; + public function profileShow(Request $request, $id) + { + $profile = Profile::findOrFail($id); + $user = $profile->user; - case 'boolean': - $state = $request->{$field} == 'on' ? true : false; - if($state != $news->{$field}) { - $news->{$field} = $state; - $changed = true; - array_push($changedFields, $field); - } - break; - case 'published': - $state = $request->{$field} == 'on' ? true : false; - $published = $news->published_at != null; - if($state != $published) { - $news->published_at = $state ? now() : null; - $changed = true; - array_push($changedFields, $field); - } - break; + return view('admin.profiles.edit', compact('profile', 'user')); + } - } - } + public function appsHome(Request $request) + { + $filter = $request->input('filter'); + if ($filter == 'revoked') { + $apps = OauthClient::with('user') + ->whereNotNull('user_id') + ->whereRevoked(true) + ->orderByDesc('id') + ->paginate(10); + } else { + $apps = OauthClient::with('user') + ->whereNotNull('user_id') + ->orderByDesc('id') + ->paginate(10); + } - if($changed) { - $news->save(); - } - $redirect = $news->published_at ? $news->permalink() : $news->editUrl(); - return redirect($redirect); - } + return view('admin.apps.home', compact('apps')); + } + public function messagesHome(Request $request) + { + $this->validate($request, [ + 'sort' => 'sometimes|string|in:all,open,closed', + ]); + $sort = $request->input('sort', 'open'); - public function newsroomStore(Request $request) - { - $this->validate($request, [ - 'title' => 'required|string|min:1|max:100', - 'summary' => 'nullable|string|max:200', - 'body' => 'nullable|string' - ]); - $changed = false; - $changedFields = []; - $slug = str_slug($request->input('title')); - if(Newsroom::whereSlug($slug)->exists()) { - $slug = $slug . '-' . str_random(4); - } - $news = new Newsroom(); - $fields = [ - 'title' => 'string', - 'summary' => 'string', - 'body' => 'string', - 'category' => 'string', - 'show_timeline' => 'boolean', - 'auth_only' => 'boolean', - 'show_link' => 'boolean', - 'force_modal' => 'boolean', - 'published' => 'published' - ]; - foreach($fields as $field => $type) { - switch ($type) { - case 'string': - if($request->{$field} != $news->{$field}) { - if($field == 'title') { - $news->slug = $slug; - } - $news->{$field} = $request->{$field}; - $changed = true; - array_push($changedFields, $field); - } - break; + $messages = Contact::when($sort, function ($query, $sort) { + if ($sort === 'open') { + $query->whereNull('read_at'); + } + if ($sort === 'closed') { + $query->whereNotNull('read_at'); + } + }) + ->orderByDesc('id') + ->paginate(10) + ->withQueryString(); - case 'boolean': - $state = $request->{$field} == 'on' ? true : false; - if($state != $news->{$field}) { - $news->{$field} = $state; - $changed = true; - array_push($changedFields, $field); - } - break; - case 'published': - $state = $request->{$field} == 'on' ? true : false; - $published = $news->published_at != null; - if($state != $published) { - $news->published_at = $state ? now() : null; - $changed = true; - array_push($changedFields, $field); - } - break; + return view('admin.messages.home', compact('messages', 'sort')); + } - } - } + public function messagesShow(Request $request, $id) + { + $message = Contact::findOrFail($id); + $user = User::whereNull('status')->find($message->user_id); + if(!$user) { + $message->read_at = now(); + $message->save(); + return redirect('/i/admin/messages/home')->with('status', 'Redirected from message sent from a deleted account'); + } - if($changed) { - $news->save(); - } - $redirect = $news->published_at ? $news->permalink() : $news->editUrl(); - return redirect($redirect); - } + return view('admin.messages.show', compact('message')); + } - public function diagnosticsHome(Request $request) - { - return view('admin.diagnostics.home'); - } + public function messagesReply(Request $request, $id) + { + $this->validate($request, [ + 'message' => 'required|string|min:1|max:500', + ]); - public function diagnosticsDecrypt(Request $request) - { - $this->validate($request, [ - 'payload' => 'required' - ]); + if(config('mail.default') === 'log') { + return redirect('/i/admin/messages/home')->with('error', 'Mail driver not configured, please setup before you can sent email.'); + } - $key = 'exception_report:'; - $decrypted = decrypt($request->input('payload')); + $message = Contact::whereNull('responded_at')->findOrFail($id); + $user = User::whereNull('status')->find($message->user_id); + if(!$user) { + $message->read_at = now(); + $message->save(); + return redirect('/i/admin/messages/home')->with('status', 'Redirected from message sent from a deleted account'); + } + $message->response = $request->input('message'); + $message->read_at = now(); + $message->responded_at = now(); + $message->save(); - if(!starts_with($decrypted, $key)) { - abort(403, 'Can only decrypt error diagnostics'); - } + Mail::to($message->user->email)->send(new AdminMessageResponse($message)); - $res = [ - 'decrypted' => substr($decrypted, strlen($key)) - ]; + return redirect('/i/admin/messages/home')->with('status', 'Sent response to '.$message->user->username); + } - return response()->json($res); - } + public function messagesReplyPreview(Request $request, $id) + { + $this->validate($request, [ + 'message' => 'required|string|min:1|max:500', + ]); - public function stories(Request $request) - { - $stories = Story::with('profile')->latest()->paginate(10); - $stats = StoryService::adminStats(); - return view('admin.stories.home', compact('stories', 'stats')); - } + if(config('mail.default') === 'log') { + return redirect('/i/admin/messages/home')->with('error', 'Mail driver not configured, please setup before you can sent email.'); + } - public function customEmojiHome(Request $request) - { - if(!config('federation.custom_emoji.enabled')) { - return view('admin.custom-emoji.not-enabled'); - } - $this->validate($request, [ - 'sort' => 'sometimes|in:all,local,remote,duplicates,disabled,search' - ]); + $message = Contact::whereNull('read_at')->findOrFail($id); + $user = User::whereNull('status')->find($message->user_id); + if(!$user) { + $message->read_at = now(); + $message->save(); + return redirect('/i/admin/messages/home')->with('error', 'Redirected from message sent from a deleted account'); + } + return new AdminMessageResponse($message); + } - if($request->has('cc')) { - Cache::forget('pf:admin:custom_emoji:stats'); - Cache::forget('pf:custom_emoji'); - return redirect(route('admin.custom-emoji')); - } + public function messagesMarkRead(Request $request) + { + $this->validate($request, [ + 'id' => 'required|integer|min:1', + ]); + $id = $request->input('id'); + $message = Contact::findOrFail($id); - $sort = $request->input('sort') ?? 'all'; + $user = User::whereNull('status')->find($message->user_id); + if(!$user) { + $message->read_at = now(); + $message->save(); + return redirect('/i/admin/messages/home')->with('error', 'Redirected from message sent from a deleted account'); + } + if ($message->read_at) { + return; + } + $message->read_at = now(); + $message->save(); + $request->session()->flash('status', 'Marked response from '.$message->user->username.' as read!'); - if($sort == 'search' && empty($request->input('q'))) { - return redirect(route('admin.custom-emoji')); - } + return ['status' => 200]; + } - $pg = config('database.default') == 'pgsql'; + public function newsroomHome(Request $request) + { + $newsroom = Newsroom::latest()->paginate(10); - $emojis = CustomEmoji::when($sort, function($query, $sort) use($request, $pg) { - if($sort == 'all') { - if($pg) { - return $query->latest(); - } else { - return $query->groupBy('shortcode')->latest(); - } - } else if($sort == 'local') { - return $query->latest()->where('domain', '=', config('pixelfed.domain.app')); - } else if($sort == 'remote') { - return $query->latest()->where('domain', '!=', config('pixelfed.domain.app')); - } else if($sort == 'duplicates') { - return $query->latest()->groupBy('shortcode')->havingRaw('count(*) > 1'); - } else if($sort == 'disabled') { - return $query->latest()->whereDisabled(true); - } else if($sort == 'search') { - $q = $query - ->latest() - ->where('shortcode', 'like', '%' . $request->input('q') . '%') - ->orWhere('domain', 'like', '%' . $request->input('q') . '%'); - if(!$request->has('dups')) { - if(!$pg) { - $q = $q->groupBy('shortcode'); - } - } - return $q; - } - }) - ->simplePaginate(10) - ->withQueryString(); + return view('admin.newsroom.home', compact('newsroom')); + } - $stats = Cache::remember('pf:admin:custom_emoji:stats', 43200, function() use($pg) { - $res = [ - 'total' => CustomEmoji::count(), - 'active' => CustomEmoji::whereDisabled(false)->count(), - 'remote' => CustomEmoji::where('domain', '!=', config('pixelfed.domain.app'))->count(), - ]; + public function newsroomCreate(Request $request) + { + return view('admin.newsroom.create'); + } - if($pg) { - $res['duplicate'] = CustomEmoji::select('shortcode')->groupBy('shortcode')->havingRaw('count(*) > 1')->count(); - } else { - $res['duplicate'] = CustomEmoji::groupBy('shortcode')->havingRaw('count(*) > 1')->count(); - } + public function newsroomEdit(Request $request, $id) + { + $news = Newsroom::findOrFail($id); - return $res; - }); + return view('admin.newsroom.edit', compact('news')); + } - return view('admin.custom-emoji.home', compact('emojis', 'sort', 'stats')); - } + public function newsroomDelete(Request $request, $id) + { + $news = Newsroom::findOrFail($id); + $news->delete(); - public function customEmojiToggleActive(Request $request, $id) - { - abort_unless(config('federation.custom_emoji.enabled'), 404); - $emoji = CustomEmoji::findOrFail($id); - $emoji->disabled = !$emoji->disabled; - $emoji->save(); - $key = CustomEmoji::CACHE_KEY . str_replace(':', '', $emoji->shortcode); - Cache::forget($key); - return redirect()->back(); - } + return redirect('/i/admin/newsroom'); + } - public function customEmojiAdd(Request $request) - { - abort_unless(config('federation.custom_emoji.enabled'), 404); - return view('admin.custom-emoji.add'); - } + public function newsroomUpdate(Request $request, $id) + { + $this->validate($request, [ + 'title' => 'required|string|min:1|max:100', + 'summary' => 'nullable|string|max:200', + 'body' => 'nullable|string', + ]); + $changed = false; + $changedFields = []; + $slug = str_slug($request->input('title')); + if (Newsroom::whereSlug($slug)->exists()) { + $slug = $slug.'-'.str_random(4); + } + $news = Newsroom::findOrFail($id); + $fields = [ + 'title' => 'string', + 'summary' => 'string', + 'body' => 'string', + 'category' => 'string', + 'show_timeline' => 'boolean', + 'auth_only' => 'boolean', + 'show_link' => 'boolean', + 'force_modal' => 'boolean', + 'published' => 'published', + ]; + foreach ($fields as $field => $type) { + switch ($type) { + case 'string': + if ($request->{$field} != $news->{$field}) { + if ($field == 'title') { + $news->slug = $slug; + } + $news->{$field} = $request->{$field}; + $changed = true; + array_push($changedFields, $field); + } + break; - public function customEmojiStore(Request $request) - { - abort_unless(config('federation.custom_emoji.enabled'), 404); - $this->validate($request, [ - 'shortcode' => [ - 'required', - 'min:3', - 'max:80', - 'starts_with::', - 'ends_with::', - Rule::unique('custom_emoji')->where(function ($query) use($request) { - return $query->whereDomain(config('pixelfed.domain.app')) - ->whereShortcode($request->input('shortcode')); - }) - ], - 'emoji' => 'required|file|mimes:jpg,png|max:' . (config('federation.custom_emoji.max_size') / 1000) - ]); + case 'boolean': + $state = $request->{$field} == 'on' ? true : false; + if ($state != $news->{$field}) { + $news->{$field} = $state; + $changed = true; + array_push($changedFields, $field); + } + break; + case 'published': + $state = $request->{$field} == 'on' ? true : false; + $published = $news->published_at != null; + if ($state != $published) { + $news->published_at = $state ? now() : null; + $changed = true; + array_push($changedFields, $field); + } + break; - $emoji = new CustomEmoji; - $emoji->shortcode = $request->input('shortcode'); - $emoji->domain = config('pixelfed.domain.app'); - $emoji->save(); + } + } - $fileName = $emoji->id . '.' . $request->emoji->extension(); - $request->emoji->storePubliclyAs('public/emoji', $fileName); - $emoji->media_path = 'emoji/' . $fileName; - $emoji->save(); - Cache::forget('pf:custom_emoji'); - return redirect(route('admin.custom-emoji')); - } + if ($changed) { + $news->save(); + } + $redirect = $news->published_at ? $news->permalink() : $news->editUrl(); - public function customEmojiDelete(Request $request, $id) - { - abort_unless(config('federation.custom_emoji.enabled'), 404); - $emoji = CustomEmoji::findOrFail($id); - Storage::delete("public/{$emoji->media_path}"); - Cache::forget('pf:custom_emoji'); - $emoji->delete(); - return redirect(route('admin.custom-emoji')); - } + return redirect($redirect); + } - public function customEmojiShowDuplicates(Request $request, $id) - { - abort_unless(config('federation.custom_emoji.enabled'), 404); - $emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail(); - $emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10); - return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis')); - } + public function newsroomStore(Request $request) + { + $this->validate($request, [ + 'title' => 'required|string|min:1|max:100', + 'summary' => 'nullable|string|max:200', + 'body' => 'nullable|string', + ]); + $changed = false; + $changedFields = []; + $slug = str_slug($request->input('title')); + if (Newsroom::whereSlug($slug)->exists()) { + $slug = $slug.'-'.str_random(4); + } + $news = new Newsroom; + $fields = [ + 'title' => 'string', + 'summary' => 'string', + 'body' => 'string', + 'category' => 'string', + 'show_timeline' => 'boolean', + 'auth_only' => 'boolean', + 'show_link' => 'boolean', + 'force_modal' => 'boolean', + 'published' => 'published', + ]; + foreach ($fields as $field => $type) { + switch ($type) { + case 'string': + if ($request->{$field} != $news->{$field}) { + if ($field == 'title') { + $news->slug = $slug; + } + $news->{$field} = $request->{$field}; + $changed = true; + array_push($changedFields, $field); + } + break; + + case 'boolean': + $state = $request->{$field} == 'on' ? true : false; + if ($state != $news->{$field}) { + $news->{$field} = $state; + $changed = true; + array_push($changedFields, $field); + } + break; + case 'published': + $state = $request->{$field} == 'on' ? true : false; + $published = $news->published_at != null; + if ($state != $published) { + $news->published_at = $state ? now() : null; + $changed = true; + array_push($changedFields, $field); + } + break; + + } + } + + if ($changed) { + $news->save(); + } + $redirect = $news->published_at ? $news->permalink() : $news->editUrl(); + + return redirect($redirect); + } + + public function diagnosticsHome(Request $request) + { + return view('admin.diagnostics.home'); + } + + public function diagnosticsDecrypt(Request $request) + { + $this->validate($request, [ + 'payload' => 'required', + ]); + + $key = 'exception_report:'; + $decrypted = decrypt($request->input('payload')); + + if (! starts_with($decrypted, $key)) { + abort(403, 'Can only decrypt error diagnostics'); + } + + $res = [ + 'decrypted' => substr($decrypted, strlen($key)), + ]; + + return response()->json($res); + } + + public function stories(Request $request) + { + $stories = Story::with('profile')->latest()->paginate(10); + $stats = StoryService::adminStats(); + + return view('admin.stories.home', compact('stories', 'stats')); + } + + public function customEmojiHome(Request $request) + { + if (! (bool) config_cache('federation.custom_emoji.enabled')) { + return view('admin.custom-emoji.not-enabled'); + } + $this->validate($request, [ + 'sort' => 'sometimes|in:all,local,remote,duplicates,disabled,search', + ]); + + if ($request->has('cc')) { + Cache::forget('pf:admin:custom_emoji:stats'); + Cache::forget('pf:custom_emoji'); + + return redirect(route('admin.custom-emoji')); + } + + $sort = $request->input('sort') ?? 'all'; + + if ($sort == 'search' && empty($request->input('q'))) { + return redirect(route('admin.custom-emoji')); + } + + $pg = config('database.default') == 'pgsql'; + + $emojis = CustomEmoji::when($sort, function ($query, $sort) use ($request, $pg) { + if ($sort == 'all') { + if ($pg) { + return $query->latest(); + } else { + return $query->groupBy('shortcode')->latest(); + } + } elseif ($sort == 'local') { + return $query->latest()->where('domain', '=', config('pixelfed.domain.app')); + } elseif ($sort == 'remote') { + return $query->latest()->where('domain', '!=', config('pixelfed.domain.app')); + } elseif ($sort == 'duplicates') { + return $query->latest()->groupBy('shortcode')->havingRaw('count(*) > 1'); + } elseif ($sort == 'disabled') { + return $query->latest()->whereDisabled(true); + } elseif ($sort == 'search') { + $q = $query + ->latest() + ->where('shortcode', 'like', '%'.$request->input('q').'%') + ->orWhere('domain', 'like', '%'.$request->input('q').'%'); + if (! $request->has('dups')) { + if (! $pg) { + $q = $q->groupBy('shortcode'); + } + } + + return $q; + } + }) + ->simplePaginate(10) + ->withQueryString(); + + $stats = Cache::remember('pf:admin:custom_emoji:stats', 43200, function () use ($pg) { + $res = [ + 'total' => CustomEmoji::count(), + 'active' => CustomEmoji::whereDisabled(false)->count(), + 'remote' => CustomEmoji::where('domain', '!=', config('pixelfed.domain.app'))->count(), + ]; + + if ($pg) { + $res['duplicate'] = CustomEmoji::select('shortcode')->groupBy('shortcode')->havingRaw('count(*) > 1')->count(); + } else { + $res['duplicate'] = CustomEmoji::groupBy('shortcode')->havingRaw('count(*) > 1')->count(); + } + + return $res; + }); + + return view('admin.custom-emoji.home', compact('emojis', 'sort', 'stats')); + } + + public function customEmojiToggleActive(Request $request, $id) + { + abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404); + $emoji = CustomEmoji::findOrFail($id); + $emoji->disabled = ! $emoji->disabled; + $emoji->save(); + $key = CustomEmoji::CACHE_KEY.str_replace(':', '', $emoji->shortcode); + Cache::forget($key); + + return redirect()->back(); + } + + public function customEmojiAdd(Request $request) + { + abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404); + + return view('admin.custom-emoji.add'); + } + + public function customEmojiStore(Request $request) + { + abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404); + $this->validate($request, [ + 'shortcode' => [ + 'required', + 'min:3', + 'max:80', + 'starts_with::', + 'ends_with::', + Rule::unique('custom_emoji')->where(function ($query) use ($request) { + return $query->whereDomain(config('pixelfed.domain.app')) + ->whereShortcode($request->input('shortcode')); + }), + ], + 'emoji' => 'required|file|mimes:jpg,png|max:'.(config('federation.custom_emoji.max_size') / 1000), + ]); + + $emoji = new CustomEmoji; + $emoji->shortcode = $request->input('shortcode'); + $emoji->domain = config('pixelfed.domain.app'); + $emoji->save(); + + $fileName = $emoji->id.'.'.$request->emoji->extension(); + $request->emoji->storePubliclyAs('public/emoji', $fileName); + $emoji->media_path = 'emoji/'.$fileName; + $emoji->save(); + Cache::forget('pf:custom_emoji'); + + return redirect(route('admin.custom-emoji')); + } + + public function customEmojiDelete(Request $request, $id) + { + abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404); + $emoji = CustomEmoji::findOrFail($id); + Storage::delete("public/{$emoji->media_path}"); + Cache::forget('pf:custom_emoji'); + $emoji->delete(); + + return redirect(route('admin.custom-emoji')); + } + + public function customEmojiShowDuplicates(Request $request, $id) + { + abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404); + $emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail(); + $emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10); + + return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis')); + } } diff --git a/app/Http/Controllers/AdminCuratedRegisterController.php b/app/Http/Controllers/AdminCuratedRegisterController.php new file mode 100644 index 000000000..afdcfba1a --- /dev/null +++ b/app/Http/Controllers/AdminCuratedRegisterController.php @@ -0,0 +1,340 @@ +middleware(['auth', 'admin']); + } + + public function index(Request $request) + { + $this->validate($request, [ + 'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses', + 'sort' => 'sometimes|in:asc,desc', + ]); + $filter = $request->input('filter', 'open'); + $sort = $request->input('sort', 'asc'); + $records = CuratedRegister::when($filter, function ($q, $filter) { + if ($filter === 'open') { + return $q->where('is_rejected', false) + ->where(function ($query) { + return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false); + }) + ->whereNotNull('email_verified_at') + ->whereIsClosed(false); + } elseif ($filter === 'all') { + return $q; + } elseif ($filter === 'responses') { + return $q->whereIsClosed(false) + ->whereNotNull('email_verified_at') + ->where('user_has_responded', true) + ->where('is_awaiting_more_info', true); + } elseif ($filter === 'awaiting') { + return $q->whereIsClosed(false) + ->where('is_rejected', false) + ->where('is_approved', false) + ->where('user_has_responded', false) + ->where('is_awaiting_more_info', true); + } elseif ($filter === 'approved') { + return $q->whereIsClosed(true)->whereIsApproved(true); + } elseif ($filter === 'rejected') { + return $q->whereIsClosed(true)->whereIsRejected(true); + } + }) + ->when($sort, function ($query, $sort) { + return $query->orderBy('id', $sort); + }) + ->paginate(10) + ->withQueryString(); + + return view('admin.curated-register.index', compact('records', 'filter')); + } + + public function show(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + + return view('admin.curated-register.show', compact('record')); + } + + public function apiActivityLog(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + + $res = collect([ + [ + 'id' => 1, + 'action' => 'created', + 'title' => 'Onboarding application created', + 'message' => null, + 'link' => null, + 'timestamp' => $record->created_at, + ], + ]); + + if ($record->email_verified_at) { + $res->push([ + 'id' => 3, + 'action' => 'email_verified_at', + 'title' => 'Applicant successfully verified email address', + 'message' => null, + 'link' => null, + 'timestamp' => $record->email_verified_at, + ]); + } + + $activities = CuratedRegisterActivity::whereRegisterId($record->id)->get(); + + $idx = 4; + $userResponses = collect([]); + + foreach ($activities as $activity) { + $idx++; + + if ($activity->type === 'user_resend_email_confirmation') { + continue; + } + if ($activity->from_user) { + $userResponses->push($activity); + + continue; + } + $res->push([ + 'id' => $idx, + 'aid' => $activity->id, + 'action' => $activity->type, + 'title' => $activity->from_admin ? 'Admin requested info' : 'User responded', + 'message' => $activity->message, + 'link' => $activity->adminReviewUrl(), + 'timestamp' => $activity->created_at, + ]); + } + + foreach ($userResponses as $ur) { + $res = $res->map(function ($r) use ($ur) { + if (! isset($r['aid'])) { + return $r; + } + if ($ur->reply_to_id === $r['aid']) { + $r['user_response'] = $ur; + + return $r; + } + + return $r; + }); + } + + if ($record->is_approved) { + $idx++; + $res->push([ + 'id' => $idx, + 'action' => 'approved', + 'title' => 'Application Approved', + 'message' => null, + 'link' => null, + 'timestamp' => $record->action_taken_at, + ]); + } elseif ($record->is_rejected) { + $idx++; + $res->push([ + 'id' => $idx, + 'action' => 'rejected', + 'title' => 'Application Rejected', + 'message' => null, + 'link' => null, + 'timestamp' => $record->action_taken_at, + ]); + } + + return $res->reverse()->values(); + } + + public function apiMessagePreviewStore(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + + return $request->all(); + } + + public function apiMessageSendStore(Request $request, $id) + { + $this->validate($request, [ + 'message' => 'required|string|min:5|max:3000', + ]); + $record = CuratedRegister::findOrFail($id); + abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); + $activity = new CuratedRegisterActivity; + $activity->register_id = $record->id; + $activity->admin_id = $request->user()->id; + $activity->secret_code = Str::random(32); + $activity->type = 'request_details'; + $activity->from_admin = true; + $activity->message = $request->input('message'); + $activity->save(); + $record->is_awaiting_more_info = true; + $record->user_has_responded = false; + $record->save(); + Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity)); + + return $request->all(); + } + + public function previewDetailsMessageShow(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); + $activity = new CuratedRegisterActivity; + $activity->message = $request->input('message'); + + return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity); + } + + public function previewMessageShow(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); + $record->message = $request->input('message'); + + return new \App\Mail\CuratedRegisterSendMessage($record); + } + + public function apiHandleReject(Request $request, $id) + { + $this->validate($request, [ + 'action' => 'required|in:reject-email,reject-silent', + ]); + $action = $request->input('action'); + $record = CuratedRegister::findOrFail($id); + abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email'); + $record->is_rejected = true; + $record->is_closed = true; + $record->action_taken_at = now(); + $record->save(); + if ($action === 'reject-email') { + Mail::to($record->email)->send(new CuratedRegisterRejectUser($record)); + } + + return [200]; + } + + public function apiHandleApprove(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email'); + $record->is_approved = true; + $record->is_closed = true; + $record->action_taken_at = now(); + $record->save(); + + if (User::withTrashed()->whereEmail($record->email)->exists()) { + return [200]; + } + + $user = User::create([ + 'name' => $record->username, + 'username' => $record->username, + 'email' => $record->email, + 'password' => $record->password, + 'app_register_ip' => $record->ip_address, + 'email_verified_at' => now(), + 'register_source' => 'cur_onboarding', + ]); + + Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record)); + + return [200]; + } + + public function templates(Request $request) + { + $templates = CuratedRegisterTemplate::paginate(10); + + return view('admin.curated-register.templates', compact('templates')); + } + + public function templateCreate(Request $request) + { + return view('admin.curated-register.template-create'); + } + + public function templateEdit(Request $request, $id) + { + $template = CuratedRegisterTemplate::findOrFail($id); + + return view('admin.curated-register.template-edit', compact('template')); + } + + public function templateEditStore(Request $request, $id) + { + $this->validate($request, [ + 'name' => 'required|string|max:30', + 'content' => 'required|string|min:5|max:3000', + 'description' => 'nullable|sometimes|string|max:1000', + 'active' => 'sometimes', + ]); + $template = CuratedRegisterTemplate::findOrFail($id); + $template->name = $request->input('name'); + $template->content = $request->input('content'); + $template->description = $request->input('description'); + $template->is_active = $request->boolean('active'); + $template->save(); + + return redirect()->back()->with('status', 'Successfully updated template!'); + } + + public function templateDelete(Request $request, $id) + { + $template = CuratedRegisterTemplate::findOrFail($id); + $template->delete(); + + return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully deleted template!'); + } + + public function templateStore(Request $request) + { + $this->validate($request, [ + 'name' => 'required|string|max:30', + 'content' => 'required|string|min:5|max:3000', + 'description' => 'nullable|sometimes|string|max:1000', + 'active' => 'sometimes', + ]); + CuratedRegisterTemplate::create([ + 'name' => $request->input('name'), + 'content' => $request->input('content'), + 'description' => $request->input('description'), + 'is_active' => $request->boolean('active'), + ]); + + return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully created new template!'); + } + + public function getActiveTemplates(Request $request) + { + $templates = CuratedRegisterTemplate::whereIsActive(true) + ->orderBy('order') + ->get() + ->map(function ($tmp) { + return [ + 'name' => $tmp->name, + 'content' => $tmp->content, + ]; + }); + + return response()->json($templates); + } +} diff --git a/app/Http/Controllers/AdminShadowFilterController.php b/app/Http/Controllers/AdminShadowFilterController.php index 461e1d0c2..e181be5c1 100644 --- a/app/Http/Controllers/AdminShadowFilterController.php +++ b/app/Http/Controllers/AdminShadowFilterController.php @@ -19,7 +19,8 @@ class AdminShadowFilterController extends Controller { $filter = $request->input('filter'); $searchQuery = $request->input('q'); - $filters = AdminShadowFilter::when($filter, function($q, $filter) { + $filters = AdminShadowFilter::whereHas('profile') + ->when($filter, function($q, $filter) { if($filter == 'all') { return $q; } else if($filter == 'inactive') { diff --git a/app/Http/Controllers/Api/AdminApiController.php b/app/Http/Controllers/Api/AdminApiController.php index 76f73e720..21a832a76 100644 --- a/app/Http/Controllers/Api/AdminApiController.php +++ b/app/Http/Controllers/Api/AdminApiController.php @@ -2,91 +2,94 @@ namespace App\Http\Controllers\Api; -use Illuminate\Http\Request; +use App\AccountInterstitial; use App\Http\Controllers\Controller; +use App\Http\Resources\AdminInstance; +use App\Http\Resources\AdminUser; +use App\Instance; +use App\Jobs\DeletePipeline\DeleteAccountPipeline; +use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; use App\Jobs\StatusPipeline\StatusDelete; -use Auth, Cache, DB; -use Carbon\Carbon; -use App\{ - AccountInterstitial, - Instance, - Like, - Notification, - Media, - Profile, - Report, - Status, - User -}; use App\Models\Conversation; use App\Models\RemoteReport; +use App\Notification; +use App\Profile; +use App\Report; use App\Services\AccountService; use App\Services\AdminStatsService; use App\Services\ConfigCacheService; use App\Services\InstanceService; use App\Services\ModLogService; -use App\Services\SnowflakeService; -use App\Services\StatusService; -use App\Services\PublicTimelineService; use App\Services\NetworkTimelineService; use App\Services\NotificationService; -use App\Http\Resources\AdminInstance; -use App\Http\Resources\AdminUser; -use App\Jobs\DeletePipeline\DeleteAccountPipeline; -use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; -use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline; +use App\Services\PublicTimelineService; +use App\Services\SnowflakeService; +use App\Services\StatusService; +use App\Status; +use App\User; +use Cache; +use DB; +use Illuminate\Http\Request; class AdminApiController extends Controller { public function supported(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); return response()->json(['supported' => true]); } public function getStats(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); $res = AdminStatsService::summary(); $res['autospam_count'] = AccountInterstitial::whereType('post.autospam') ->whereNull('appeal_handled_at') ->count(); + return $res; } public function autospam(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); $appeals = AccountInterstitial::whereType('post.autospam') ->whereNull('appeal_handled_at') ->latest() ->simplePaginate(6) - ->map(function($report) { + ->map(function ($report) { $r = [ 'id' => $report->id, 'type' => $report->type, 'item_id' => $report->item_id, 'item_type' => $report->item_type, - 'created_at' => $report->created_at + 'created_at' => $report->created_at, ]; - if($report->item_type === 'App\\Status') { + if ($report->item_type === 'App\\Status') { $status = StatusService::get($report->item_id, false); - if(!$status) { + if (! $status) { return; } $r['status'] = $status; - if($status['in_reply_to_id']) { + if ($status['in_reply_to_id']) { $r['parent'] = StatusService::get($status['in_reply_to_id'], false); } } + return $r; }); @@ -95,12 +98,14 @@ class AdminApiController extends Controller public function autospamHandle(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-post,delete-account', - 'id' => 'required' + 'id' => 'required', ]); $action = $request->input('action'); @@ -114,18 +119,19 @@ class AdminApiController extends Controller $user = $appeal->user; $profile = $user->profile; - if($action == 'dismiss') { + if ($action == 'dismiss') { $appeal->is_spam = true; $appeal->appeal_handled_at = $now; $appeal->save(); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $profile->id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $profile->id); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$profile->id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$profile->id); Cache::forget('admin-dash:reports:spam-count'); + return $res; } - if($action == 'delete-post') { + if ($action == 'delete-post') { $appeal->appeal_handled_at = now(); $appeal->is_spam = true; $appeal->save(); @@ -140,10 +146,11 @@ class AdminApiController extends Controller PublicTimelineService::deleteByProfileId($profile->id); StatusDelete::dispatch($appeal->status)->onQueue('high'); Cache::forget('admin-dash:reports:spam-count'); + return $res; } - if($action == 'delete-account') { + if ($action == 'delete-account') { abort_if($user->is_admin, 400, 'Cannot delete an admin account.'); $appeal->appeal_handled_at = now(); $appeal->is_spam = true; @@ -159,22 +166,24 @@ class AdminApiController extends Controller PublicTimelineService::deleteByProfileId($profile->id); DeleteAccountPipeline::dispatch($appeal->user)->onQueue('high'); Cache::forget('admin-dash:reports:spam-count'); + return $res; } - if($action == 'dismiss-all') { + if ($action == 'dismiss-all') { AccountInterstitial::whereType('post.autospam') ->whereItemType('App\Status') ->whereNull('appeal_handled_at') ->whereUserId($appeal->user_id) ->update(['appeal_handled_at' => $now, 'is_spam' => true]); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); Cache::forget('admin-dash:reports:spam-count'); + return $res; } - if($action == 'approve') { + if ($action == 'approve') { $status = $appeal->status; $status->is_nsfw = $meta->is_nsfw; $status->scope = 'public'; @@ -190,29 +199,30 @@ class AdminApiController extends Controller Notification::whereAction('autospam.warning') ->whereProfileId($appeal->user->profile_id) ->get() - ->each(function($n) use($appeal) { + ->each(function ($n) use ($appeal) { NotificationService::del($appeal->user->profile_id, $n->id); $n->forceDelete(); }); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); Cache::forget('admin-dash:reports:spam-count'); + return $res; } - if($action == 'approve-all') { + if ($action == 'approve-all') { AccountInterstitial::whereType('post.autospam') ->whereItemType('App\Status') ->whereNull('appeal_handled_at') ->whereUserId($appeal->user_id) ->get() - ->each(function($report) use($meta) { + ->each(function ($report) use ($meta) { $report->is_spam = false; $report->appeal_handled_at = now(); $report->save(); $status = Status::find($report->item_id); - if($status) { + if ($status) { $status->is_nsfw = $meta->is_nsfw; $status->scope = 'public'; $status->visibility = 'public'; @@ -223,14 +233,15 @@ class AdminApiController extends Controller Notification::whereAction('autospam.warning') ->whereProfileId($report->user->profile_id) ->get() - ->each(function($n) use($report) { + ->each(function ($n) use ($report) { NotificationService::del($report->user->profile_id, $n->id); $n->forceDelete(); }); }); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); Cache::forget('admin-dash:reports:spam-count'); + return $res; } @@ -239,42 +250,48 @@ class AdminApiController extends Controller public function modReports(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); $reports = Report::whereNull('admin_seen') - ->orderBy('created_at','desc') + ->orderBy('created_at', 'desc') ->paginate(6) - ->map(function($report) { + ->map(function ($report) { $r = [ 'id' => $report->id, 'type' => $report->type, 'message' => $report->message, 'object_id' => $report->object_id, 'object_type' => $report->object_type, - 'created_at' => $report->created_at + 'created_at' => $report->created_at, ]; - if($report->profile_id) { + if ($report->profile_id) { $r['reported_by_account'] = AccountService::get($report->profile_id, true); } - if($report->object_type === 'App\\Status') { + if ($report->object_type === 'App\\Status') { $status = StatusService::get($report->object_id, false); - if(!$status) { + if (! $status) { return; } $r['status'] = $status; - if($status['in_reply_to_id']) { + if (isset($status['in_reply_to_id'])) { $r['parent'] = StatusService::get($status['in_reply_to_id'], false); } } - if($report->object_type === 'App\\Profile') { - $r['account'] = AccountService::get($report->object_id, false); + if ($report->object_type === 'App\\Profile') { + $acct = AccountService::get($report->object_id, true); + if ($acct) { + $r['account'] = $acct; + } } + return $r; }) ->filter() @@ -285,12 +302,14 @@ class AdminApiController extends Controller public function modReportHandle(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ - 'action' => 'required|string', - 'id' => 'required' + 'action' => 'required|string', + 'id' => 'required', ]); $action = $request->input('action'); @@ -299,10 +318,10 @@ class AdminApiController extends Controller $actions = [ 'ignore', 'cw', - 'unlist' + 'unlist', ]; - if (!in_array($action, $actions)) { + if (! in_array($action, $actions)) { return abort(403); } @@ -343,56 +362,63 @@ class AdminApiController extends Controller public function getConfiguration(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); + abort_unless(config('instance.enable_cc'), 400); return collect([ [ 'name' => 'ActivityPub Federation', 'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.', - 'key' => 'federation.activitypub.enabled' + 'key' => 'federation.activitypub.enabled', ], [ 'name' => 'Open Registration', 'description' => 'Allow new account registrations.', - 'key' => 'pixelfed.open_registration' + 'key' => 'pixelfed.open_registration', ], [ 'name' => 'Stories', 'description' => 'Enable the ephemeral Stories feature.', - 'key' => 'instance.stories.enabled' + 'key' => 'instance.stories.enabled', ], [ 'name' => 'Require Email Verification', 'description' => 'Require new accounts to verify their email address.', - 'key' => 'pixelfed.enforce_email_verification' + 'key' => 'pixelfed.enforce_email_verification', ], [ 'name' => 'AutoSpam Detection', 'description' => 'Detect and remove spam from public timelines.', - 'key' => 'pixelfed.bouncer.enabled' + 'key' => 'pixelfed.bouncer.enabled', ], ]) - ->map(function($s) { - $s['state'] = (bool) config_cache($s['key']); - return $s; - }); + ->map(function ($s) { + $s['state'] = (bool) config_cache($s['key']); + + return $s; + }); } public function updateConfiguration(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); + abort_unless(config('instance.enable_cc'), 400); $this->validate($request, [ 'key' => 'required', - 'value' => 'required' + 'value' => 'required', ]); $allowedKeys = [ @@ -405,76 +431,84 @@ class AdminApiController extends Controller $key = $request->input('key'); $value = (bool) filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN); - abort_if(!in_array($key, $allowedKeys), 400, 'Invalid cache key.'); + abort_if(! in_array($key, $allowedKeys), 400, 'Invalid cache key.'); ConfigCacheService::put($key, $value); - return collect([ + return collect([ [ 'name' => 'ActivityPub Federation', 'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.', - 'key' => 'federation.activitypub.enabled' + 'key' => 'federation.activitypub.enabled', ], [ 'name' => 'Open Registration', 'description' => 'Allow new account registrations.', - 'key' => 'pixelfed.open_registration' + 'key' => 'pixelfed.open_registration', ], [ 'name' => 'Stories', 'description' => 'Enable the ephemeral Stories feature.', - 'key' => 'instance.stories.enabled' + 'key' => 'instance.stories.enabled', ], [ 'name' => 'Require Email Verification', 'description' => 'Require new accounts to verify their email address.', - 'key' => 'pixelfed.enforce_email_verification' + 'key' => 'pixelfed.enforce_email_verification', ], [ 'name' => 'AutoSpam Detection', 'description' => 'Detect and remove spam from public timelines.', - 'key' => 'pixelfed.bouncer.enabled' + 'key' => 'pixelfed.bouncer.enabled', ], ]) - ->map(function($s) { - $s['state'] = (bool) config_cache($s['key']); - return $s; - }); + ->map(function ($s) { + $s['state'] = (bool) config_cache($s['key']); + + return $s; + }); } public function getUsers(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); + $this->validate($request, [ 'sort' => 'sometimes|in:asc,desc', ]); $q = $request->input('q'); $sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc'; $res = User::whereNull('status') - ->when($q, function($query, $q) { - return $query->where('username', 'like', '%' . $q . '%'); + ->when($q, function ($query, $q) { + return $query->where('username', 'like', '%'.$q.'%'); }) ->orderBy('id', $sort) ->cursorPaginate(10); + return AdminUser::collection($res); } public function getUser(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); $id = $request->input('user_id'); - $key = 'pf-admin-api:getUser:byId:' . $id; - if($request->has('refresh')) { + $key = 'pf-admin-api:getUser:byId:'.$id; + if ($request->has('refresh')) { Cache::forget($key); } - return Cache::remember($key, 86400, function() use($id) { + + return Cache::remember($key, 86400, function () use ($id) { $user = User::findOrFail($id); $profile = $user->profile; $account = AccountService::get($user->profile_id, true); @@ -487,8 +521,8 @@ class AdminApiController extends Controller 'moderation' => [ 'unlisted' => (bool) $profile->unlisted, 'cw' => (bool) $profile->cw, - 'no_autolink' => (bool) $profile->no_autolink - ] + 'no_autolink' => (bool) $profile->no_autolink, + ], ]]); return $res; @@ -497,13 +531,15 @@ class AdminApiController extends Controller public function userAdminAction(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'id' => 'required', 'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email,delete', - 'value' => 'sometimes' + 'value' => 'sometimes', ]); $id = $request->input('id'); @@ -513,8 +549,8 @@ class AdminApiController extends Controller abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts'); - if($action === 'delete') { - if(config('pixelfed.account_deletion') == false) { + if ($action === 'delete') { + if (config('pixelfed.account_deletion') == false) { abort(404); } @@ -542,7 +578,7 @@ class AdminApiController extends Controller PublicTimelineService::deleteByProfileId($profile->id); NetworkTimelineService::deleteByProfileId($profile->id); - if($profile->user_id) { + if ($profile->user_id) { DB::table('oauth_access_tokens')->whereUserId($user->id)->delete(); DB::table('oauth_auth_codes')->whereUserId($user->id)->delete(); $user->email = $user->id; @@ -561,11 +597,12 @@ class AdminApiController extends Controller AccountService::del($profile->id); DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high'); } + return [ 'status' => 200, 'msg' => 'deleted', ]; - } else if($action === 'refresh_stats') { + } elseif ($action === 'refresh_stats') { $profile->following_count = DB::table('followers')->whereProfileId($user->profile_id)->count(); $profile->followers_count = DB::table('followers')->whereFollowingId($user->profile_id)->count(); $statusCount = Status::whereProfileId($user->profile_id) @@ -575,7 +612,7 @@ class AdminApiController extends Controller ->count(); $profile->status_count = $statusCount; $profile->save(); - } else if($action === 'verify_email') { + } elseif ($action === 'verify_email') { $user->email_verified_at = now(); $user->save(); @@ -587,11 +624,11 @@ class AdminApiController extends Controller ->action('admin.user.moderate') ->metadata([ 'action' => 'Manually verified email address', - 'message' => 'Success!' + 'message' => 'Success!', ]) ->accessLevel('admin') ->save(); - } else if($action === 'unlisted') { + } elseif ($action === 'unlisted') { ModLogService::boot() ->objectUid($profile->id) ->objectId($profile->id) @@ -600,13 +637,13 @@ class AdminApiController extends Controller ->action('admin.user.moderate') ->metadata([ 'action' => $action, - 'message' => 'Success!' + 'message' => 'Success!', ]) ->accessLevel('admin') ->save(); - $profile->unlisted = !$profile->unlisted; + $profile->unlisted = ! $profile->unlisted; $profile->save(); - } else if($action === 'cw') { + } elseif ($action === 'cw') { ModLogService::boot() ->objectUid($profile->id) ->objectId($profile->id) @@ -615,13 +652,13 @@ class AdminApiController extends Controller ->action('admin.user.moderate') ->metadata([ 'action' => $action, - 'message' => 'Success!' + 'message' => 'Success!', ]) ->accessLevel('admin') ->save(); - $profile->cw = !$profile->cw; + $profile->cw = ! $profile->cw; $profile->save(); - } else if($action === 'no_autolink') { + } elseif ($action === 'no_autolink') { ModLogService::boot() ->objectUid($profile->id) ->objectId($profile->id) @@ -630,11 +667,11 @@ class AdminApiController extends Controller ->action('admin.user.moderate') ->metadata([ 'action' => $action, - 'message' => 'Success!' + 'message' => 'Success!', ]) ->accessLevel('admin') ->save(); - $profile->no_autolink = !$profile->no_autolink; + $profile->no_autolink = ! $profile->no_autolink; $profile->save(); } else { $profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN); @@ -648,7 +685,7 @@ class AdminApiController extends Controller ->action('admin.user.moderate') ->metadata([ 'action' => $action, - 'message' => 'Success!' + 'message' => 'Success!', ]) ->accessLevel('admin') ->save(); @@ -662,15 +699,17 @@ class AdminApiController extends Controller 'moderation' => [ 'unlisted' => (bool) $profile->unlisted, 'cw' => (bool) $profile->cw, - 'no_autolink' => (bool) $profile->no_autolink - ] + 'no_autolink' => (bool) $profile->no_autolink, + ], ]]); } public function instances(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'q' => 'sometimes', @@ -684,19 +723,19 @@ class AdminApiController extends Controller $sortBy = $request->input('sort_by', 'id'); $filter = $request->input('filter'); - $res = Instance::when($q, function($query, $q) { - return $query->where('domain', 'like', '%' . $q . '%'); - }) - ->when($filter, function($query, $filter) { - if($filter === 'all') { + $res = Instance::when($q, function ($query, $q) { + return $query->where('domain', 'like', '%'.$q.'%'); + }) + ->when($filter, function ($query, $filter) { + if ($filter === 'all') { return $query; } else { return $query->where($filter, true); } }) - ->when($sortBy, function($query, $sortBy) use($sort) { + ->when($sortBy, function ($query, $sortBy) use ($sort) { return $query->orderBy($sortBy, $sort); - }, function($query) { + }, function ($query) { return $query->orderBy('id', 'desc'); }) ->cursorPaginate(10) @@ -707,8 +746,10 @@ class AdminApiController extends Controller public function getInstance(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); $id = $request->input('id'); $res = Instance::findOrFail($id); @@ -718,13 +759,15 @@ class AdminApiController extends Controller public function moderateInstance(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'id' => 'required', 'key' => 'required|in:unlisted,auto_cw,banned', - 'value' => 'required' + 'value' => 'required', ]); $id = $request->input('id'); @@ -742,8 +785,10 @@ class AdminApiController extends Controller public function refreshInstanceStats(Request $request) { - abort_if(!$request->user(), 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'id' => 'required', @@ -760,49 +805,51 @@ class AdminApiController extends Controller public function getAllStats(Request $request) { - abort_if(!$request->user(), 404); - abort_unless($request->user()->is_admin === 1, 404); + abort_if(! $request->user() || ! $request->user()->token(), 404); - if($request->has('refresh')) { + abort_unless($request->user()->is_admin === 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); + + if ($request->has('refresh')) { Cache::forget('admin-api:instance-all-stats-v1'); } - return Cache::remember('admin-api:instance-all-stats-v1', 1209600, function() { + return Cache::remember('admin-api:instance-all-stats-v1', 1209600, function () { $days = range(1, 7); $res = [ 'cached_at' => now()->format('c'), ]; $minStatusId = SnowflakeService::byDate(now()->subDays(7)); - foreach($days as $day) { + foreach ($days as $day) { $label = now()->subDays($day)->format('D'); $labelShort = substr($label, 0, 1); $res['users']['days'][] = [ 'date' => now()->subDays($day)->format('M j Y'), 'label_full' => $label, 'label' => $labelShort, - 'count' => User::whereDate('created_at', now()->subDays($day))->count() + 'count' => User::whereDate('created_at', now()->subDays($day))->count(), ]; $res['posts']['days'][] = [ 'date' => now()->subDays($day)->format('M j Y'), 'label_full' => $label, 'label' => $labelShort, - 'count' => Status::whereNull('uri')->where('id', '>', $minStatusId)->whereDate('created_at', now()->subDays($day))->count() + 'count' => Status::whereNull('uri')->where('id', '>', $minStatusId)->whereDate('created_at', now()->subDays($day))->count(), ]; $res['instances']['days'][] = [ 'date' => now()->subDays($day)->format('M j Y'), 'label_full' => $label, 'label' => $labelShort, - 'count' => Instance::whereDate('created_at', now()->subDays($day))->count() + 'count' => Instance::whereDate('created_at', now()->subDays($day))->count(), ]; } $res['users']['total'] = DB::table('users')->count(); $res['users']['min'] = collect($res['users']['days'])->min('count'); $res['users']['max'] = collect($res['users']['days'])->max('count'); - $res['users']['change'] = collect($res['users']['days'])->sum('count');; + $res['users']['change'] = collect($res['users']['days'])->sum('count'); $res['posts']['total'] = DB::table('statuses')->whereNull('uri')->count(); $res['posts']['min'] = collect($res['posts']['days'])->min('count'); $res['posts']['max'] = collect($res['posts']['days'])->max('count'); diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php new file mode 100644 index 000000000..d8ba76668 --- /dev/null +++ b/app/Http/Controllers/Api/ApiController.php @@ -0,0 +1,38 @@ +json($res, $code, $this->filterHeaders($headers), JSON_UNESCAPED_SLASHES); + } + + public function linksForCollection($paginator) { + $link = null; + + if ($paginator->onFirstPage()) { + if ($paginator->hasMorePages()) { + $link = '<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } else { + if ($paginator->previousPageUrl()) { + $link = '<'.$paginator->previousPageUrl().'>; rel="next"'; + } + + if ($paginator->hasMorePages()) { + $link .= ($link ? ', ' : '').'<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } + + return $link; + } + + private function filterHeaders($headers) { + return array_filter($headers, function($v, $k) { + return $v != null; + }, ARRAY_FILTER_USE_BOTH); + } +} diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 9e8f7ef23..3a309145c 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2,3805 +2,4349 @@ namespace App\Http\Controllers\Api; -use Illuminate\Http\Request; -use App\Http\Controllers\Controller; -use Illuminate\Support\Str; -use App\Util\ActivityPub\Helpers; -use App\Util\Media\Filter; -use Laravel\Passport\Passport; -use Auth, Cache, DB, Storage, URL; -use Illuminate\Support\Facades\Redis; -use App\{ - Avatar, - Bookmark, - Collection, - CollectionItem, - DirectMessage, - Follower, - FollowRequest, - Hashtag, - HashtagFollow, - Instance, - Like, - Media, - Notification, - Profile, - Status, - StatusHashtag, - User, - UserSetting, - UserFilter, -}; -use League\Fractal; -use App\Transformer\Api\Mastodon\v1\{ - AccountTransformer, - MediaTransformer, - NotificationTransformer, - StatusTransformer, -}; -use App\Transformer\Api\{ - RelationshipTransformer, -}; -use App\Http\Controllers\FollowerController; -use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use App\Avatar; +use App\Bookmark; +use App\Collection; +use App\CollectionItem; +use App\DirectMessage; +use App\Follower; +use App\FollowRequest; +use App\Hashtag; use App\Http\Controllers\AccountController; +use App\Http\Controllers\Controller; +use App\Http\Controllers\FollowerController; use App\Http\Controllers\StatusController; - +use App\Instance; use App\Jobs\AvatarPipeline\AvatarOptimize; use App\Jobs\CommentPipeline\CommentPipeline; +use App\Jobs\FollowPipeline\FollowAcceptPipeline; +use App\Jobs\FollowPipeline\FollowPipeline; +use App\Jobs\FollowPipeline\FollowRejectPipeline; +use App\Jobs\FollowPipeline\UnfollowPipeline; +use App\Jobs\HomeFeedPipeline\FeedWarmCachePipeline; +use App\Jobs\ImageOptimizePipeline\ImageOptimize; use App\Jobs\LikePipeline\LikePipeline; use App\Jobs\MediaPipeline\MediaDeletePipeline; +use App\Jobs\MediaPipeline\MediaSyncLicensePipeline; use App\Jobs\SharePipeline\SharePipeline; use App\Jobs\SharePipeline\UndoSharePipeline; use App\Jobs\StatusPipeline\NewStatusPipeline; use App\Jobs\StatusPipeline\StatusDelete; -use App\Jobs\FollowPipeline\FollowPipeline; -use App\Jobs\FollowPipeline\UnfollowPipeline; -use App\Jobs\ImageOptimizePipeline\ImageOptimize; -use App\Jobs\VideoPipeline\{ - VideoOptimize, - VideoPostProcess, - VideoThumbnail -}; - -use App\Services\{ - AccountService, - BookmarkService, - BouncerService, - CollectionService, - FollowerService, - HashtagService, - InstanceService, - LikeService, - NetworkTimelineService, - NotificationService, - MediaService, - MediaPathService, - ProfileStatusService, - PublicTimelineService, - ReblogService, - RelationshipService, - SearchApiV2Service, - StatusService, - MediaBlocklistService, - SnowflakeService, - UserFilterService -}; +use App\Jobs\VideoPipeline\VideoThumbnail; +use App\Like; +use App\Media; +use App\Models\Conversation; +use App\Notification; +use App\Profile; +use App\Services\AccountService; +use App\Services\AdminShadowFilterService; +use App\Services\BookmarkService; +use App\Services\BouncerService; +use App\Services\CollectionService; +use App\Services\CustomEmojiService; +use App\Services\DiscoverService; +use App\Services\FollowerService; +use App\Services\HomeTimelineService; +use App\Services\InstanceService; +use App\Services\LikeService; +use App\Services\MarkerService; +use App\Services\MediaBlocklistService; +use App\Services\MediaPathService; +use App\Services\MediaService; +use App\Services\NetworkTimelineService; +use App\Services\NotificationService; +use App\Services\PublicTimelineService; +use App\Services\ReblogService; +use App\Services\RelationshipService; +use App\Services\SnowflakeService; +use App\Services\StatusService; +use App\Services\UserFilterService; +use App\Services\UserRoleService; +use App\Services\UserStorageService; +use App\Status; +use App\StatusHashtag; +use App\Transformer\Api\Mastodon\v1\AccountTransformer; +use App\Transformer\Api\Mastodon\v1\MediaTransformer; +use App\Transformer\Api\Mastodon\v1\NotificationTransformer; +use App\Transformer\Api\Mastodon\v1\StatusTransformer; +use App\Transformer\Api\RelationshipTransformer; +use App\User; +use App\UserFilter; +use App\UserSetting; use App\Util\Lexer\Autolink; use App\Util\Lexer\PrettyNumber; use App\Util\Localization\Localization; +use App\Util\Media\Filter; use App\Util\Media\License; -use App\Jobs\MediaPipeline\MediaSyncLicensePipeline; -use App\Services\DiscoverService; -use App\Services\CustomEmojiService; -use App\Services\MarkerService; -use App\Models\Conversation; -use App\Jobs\FollowPipeline\FollowAcceptPipeline; -use App\Jobs\FollowPipeline\FollowRejectPipeline; -use Illuminate\Support\Facades\RateLimiter; -use Purify; +use Cache; use Carbon\Carbon; -use App\Http\Resources\MastoApi\FollowedTagResource; +use DB; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Support\Str; +use Laravel\Passport\Passport; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; +use Purify; +use Storage; class ApiV1Controller extends Controller { - protected $fractal; - const PF_API_ENTITY_KEY = "_pe"; - - public function __construct() - { - $this->fractal = new Fractal\Manager(); - $this->fractal->setSerializer(new ArraySerializer()); - } - - public function json($res, $code = 200, $headers = []) - { - return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); - } - - public function getApp(Request $request) - { - if(!$request->user()) { - return response('', 403); - } - - $client = $request->user()->token()->client; - $res = [ - 'name' => $client->name, - 'website' => null, - 'vapid_key' => null - ]; - - return $this->json($res); - } - - public function apps(Request $request) - { - abort_if(!config_cache('pixelfed.oauth_enabled'), 404); - - $this->validate($request, [ - 'client_name' => 'required', - 'redirect_uris' => 'required' - ]); - - $uris = implode(',', explode('\n', $request->redirect_uris)); - - $client = Passport::client()->forceFill([ - 'user_id' => null, - 'name' => e($request->client_name), - 'secret' => Str::random(40), - 'redirect' => $uris, - 'personal_access_client' => false, - 'password_client' => false, - 'revoked' => false, - ]); - - $client->save(); - - $res = [ - 'id' => (string) $client->id, - 'name' => $client->name, - 'website' => null, - 'redirect_uri' => $client->redirect, - 'client_id' => (string) $client->id, - 'client_secret' => $client->secret, - 'vapid_key' => null - ]; - - return $this->json($res, 200, [ - 'Access-Control-Allow-Origin' => '*' - ]); - } - - /** - * GET /api/v1/accounts/verify_credentials - * - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function verifyCredentials(Request $request) - { - $user = $request->user(); - - abort_if(!$user, 403); - abort_if($user->status != null, 403); - - $res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($user->profile_id) : AccountService::getMastodon($user->profile_id); - - $res['source'] = [ - 'privacy' => $res['locked'] ? 'private' : 'public', - 'sensitive' => false, - 'language' => $user->language ?? 'en', - 'note' => strip_tags($res['note']), - 'fields' => [] - ]; - - return $this->json($res); - } - - /** - * GET /api/v1/accounts/{id} - * - * @param integer $id - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountById(Request $request, $id) - { - $res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($id, true) : AccountService::getMastodon($id, true); - if(!$res) { - return response()->json(['error' => 'Record not found'], 404); - } - return $this->json($res); - } - - /** - * PATCH /api/v1/accounts/update_credentials - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountUpdateCredentials(Request $request) - { - abort_if(!$request->user(), 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_api')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $this->validate($request, [ - 'avatar' => 'sometimes|mimetypes:image/jpeg,image/png|max:' . config('pixelfed.max_avatar_size'), - 'display_name' => 'nullable|string|max:30', - 'note' => 'nullable|string|max:200', - 'locked' => 'nullable', - 'website' => 'nullable|string|max:120', - // 'source.privacy' => 'nullable|in:unlisted,public,private', - // 'source.sensitive' => 'nullable|boolean' - ], [ - 'required' => 'The :attribute field is required.', - 'avatar.mimetypes' => 'The file must be in jpeg or png format', - 'avatar.max' => 'The :attribute exceeds the file size limit of ' . PrettyNumber::size(config('pixelfed.max_avatar_size'), true, false), - ]); - - $user = $request->user(); - $profile = $user->profile; - $settings = $user->settings; - - $changes = false; - $other = array_merge(AccountService::defaultSettings()['other'], $settings->other ?? []); - $syncLicenses = false; - $licenseChanged = false; - $composeSettings = array_merge(AccountService::defaultSettings()['compose_settings'], $settings->compose_settings ?? []); - - if($request->has('avatar')) { - $av = Avatar::whereProfileId($profile->id)->first(); - if($av) { - $currentAvatar = storage_path('app/'.$av->media_path); - $file = $request->file('avatar'); - $path = "public/avatars/{$profile->id}"; - $name = strtolower(str_random(6)). '.' . $file->guessExtension(); - $request->file('avatar')->storePubliclyAs($path, $name); - $av->media_path = "{$path}/{$name}"; - $av->save(); - Cache::forget("avatar:{$profile->id}"); - Cache::forget('user:account:id:'.$user->id); - AvatarOptimize::dispatch($user->profile, $currentAvatar); - } - $changes = true; - } - - if($request->has('source[language]')) { - $lang = $request->input('source[language]'); - if(in_array($lang, Localization::languages())) { - $user->language = $lang; - $changes = true; - $other['language'] = $lang; - } - } - - if($request->has('website')) { - $website = $request->input('website'); - if($website != $profile->website) { - if($website) { - if(!strpos($website, '.')) { - $website = null; - } - - if($website && !strpos($website, '://')) { - $website = 'https://' . $website; - } - - $host = parse_url($website, PHP_URL_HOST); - - $bannedInstances = InstanceService::getBannedDomains(); - if(in_array($host, $bannedInstances)) { - $website = null; - } - } - $profile->website = $website ? $website : null; - $changes = true; - } - } - - if($request->has('display_name')) { - $displayName = $request->input('display_name'); - if($displayName !== $user->name) { - $user->name = $displayName; - $profile->name = $displayName; - $changes = true; - } - } - - if($request->has('note')) { - $note = $request->input('note'); - if($note !== strip_tags($profile->bio)) { - $profile->bio = Autolink::create()->autolink(strip_tags($note)); - $changes = true; - } - } - - if($request->has('locked')) { - $locked = $request->input('locked') == 'true'; - if($profile->is_private != $locked) { - $profile->is_private = $locked; - $changes = true; - } - } - - if($request->has('reduce_motion')) { - $reduced = $request->input('reduce_motion'); - if($settings->reduce_motion != $reduced) { - $settings->reduce_motion = $reduced; - $changes = true; - } - } - - if($request->has('high_contrast_mode')) { - $contrast = $request->input('high_contrast_mode'); - if($settings->high_contrast_mode != $contrast) { - $settings->high_contrast_mode = $contrast; - $changes = true; - } - } - - if($request->has('video_autoplay')) { - $autoplay = $request->input('video_autoplay'); - if($settings->video_autoplay != $autoplay) { - $settings->video_autoplay = $autoplay; - $changes = true; - } - } - - if($request->has('license')) { - $license = $request->input('license'); - abort_if(!in_array($license, License::keys()), 422, 'Invalid media license id'); - $syncLicenses = $request->input('sync_licenses') == true; - abort_if($syncLicenses && Cache::get('pf:settings:mls_recently:'.$user->id) == 2, 422, 'You can only sync licenses twice per 24 hours'); - if($composeSettings['default_license'] != $license) { - $composeSettings['default_license'] = $license; - $licenseChanged = true; - $changes = true; - } - } - - if($request->has('media_descriptions')) { - $md = $request->input('media_descriptions') == true; - if($composeSettings['media_descriptions'] != $md) { - $composeSettings['media_descriptions'] = $md; - $changes = true; - } - } - - if($request->has('crawlable')) { - $crawlable = $request->input('crawlable'); - if($settings->crawlable != $crawlable) { - $settings->crawlable = $crawlable; - $changes = true; - } - } - - if($request->has('show_profile_follower_count')) { - $show_profile_follower_count = $request->input('show_profile_follower_count'); - if($settings->show_profile_follower_count != $show_profile_follower_count) { - $settings->show_profile_follower_count = $show_profile_follower_count; - $changes = true; - } - } - - if($request->has('show_profile_following_count')) { - $show_profile_following_count = $request->input('show_profile_following_count'); - if($settings->show_profile_following_count != $show_profile_following_count) { - $settings->show_profile_following_count = $show_profile_following_count; - $changes = true; - } - } - - if($request->has('public_dm')) { - $public_dm = $request->input('public_dm'); - if($settings->public_dm != $public_dm) { - $settings->public_dm = $public_dm; - $changes = true; - } - } - - if($request->has('source[privacy]')) { - $scope = $request->input('source[privacy]'); - if(in_array($scope, ['public', 'private', 'unlisted'])) { - if($composeSettings['default_scope'] != $scope) { - $composeSettings['default_scope'] = $profile->is_private ? 'private' : $scope; - $changes = true; - } - } - } - - if($request->has('disable_embeds')) { - $disabledEmbeds = $request->input('disable_embeds'); - if($other['disable_embeds'] != $disabledEmbeds) { - $other['disable_embeds'] = $disabledEmbeds; - $changes = true; - } - } - - if($changes) { - $settings->other = $other; - $settings->compose_settings = $composeSettings; - $settings->save(); - $user->save(); - $profile->save(); - Cache::forget('profile:settings:' . $profile->id); - Cache::forget('user:account:id:' . $profile->user_id); - Cache::forget('profile:follower_count:' . $profile->id); - Cache::forget('profile:following_count:' . $profile->id); - Cache::forget('profile:embed:' . $profile->id); - Cache::forget('profile:compose:settings:' . $user->id); - Cache::forget('profile:view:'.$user->username); - AccountService::del($user->profile_id); - } - - if($syncLicenses && $licenseChanged) { - $key = 'pf:settings:mls_recently:'.$user->id; - $val = Cache::has($key) ? 2 : 1; - Cache::put($key, $val, 86400); - MediaSyncLicensePipeline::dispatch($user->id, $request->input('license')); - } - - if($request->has(self::PF_API_ENTITY_KEY)) { - $res = AccountService::get($user->profile_id, true); - } else { - $res = AccountService::getMastodon($user->profile_id, true); - $res['bio'] = strip_tags($res['note']); - $res = array_merge($res, $other); - } - - return $this->json($res); - } - - /** - * GET /api/v1/accounts/{id}/followers - * - * @param integer $id - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountFollowersById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $account = AccountService::get($id); - abort_if(!$account, 404); - $pid = $request->user()->profile_id; - $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:80' - ]); - $limit = $request->input('limit', 10); - $napi = $request->has(self::PF_API_ENTITY_KEY); - - if(intval($pid) !== intval($account['id'])) { - if($account['locked']) { - if(!FollowerService::follows($pid, $account['id'])) { - return []; - } - } - - if(AccountService::hiddenFollowers($id)) { - return []; - } - - if($request->has('page') && $request->user()->is_admin == false) { - $page = (int) $request->input('page'); - if(($page * $limit) >= 100) { - return []; - } - } - } - if($request->has('page')) { - $res = DB::table('followers') - ->select('id', 'profile_id', 'following_id') - ->whereFollowingId($account['id']) - ->orderByDesc('id') - ->simplePaginate($limit) - ->map(function($follower) use($napi) { - return $napi ? AccountService::get($follower->profile_id, true) : AccountService::getMastodon($follower->profile_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->toArray(); - - return $this->json($res); - } - - $paginator = DB::table('followers') - ->select('id', 'profile_id', 'following_id') - ->whereFollowingId($account['id']) - ->orderByDesc('id') - ->cursorPaginate($limit) - ->withQueryString(); - - $link = null; - - if($paginator->onFirstPage()) { - if($paginator->hasMorePages()) { - $link = '<'.$paginator->nextPageUrl().'>; rel="prev"'; - } - } else { - if($paginator->previousPageUrl()) { - $link = '<'.$paginator->previousPageUrl().'>; rel="next"'; - } - - if($paginator->hasMorePages()) { - $link .= ($link ? ', ' : '') . '<'.$paginator->nextPageUrl().'>; rel="prev"'; - } - } - - $res = $paginator->map(function($follower) use($napi) { - return $napi ? AccountService::get($follower->profile_id, true) : AccountService::getMastodon($follower->profile_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->toArray(); - - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res, 200, $headers); - } - - /** - * GET /api/v1/accounts/{id}/following - * - * @param integer $id - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountFollowingById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $account = AccountService::get($id); - abort_if(!$account, 404); - $pid = $request->user()->profile_id; - $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:80' - ]); - $limit = $request->input('limit', 10); - $napi = $request->has(self::PF_API_ENTITY_KEY); - - if(intval($pid) !== intval($account['id'])) { - if($account['locked']) { - if(!FollowerService::follows($pid, $account['id'])) { - return []; - } - } - - if(AccountService::hiddenFollowing($id)) { - return []; - } - - if($request->has('page') && $request->user()->is_admin == false) { - $page = (int) $request->input('page'); - if(($page * $limit) >= 100) { - return []; - } - } - } - - if($request->has('page')) { - $res = DB::table('followers') - ->select('id', 'profile_id', 'following_id') - ->whereProfileId($account['id']) - ->orderByDesc('id') - ->simplePaginate($limit) - ->map(function($follower) use($napi) { - return $napi ? AccountService::get($follower->following_id, true) : AccountService::getMastodon($follower->following_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->toArray(); - return $this->json($res); - } - - $paginator = DB::table('followers') - ->select('id', 'profile_id', 'following_id') - ->whereProfileId($account['id']) - ->orderByDesc('id') - ->cursorPaginate($limit) - ->withQueryString(); - - $link = null; - - if($paginator->onFirstPage()) { - if($paginator->hasMorePages()) { - $link = '<'.$paginator->nextPageUrl().'>; rel="prev"'; - } - } else { - if($paginator->previousPageUrl()) { - $link = '<'.$paginator->previousPageUrl().'>; rel="next"'; - } - - if($paginator->hasMorePages()) { - $link .= ($link ? ', ' : '') . '<'.$paginator->nextPageUrl().'>; rel="prev"'; - } - } - - $res = $paginator->map(function($follower) use($napi) { - return $napi ? AccountService::get($follower->following_id, true) : AccountService::getMastodon($follower->following_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->toArray(); - - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res, 200, $headers); - } - - /** - * GET /api/v1/accounts/{id}/statuses - * - * @param integer $id - * - * @return \App\Transformer\Api\StatusTransformer - */ - public function accountStatusesById(Request $request, $id) - { - $user = $request->user(); - - $this->validate($request, [ - 'only_media' => 'nullable', - 'media_type' => 'sometimes|string|in:photo,video', - 'pinned' => 'nullable', - 'exclude_replies' => 'nullable', - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|min:1|max:100' - ]); - - $napi = $request->has(self::PF_API_ENTITY_KEY); - $profile = $napi ? AccountService::get($id, true) : AccountService::getMastodon($id, true); - - if(!$profile || !isset($profile['id']) || !$user) { - return $this->json(['error' => 'Account not found'], 404); + protected $fractal; + + const PF_API_ENTITY_KEY = '_pe'; + + public function __construct() + { + $this->fractal = new Fractal\Manager; + $this->fractal->setSerializer(new ArraySerializer); + } + + public function json($res, $code = 200, $headers = []) + { + return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); + } + + /** + * GET /api/v1/apps/verify_credentials + */ + public function getApp(Request $request) + { + // FIXME: /api/v1/apps/verify_credentials should be accessible with any + // valid Access Token, not just a user's access token (i.e., client + // credentails grant flow access tokens) + abort_if(! $request->user() || ! $request->user()->token(), 403); + + $client = $request->user()->token()->client; + $res = [ + 'name' => $client->name, + 'website' => null, + 'vapid_key' => null, + ]; + + return $this->json($res); + } + + /** + * POST /api/v1/apps + */ + public function apps(Request $request) + { + abort_if(! (bool) config_cache('pixelfed.oauth_enabled'), 404); + + $this->validate($request, [ + 'client_name' => 'required', + 'redirect_uris' => 'required', + ]); + + $uris = implode(',', explode('\n', $request->redirect_uris)); + + $client = Passport::client()->forceFill([ + 'user_id' => null, + 'name' => e($request->client_name), + 'secret' => Str::random(40), + 'redirect' => $uris, + 'personal_access_client' => false, + 'password_client' => false, + 'revoked' => false, + ]); + + $client->save(); + + $res = [ + 'id' => (string) $client->id, + 'name' => $client->name, + 'website' => null, + 'redirect_uri' => $client->redirect, + 'client_id' => (string) $client->id, + 'client_secret' => $client->secret, + 'vapid_key' => null, + ]; + + return $this->json($res, 200, [ + 'Access-Control-Allow-Origin' => '*', + ]); + } + + /** + * GET /api/v1/accounts/verify_credentials + * + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function verifyCredentials(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $user = $request->user(); + + abort_if($user->status != null, 403); + AccountService::setLastActive($user->id); + + $res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($user->profile_id) : AccountService::getMastodon($user->profile_id); + + $res['source'] = [ + 'privacy' => $res['locked'] ? 'private' : 'public', + 'sensitive' => false, + 'language' => $user->language ?? 'en', + 'note' => strip_tags($res['note']), + 'fields' => [], + ]; + + if ($request->has(self::PF_API_ENTITY_KEY)) { + $res['settings'] = AccountService::getAccountSettings($user->profile_id); } - $limit = $request->limit ?? 20; - $max_id = $request->max_id; - $min_id = $request->min_id; + return $this->json($res); + } - if(!$max_id && !$min_id) { - $min_id = 1; - } + /** + * GET /api/v1/accounts/{id} + * + * @param int $id + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - $pid = $request->user()->profile_id; - $scope = $request->only_media == true ? - ['photo', 'photo:album', 'video', 'video:album'] : - ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply']; + $withInstanceMeta = $request->has('_wim'); + $res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($id, true) : AccountService::getMastodon($id, true); + if (! $res) { + return response()->json(['error' => 'Record not found'], 404); + } + if ($res && strpos($res['acct'], '@') != -1) { + $domain = parse_url($res['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } - if($request->only_media && $request->has('media_type')) { - $mt = $request->input('media_type'); - if($mt == 'video') { - $scope = ['video', 'video:album']; - } - } + return $this->json($res); + } - if(intval($pid) === intval($profile['id'])) { - $visibility = ['public', 'unlisted', 'private']; - } else if($profile['locked']) { - $following = FollowerService::follows($pid, $profile['id']); - if(!$following) { - return response('', 403); - } - $visibility = ['public', 'unlisted', 'private']; - } else { - $following = FollowerService::follows($pid, $profile['id']); - $visibility = $following ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; - } + /** + * PATCH /api/v1/accounts/update_credentials + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountUpdateCredentials(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); - $dir = $min_id ? '>' : '<'; - $id = $min_id ?? $max_id; - $res = Status::whereProfileId($profile['id']) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->whereIn('type', $scope) - ->where('id', $dir, $id) - ->whereIn('scope', $visibility) - ->limit($limit) - ->orderByDesc('id') - ->get() - ->map(function($s) use($user, $napi, $profile) { - try { - $status = $napi ? StatusService::get($s->id, false) : StatusService::getMastodon($s->id, false); - } catch (\Exception $e) { - return false; + if (config('pixelfed.bouncer.cloud_ips.ban_api')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $this->validate($request, [ + 'avatar' => 'sometimes|mimetypes:image/jpeg,image/png|max:'.config('pixelfed.max_avatar_size'), + 'display_name' => 'nullable|string|max:30', + 'note' => 'nullable|string|max:200', + 'locked' => 'nullable', + 'website' => 'nullable|string|max:120', + // 'source.privacy' => 'nullable|in:unlisted,public,private', + // 'source.sensitive' => 'nullable|boolean' + ], [ + 'required' => 'The :attribute field is required.', + 'avatar.mimetypes' => 'The file must be in jpeg or png format', + 'avatar.max' => 'The :attribute exceeds the file size limit of '.PrettyNumber::size(config('pixelfed.max_avatar_size'), true, false), + ]); + + $user = $request->user(); + AccountService::setLastActive($user->id); + $profile = $user->profile; + $settings = $user->settings; + + $changes = false; + $other = array_merge(AccountService::defaultSettings()['other'], $settings->other ?? []); + $syncLicenses = false; + $licenseChanged = false; + $composeSettings = array_merge(AccountService::defaultSettings()['compose_settings'], $settings->compose_settings ?? []); + + if ($request->has('avatar')) { + $av = Avatar::whereProfileId($profile->id)->first(); + if ($av) { + $currentAvatar = storage_path('app/'.$av->media_path); + $file = $request->file('avatar'); + $path = "public/avatars/{$profile->id}"; + $name = strtolower(str_random(6)).'.'.$file->guessExtension(); + $request->file('avatar')->storePubliclyAs($path, $name); + $av->media_path = "{$path}/{$name}"; + $av->save(); + Cache::forget("avatar:{$profile->id}"); + Cache::forget('user:account:id:'.$user->id); + AvatarOptimize::dispatch($user->profile, $currentAvatar); + } + $changes = true; + } + + if ($request->has('source[language]')) { + $lang = $request->input('source[language]'); + if (in_array($lang, Localization::languages())) { + $user->language = $lang; + $changes = true; + $other['language'] = $lang; + } + } + + if ($request->has('website')) { + $website = $request->input('website'); + if ($website != $profile->website) { + if ($website) { + if (! strpos($website, '.')) { + $website = null; + } + + if ($website && ! strpos($website, '://')) { + $website = 'https://'.$website; + } + + $host = parse_url($website, PHP_URL_HOST); + + $bannedInstances = InstanceService::getBannedDomains(); + if (in_array($host, $bannedInstances)) { + $website = null; + } + } + $profile->website = $website ? $website : null; + $changes = true; + } + } + + if ($request->has('display_name')) { + $displayName = $request->input('display_name'); + if ($displayName !== $user->name) { + $user->name = $displayName; + $profile->name = $displayName; + $changes = true; + } + } + + if ($request->has('note')) { + $note = $request->input('note'); + if ($note !== strip_tags($profile->bio)) { + $profile->bio = Autolink::create()->autolink(strip_tags($note)); + $changes = true; + } + } + + if ($request->has('locked')) { + $locked = $request->boolean('locked'); + if ($profile->is_private != $locked) { + $profile->is_private = $locked; + $changes = true; + } + } + + if ($request->has('reduce_motion')) { + $reduced = $request->boolean('reduce_motion'); + if ($settings->reduce_motion != $reduced) { + $settings->reduce_motion = $reduced; + $changes = true; + } + } + + if ($request->has('high_contrast_mode')) { + $contrast = $request->boolean('high_contrast_mode'); + if ($settings->high_contrast_mode != $contrast) { + $settings->high_contrast_mode = $contrast; + $changes = true; + } + } + + if ($request->has('video_autoplay')) { + $autoplay = $request->boolean('video_autoplay'); + if ($settings->video_autoplay != $autoplay) { + $settings->video_autoplay = $autoplay; + $changes = true; + } + } + + if ($request->has('license')) { + $license = $request->input('license'); + abort_if(! in_array($license, License::keys()), 422, 'Invalid media license id'); + $syncLicenses = $request->input('sync_licenses') == true; + abort_if($syncLicenses && Cache::get('pf:settings:mls_recently:'.$user->id) == 2, 422, 'You can only sync licenses twice per 24 hours'); + if ($composeSettings['default_license'] != $license) { + $composeSettings['default_license'] = $license; + $licenseChanged = true; + $changes = true; + } + } + + if ($request->has('media_descriptions')) { + $md = $request->boolean('media_descriptions'); + if ($composeSettings['media_descriptions'] != $md) { + $composeSettings['media_descriptions'] = $md; + $changes = true; + } + } + + if ($request->has('crawlable')) { + $crawlable = $request->boolean('crawlable'); + if ($settings->crawlable != $crawlable) { + $settings->crawlable = $crawlable; + $changes = true; + } + } + + if ($request->has('show_profile_follower_count')) { + $show_profile_follower_count = $request->boolean('show_profile_follower_count'); + if ($settings->show_profile_follower_count != $show_profile_follower_count) { + $settings->show_profile_follower_count = $show_profile_follower_count; + $changes = true; + Cache::forget('pf:acct-trans:hideFollowers:'.$profile->id); + } + } + + if ($request->has('show_profile_following_count')) { + $show_profile_following_count = $request->boolean('show_profile_following_count'); + if ($settings->show_profile_following_count != $show_profile_following_count) { + $settings->show_profile_following_count = $show_profile_following_count; + $changes = true; + Cache::forget('pf:acct-trans:hideFollowing:'.$profile->id); + } + } + + if ($request->has('public_dm')) { + $public_dm = $request->boolean('public_dm'); + if ($settings->public_dm != $public_dm) { + $settings->public_dm = $public_dm; + $changes = true; + } + } + + if ($request->has('source[privacy]')) { + $scope = $request->input('source[privacy]'); + if (in_array($scope, ['public', 'private', 'unlisted'])) { + if ($composeSettings['default_scope'] != $scope) { + $composeSettings['default_scope'] = $profile->is_private ? 'private' : $scope; + $changes = true; + } + } + } + + if ($request->has('disable_embeds')) { + $disabledEmbeds = $request->boolean('disable_embeds'); + if ($other['disable_embeds'] != $disabledEmbeds) { + $other['disable_embeds'] = $disabledEmbeds; + $changes = true; + } + } + + if ($changes) { + $settings->other = $other; + $settings->compose_settings = $composeSettings; + $settings->save(); + $user->save(); + $profile->save(); + Cache::forget('profile:settings:'.$profile->id); + Cache::forget('user:account:id:'.$profile->user_id); + Cache::forget('profile:follower_count:'.$profile->id); + Cache::forget('profile:following_count:'.$profile->id); + Cache::forget('profile:embed:'.$profile->id); + Cache::forget('profile:compose:settings:'.$user->id); + Cache::forget('profile:view:'.$profile->username); + Cache::forget('profile:atom:enabled:'.$profile->id); + Cache::forget('pfc:cached-user:wt:'.strtolower($profile->username)); + Cache::forget('pfc:cached-user:wot:'.strtolower($profile->username)); + Cache::forget('pf:acct:settings:hidden-followers:'.$profile->id); + Cache::forget('pf:acct:settings:hidden-following:'.$profile->id); + Cache::forget('pf:acct-trans:hideFollowing:'.$profile->id); + Cache::forget('pf:acct-trans:hideFollowers:'.$profile->id); + AccountService::del($user->profile_id); + AccountService::forgetAccountSettings($profile->id); + } + + if ($syncLicenses && $licenseChanged) { + $key = 'pf:settings:mls_recently:'.$user->id; + $val = Cache::has($key) ? 2 : 1; + Cache::put($key, $val, 86400); + MediaSyncLicensePipeline::dispatch($user->id, $request->input('license')); + } + + if ($request->has(self::PF_API_ENTITY_KEY)) { + $res = AccountService::get($user->profile_id, true); + } else { + $res = AccountService::getMastodon($user->profile_id, true); + $res['bio'] = strip_tags($res['note']); + $res = array_merge($res, $other); + } + + return $this->json($res); + } + + /** + * GET /api/v1/accounts/{id}/followers + * + * @param int $id + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountFollowersById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $account = AccountService::get($id); + abort_if(! $account, 404); + abort_if(isset($account['moved'], $account['moved']['id']), 404, 'Account moved'); + $pid = $request->user()->profile_id; + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1', + ]); + $limit = $request->input('limit', 10); + if ($limit > 80) { + $limit = 80; + } + $napi = $request->has(self::PF_API_ENTITY_KEY); + + if ($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + + if (intval($pid) !== intval($account['id'])) { + if ($account['locked']) { + if (! FollowerService::follows($pid, $account['id'])) { + return []; + } } - if($profile) { - $status['account'] = $profile; - } + if (AccountService::hiddenFollowers($id)) { + return []; + } - if($user && $status) { - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); - $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); - } - return $status; - }) - ->filter(function($s) { - return $s; - }) - ->values(); - - return $this->json($res); - } - - /** - * POST /api/v1/accounts/{id}/follow - * - * @param integer $id - * - * @return \App\Transformer\Api\RelationshipTransformer - */ - public function accountFollowById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - - $target = Profile::where('id', '!=', $user->profile_id) - ->whereNull('status') - ->findOrFail($id); - - $private = (bool) $target->is_private; - $remote = (bool) $target->domain; - $blocked = UserFilter::whereUserId($target->id) - ->whereFilterType('block') - ->whereFilterableId($user->profile_id) - ->whereFilterableType('App\Profile') - ->exists(); - - if($blocked == true) { - abort(400, 'You cannot follow this user.'); - } - - $isFollowing = Follower::whereProfileId($user->profile_id) - ->whereFollowingId($target->id) - ->exists(); - - // Following already, return empty relationship - if($isFollowing == true) { - $res = RelationshipService::get($user->profile_id, $target->id) ?? []; - return $this->json($res); - } - - // Rate limits, max 7500 followers per account - if($user->profile->following_count && $user->profile->following_count >= Follower::MAX_FOLLOWING) { - abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts'); - } - - if($private == true) { - $follow = FollowRequest::firstOrCreate([ - 'follower_id' => $user->profile_id, - 'following_id' => $target->id - ]); - if($remote == true && config('federation.activitypub.remoteFollow') == true) { - (new FollowerController())->sendFollow($user->profile, $target); - } - } else { - $follower = Follower::firstOrCreate([ - 'profile_id' => $user->profile_id, - 'following_id' => $target->id - ]); - - if($remote == true && config('federation.activitypub.remoteFollow') == true) { - (new FollowerController())->sendFollow($user->profile, $target); - } - FollowPipeline::dispatch($follower)->onQueue('high'); - } - - RelationshipService::refresh($user->profile_id, $target->id); - Cache::forget('profile:following:'.$target->id); - Cache::forget('profile:followers:'.$target->id); - Cache::forget('profile:following:'.$user->profile_id); - Cache::forget('profile:followers:'.$user->profile_id); - Cache::forget('api:local:exp:rec:'.$user->profile_id); - Cache::forget('user:account:id:'.$target->user_id); - Cache::forget('user:account:id:'.$user->id); - Cache::forget('profile:follower_count:'.$target->id); - Cache::forget('profile:follower_count:'.$user->profile_id); - Cache::forget('profile:following_count:'.$target->id); - Cache::forget('profile:following_count:'.$user->profile_id); - AccountService::del($user->profile_id); - AccountService::del($target->id); - - $res = RelationshipService::get($user->profile_id, $target->id); - - return $this->json($res); - } - - /** - * POST /api/v1/accounts/{id}/unfollow - * - * @param integer $id - * - * @return \App\Transformer\Api\RelationshipTransformer - */ - public function accountUnfollowById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - - $target = Profile::where('id', '!=', $user->profile_id) - ->whereNull('status') - ->findOrFail($id); - - $private = (bool) $target->is_private; - $remote = (bool) $target->domain; - - $isFollowing = Follower::whereProfileId($user->profile_id) - ->whereFollowingId($target->id) - ->exists(); - - if($isFollowing == false) { - $followRequest = FollowRequest::whereFollowerId($user->profile_id) - ->whereFollowingId($target->id) - ->first(); - if($followRequest) { - $followRequest->delete(); - RelationshipService::refresh($target->id, $user->profile_id); - } - $resource = new Fractal\Resource\Item($target, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - - return $this->json($res); - } - - Follower::whereProfileId($user->profile_id) - ->whereFollowingId($target->id) - ->delete(); - - UnfollowPipeline::dispatch($user->profile_id, $target->id)->onQueue('high'); - - if($remote == true && config('federation.activitypub.remoteFollow') == true) { - (new FollowerController())->sendUndoFollow($user->profile, $target); - } - - RelationshipService::refresh($user->profile_id, $target->id); - Cache::forget('profile:following:'.$target->id); - Cache::forget('profile:followers:'.$target->id); - Cache::forget('profile:following:'.$user->profile_id); - Cache::forget('profile:followers:'.$user->profile_id); - Cache::forget('api:local:exp:rec:'.$user->profile_id); - Cache::forget('user:account:id:'.$target->user_id); - Cache::forget('user:account:id:'.$user->id); - Cache::forget('profile:follower_count:'.$target->id); - Cache::forget('profile:follower_count:'.$user->profile_id); - Cache::forget('profile:following_count:'.$target->id); - Cache::forget('profile:following_count:'.$user->profile_id); - AccountService::del($user->profile_id); - AccountService::del($target->id); - - $res = RelationshipService::get($user->profile_id, $target->id); - - return $this->json($res); - } - - /** - * GET /api/v1/accounts/relationships - * - * @param array|integer $id - * - * @return \App\Services\RelationshipService - */ - public function accountRelationshipsById(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'id' => 'required|array|min:1|max:20', - 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX - ]); - $napi = $request->has(self::PF_API_ENTITY_KEY); - $pid = $request->user()->profile_id ?? $request->user()->profile->id; - $res = collect($request->input('id')) - ->filter(function($id) use($pid) { - return intval($id) !== intval($pid); - }) - ->map(function($id) use($pid, $napi) { - return $napi ? - RelationshipService::getWithDate($pid, $id) : - RelationshipService::get($pid, $id); - }); - return $this->json($res); - } - - /** - * GET /api/v1/accounts/search - * - * - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountSearch(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'q' => 'required|string|min:1|max:255', - 'limit' => 'nullable|integer|min:1|max:40', - 'resolve' => 'nullable' - ]); - - $user = $request->user(); - $query = $request->input('q'); - $limit = $request->input('limit') ?? 20; - $resolve = (bool) $request->input('resolve', false); - $q = '%' . $query . '%'; - - $profiles = Cache::remember('api:v1:accounts:search:' . sha1($query) . ':limit:' . $limit, 86400, function() use($q, $limit) { - return Profile::whereNull('status') - ->where('username', 'like', $q) - ->orWhere('name', 'like', $q) - ->limit($limit) - ->pluck('id') - ->map(function($id) { - return AccountService::getMastodon($id); + if ($request->has('page') && $request->user()->is_admin == false) { + $page = (int) $request->input('page'); + if (($page * $limit) >= 100) { + return []; + } + } + } + if ($request->has('page')) { + $res = DB::table('followers') + ->select('id', 'profile_id', 'following_id') + ->whereFollowingId($account['id']) + ->orderByDesc('id') + ->simplePaginate($limit) + ->map(function ($follower) use ($napi) { + return $napi ? AccountService::get($follower->profile_id, true) : AccountService::getMastodon($follower->profile_id, true); }) - ->filter(function($account) { + ->filter(function ($account) { return $account && isset($account['id']); - }); + }) + ->values() + ->toArray(); + + return $this->json($res); + } + + $paginator = DB::table('followers') + ->select('id', 'profile_id', 'following_id') + ->whereFollowingId($account['id']) + ->orderByDesc('id') + ->cursorPaginate($limit) + ->withQueryString(); + + $link = null; + + if ($paginator->onFirstPage()) { + if ($paginator->hasMorePages()) { + $link = '<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } else { + if ($paginator->previousPageUrl()) { + $link = '<'.$paginator->previousPageUrl().'>; rel="next"'; + } + + if ($paginator->hasMorePages()) { + $link .= ($link ? ', ' : '').'<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } + + $res = $paginator->map(function ($follower) use ($napi) { + return $napi ? AccountService::get($follower->profile_id, true) : AccountService::getMastodon($follower->profile_id, true); + }) + ->filter(function ($account) { + return $account && isset($account['id']); + }) + ->values() + ->toArray(); + + $headers = isset($link) ? ['Link' => $link] : []; + + return $this->json($res, 200, $headers); + } + + /** + * GET /api/v1/accounts/{id}/following + * + * @param int $id + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountFollowingById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $account = AccountService::get($id); + abort_if(! $account, 404); + abort_if(isset($account['moved'], $account['moved']['id']), 404, 'Account moved'); + $pid = $request->user()->profile_id; + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1', + ]); + $limit = $request->input('limit', 10); + if ($limit > 80) { + $limit = 80; + } + $napi = $request->has(self::PF_API_ENTITY_KEY); + + if ($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + + if (intval($pid) !== intval($account['id'])) { + if ($account['locked']) { + if (! FollowerService::follows($pid, $account['id'])) { + return []; + } + } + + if (AccountService::hiddenFollowing($id)) { + return []; + } + + if ($request->has('page') && $request->user()->is_admin == false) { + $page = (int) $request->input('page'); + if (($page * $limit) >= 100) { + return []; + } + } + } + + if ($request->has('page')) { + $res = DB::table('followers') + ->select('id', 'profile_id', 'following_id') + ->whereProfileId($account['id']) + ->orderByDesc('id') + ->simplePaginate($limit) + ->map(function ($follower) use ($napi) { + return $napi ? AccountService::get($follower->following_id, true) : AccountService::getMastodon($follower->following_id, true); + }) + ->filter(function ($account) { + return $account && isset($account['id']); + }) + ->values() + ->toArray(); + + return $this->json($res); + } + + $paginator = DB::table('followers') + ->select('id', 'profile_id', 'following_id') + ->whereProfileId($account['id']) + ->orderByDesc('id') + ->cursorPaginate($limit) + ->withQueryString(); + + $link = null; + + if ($paginator->onFirstPage()) { + if ($paginator->hasMorePages()) { + $link = '<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } else { + if ($paginator->previousPageUrl()) { + $link = '<'.$paginator->previousPageUrl().'>; rel="next"'; + } + + if ($paginator->hasMorePages()) { + $link .= ($link ? ', ' : '').'<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } + + $res = $paginator->map(function ($follower) use ($napi) { + return $napi ? AccountService::get($follower->following_id, true) : AccountService::getMastodon($follower->following_id, true); + }) + ->filter(function ($account) { + return $account && isset($account['id']); + }) + ->values() + ->toArray(); + + $headers = isset($link) ? ['Link' => $link] : []; + + return $this->json($res, 200, $headers); + } + + /** + * GET /api/v1/accounts/{id}/statuses + * + * @param int $id + * @return \App\Transformer\Api\StatusTransformer + */ + public function accountStatusesById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $user = $request->user(); + + $this->validate($request, [ + 'only_media' => 'nullable', + 'media_type' => 'sometimes|string|in:photo,video', + 'pinned' => 'nullable', + 'exclude_replies' => 'nullable', + 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'limit' => 'nullable|integer|min:1', + ]); + + $napi = $request->has(self::PF_API_ENTITY_KEY); + $profile = $napi ? AccountService::get($id, true) : AccountService::getMastodon($id, true); + + if (! $profile || ! isset($profile['id']) || ! $user) { + return $this->json(['error' => 'Account not found'], 404); + } + + if ($profile && strpos($profile['acct'], '@') != -1) { + $domain = parse_url($profile['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + + $limit = $request->input('limit') ?? 20; + if ($limit > 40) { + $limit = 40; + } + $max_id = $request->max_id; + $min_id = $request->min_id; + + if (! $max_id && ! $min_id) { + $min_id = 1; + } + + $pid = $request->user()->profile_id; + $scope = $request->only_media == true ? + ['photo', 'photo:album', 'video', 'video:album'] : + ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply']; + + if ($request->only_media && $request->has('media_type')) { + $mt = $request->input('media_type'); + if ($mt == 'video') { + $scope = ['video', 'video:album']; + } + } + + if (intval($pid) === intval($profile['id'])) { + $visibility = ['public', 'unlisted', 'private']; + } elseif ($profile['locked']) { + $following = FollowerService::follows($pid, $profile['id']); + if (! $following) { + return response('', 403); + } + $visibility = ['public', 'unlisted', 'private']; + } else { + $following = FollowerService::follows($pid, $profile['id']); + $visibility = $following ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; + } + + $dir = $min_id ? '>' : '<'; + $id = $min_id ?? $max_id; + $res = Status::select( + 'profile_id', + 'in_reply_to_id', + 'reblog_of_id', + 'type', + 'id', + 'scope' + ) + ->whereProfileId($profile['id']) + ->whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->whereIn('type', $scope) + ->where('id', $dir, $id) + ->whereIn('scope', $visibility) + ->limit($limit) + ->orderByDesc('id') + ->get() + ->map(function ($s) use ($user, $napi, $profile) { + try { + $status = $napi ? StatusService::get($s->id, false) : StatusService::getMastodon($s->id, false); + } catch (\Exception $e) { + return false; + } + + if ($profile) { + $status['account'] = $profile; + } + + if ($user && $status) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); + } + + return $status; + }) + ->filter(function ($s) { + return $s; + }) + ->values(); + + return $this->json($res); + } + + /** + * POST /api/v1/accounts/{id}/follow + * + * @param int $id + * @return \App\Transformer\Api\RelationshipTransformer + */ + public function accountFollowById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-follow', $user->id), 403, 'Invalid permissions for this action'); + + AccountService::setLastActive($user->id); + + $target = Profile::where('id', '!=', $user->profile_id) + ->whereNull('status') + ->findOrFail($id); + + abort_if($target && $target->moved_to_profile_id, 400, 'Cannot follow an account that has moved!'); + + if ($target && $target->domain) { + $domain = $target->domain; + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + + $private = (bool) $target->is_private; + $remote = (bool) $target->domain; + $blocked = UserFilter::whereUserId($target->id) + ->whereFilterType('block') + ->whereFilterableId($user->profile_id) + ->whereFilterableType('App\Profile') + ->exists(); + + if ($blocked == true) { + abort(400, 'You cannot follow this user.'); + } + + $isFollowing = Follower::whereProfileId($user->profile_id) + ->whereFollowingId($target->id) + ->exists(); + + // Following already, return empty relationship + if ($isFollowing == true) { + $res = RelationshipService::get($user->profile_id, $target->id) ?? []; + + return $this->json($res); + } + + // Rate limits, max 7500 followers per account + if ($user->profile->following_count && $user->profile->following_count >= Follower::MAX_FOLLOWING) { + abort(400, 'You cannot follow more than '.Follower::MAX_FOLLOWING.' accounts'); + } + + if ($private == true) { + $follow = FollowRequest::firstOrCreate([ + 'follower_id' => $user->profile_id, + 'following_id' => $target->id, + ]); + if ($remote == true && config('federation.activitypub.remoteFollow') == true) { + (new FollowerController)->sendFollow($user->profile, $target); + } + } else { + $follower = Follower::firstOrCreate([ + 'profile_id' => $user->profile_id, + 'following_id' => $target->id, + ]); + + if ($remote == true && config('federation.activitypub.remoteFollow') == true) { + (new FollowerController)->sendFollow($user->profile, $target); + } + FollowPipeline::dispatch($follower)->onQueue('high'); + } + + RelationshipService::refresh($user->profile_id, $target->id); + Cache::forget('profile:following:'.$target->id); + Cache::forget('profile:followers:'.$target->id); + Cache::forget('profile:following:'.$user->profile_id); + Cache::forget('profile:followers:'.$user->profile_id); + Cache::forget('api:local:exp:rec:'.$user->profile_id); + Cache::forget('user:account:id:'.$target->user_id); + Cache::forget('user:account:id:'.$user->id); + Cache::forget('profile:follower_count:'.$target->id); + Cache::forget('profile:follower_count:'.$user->profile_id); + Cache::forget('profile:following_count:'.$target->id); + Cache::forget('profile:following_count:'.$user->profile_id); + AccountService::del($user->profile_id); + AccountService::del($target->id); + + $res = RelationshipService::get($user->profile_id, $target->id); + + return $this->json($res); + } + + /** + * POST /api/v1/accounts/{id}/unfollow + * + * @param int $id + * @return \App\Transformer\Api\RelationshipTransformer + */ + public function accountUnfollowById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); + + $user = $request->user(); + + AccountService::setLastActive($user->id); + + $target = Profile::where('id', '!=', $user->profile_id) + ->whereNull('status') + ->findOrFail($id); + + $private = (bool) $target->is_private; + $remote = (bool) $target->domain; + + $isFollowing = Follower::whereProfileId($user->profile_id) + ->whereFollowingId($target->id) + ->exists(); + + if ($isFollowing == false) { + $followRequest = FollowRequest::whereFollowerId($user->profile_id) + ->whereFollowingId($target->id) + ->first(); + if ($followRequest) { + $followRequest->delete(); + RelationshipService::refresh($target->id, $user->profile_id); + } + $resource = new Fractal\Resource\Item($target, new RelationshipTransformer); + $res = $this->fractal->createData($resource)->toArray(); + + return $this->json($res); + } + + Follower::whereProfileId($user->profile_id) + ->whereFollowingId($target->id) + ->delete(); + + UnfollowPipeline::dispatch($user->profile_id, $target->id)->onQueue('high'); + + if ($remote == true && config('federation.activitypub.remoteFollow') == true) { + (new FollowerController)->sendUndoFollow($user->profile, $target); + } + + RelationshipService::refresh($user->profile_id, $target->id); + Cache::forget('profile:following:'.$target->id); + Cache::forget('profile:followers:'.$target->id); + Cache::forget('profile:following:'.$user->profile_id); + Cache::forget('profile:followers:'.$user->profile_id); + Cache::forget('api:local:exp:rec:'.$user->profile_id); + Cache::forget('user:account:id:'.$target->user_id); + Cache::forget('user:account:id:'.$user->id); + Cache::forget('profile:follower_count:'.$target->id); + Cache::forget('profile:follower_count:'.$user->profile_id); + Cache::forget('profile:following_count:'.$target->id); + Cache::forget('profile:following_count:'.$user->profile_id); + AccountService::del($user->profile_id); + AccountService::del($target->id); + + $res = RelationshipService::get($user->profile_id, $target->id); + + return $this->json($res); + } + + /** + * GET /api/v1/accounts/relationships + * + * @param array|int $id + * @return \App\Services\RelationshipService + */ + public function accountRelationshipsById(Request $request) + { + abort_if(! $request->user(), 403); + + $this->validate($request, [ + 'id' => 'required|array|min:1', + 'id.*' => 'required|integer|min:1|max:'.PHP_INT_MAX, + ]); + $ids = $request->input('id'); + if (count($ids) > 20) { + $ids = collect($ids)->take(20)->toArray(); + } + $napi = $request->has(self::PF_API_ENTITY_KEY); + $pid = $request->user()->profile_id ?? $request->user()->profile->id; + $res = collect($ids) + ->map(function ($id) use ($pid, $napi) { + if (intval($id) === intval($pid)) { + return [ + 'id' => $id, + 'following' => false, + 'followed_by' => false, + 'blocking' => false, + 'muting' => false, + 'muting_notifications' => false, + 'requested' => false, + 'domain_blocking' => false, + 'showing_reblogs' => false, + 'endorsed' => false, + ]; + } + + return $napi ? + RelationshipService::getWithDate($pid, $id) : + RelationshipService::get($pid, $id); + }); + + return $this->json($res); + } + + /** + * GET /api/v1/accounts/search + * + * + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountSearch(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ + 'q' => 'required|string|min:1|max:30', + 'limit' => 'nullable|integer|min:1', + 'resolve' => 'nullable', + ]); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-view-discover', $user->id), 403, 'Invalid permissions for this action'); + + AccountService::setLastActive($user->id); + $query = $request->input('q'); + $limit = $request->input('limit') ?? 20; + if ($limit > 20) { + $limit = 20; + } + $resolve = $request->boolean('resolve', false); + $q = $query.'%'; + + $profiles = Profile::where('username', 'like', $q) + ->orderByDesc('followers_count') + ->limit($limit) + ->pluck('id') + ->map(function ($id) { + return AccountService::getMastodon($id); + }) + ->filter(function ($account) { + return $account && isset($account['id']); + }) + ->values(); + + return $this->json($profiles); + } + + /** + * GET /api/v1/blocks + * + * + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountBlocks(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1', + 'page' => 'sometimes|integer|min:1', + ]); + + $user = $request->user(); + $limit = $request->input('limit') ?? 40; + if ($limit > 80) { + $limit = 80; + } + + $blocks = UserFilter::select('filterable_id', 'filterable_type', 'filter_type', 'user_id') + ->whereUserId($user->profile_id) + ->whereFilterableType('App\Profile') + ->whereFilterType('block') + ->orderByDesc('id') + ->simplePaginate($limit) + ->withQueryString(); + + $res = $blocks->pluck('filterable_id') + ->map(function ($id) { + return AccountService::get($id, true); + }) + ->filter(function ($account) { + return $account && isset($account['id']); + }) + ->values(); + + $baseUrl = config('app.url').'/api/v1/blocks?limit='.$limit.'&'; + $next = $blocks->nextPageUrl(); + $prev = $blocks->previousPageUrl(); + + if ($next && ! $prev) { + $link = '<'.$next.'>; rel="next"'; + } + + if (! $next && $prev) { + $link = '<'.$prev.'>; rel="prev"'; + } + + if ($next && $prev) { + $link = '<'.$next.'>; rel="next",<'.$prev.'>; rel="prev"'; + } + $headers = isset($link) ? ['Link' => $link] : []; + + return $this->json($res, 200, $headers); + } + + /** + * POST /api/v1/accounts/{id}/block + * + * @param int $id + * @return \App\Transformer\Api\RelationshipTransformer + */ + public function accountBlockById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $user = $request->user(); + $pid = $user->profile_id ?? $user->profile->id; + AccountService::setLastActive($user->id); + + if (intval($id) === intval($pid)) { + abort(400, 'You cannot block yourself'); + } + + $profile = Profile::findOrFail($id); + + abort_if($profile->moved_to_profile_id, 422, 'Cannot block an account that has migrated!'); + + if ($profile->user && $profile->user->is_admin == true) { + abort(400, 'You cannot block an admin'); + } + + $count = UserFilterService::blockCount($pid); + $maxLimit = (int) config_cache('instance.user_filters.max_user_blocks'); + if ($count == 0) { + $filterCount = UserFilter::whereUserId($pid) + ->whereFilterType('block') + ->get() + ->map(function ($rec) { + return AccountService::get($rec->filterable_id, true); + }) + ->filter(function ($account) { + return $account && isset($account['id']); + }) + ->values() + ->count(); + abort_if($filterCount >= $maxLimit, 422, AccountController::FILTER_LIMIT_BLOCK_TEXT.$maxLimit.' accounts'); + } else { + abort_if($count >= $maxLimit, 422, AccountController::FILTER_LIMIT_BLOCK_TEXT.$maxLimit.' accounts'); + } + + $followed = Follower::whereProfileId($profile->id)->whereFollowingId($pid)->first(); + if ($followed) { + $followed->delete(); + $profile->following_count = Follower::whereProfileId($profile->id)->count(); + $profile->save(); + $selfProfile = $user->profile; + $selfProfile->followers_count = Follower::whereFollowingId($pid)->count(); + $selfProfile->save(); + FollowerService::remove($profile->id, $pid); + AccountService::del($pid); + AccountService::del($profile->id); + } + + $following = Follower::whereProfileId($pid)->whereFollowingId($profile->id)->first(); + if ($following) { + $following->delete(); + $profile->followers_count = Follower::whereFollowingId($profile->id)->count(); + $profile->save(); + $selfProfile = $user->profile; + $selfProfile->following_count = Follower::whereProfileId($pid)->count(); + $selfProfile->save(); + FollowerService::remove($pid, $profile->pid); + AccountService::del($pid); + AccountService::del($profile->id); + } + + Notification::whereProfileId($pid) + ->whereActorId($profile->id) + ->get() + ->map(function ($n) use ($pid) { + NotificationService::del($pid, $n['id']); + $n->forceDelete(); + }); + + $filter = UserFilter::firstOrCreate([ + 'user_id' => $pid, + 'filterable_id' => $profile->id, + 'filterable_type' => 'App\Profile', + 'filter_type' => 'block', + ]); + + UserFilterService::block($pid, $id); + RelationshipService::refresh($pid, $id); + $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer); + $res = $this->fractal->createData($resource)->toArray(); + + return $this->json($res); + } + + /** + * POST /api/v1/accounts/{id}/unblock + * + * @param int $id + * @return \App\Transformer\Api\RelationshipTransformer + */ + public function accountUnblockById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $user = $request->user(); + $pid = $user->profile_id ?? $user->profile->id; + AccountService::setLastActive($user->id); + + if (intval($id) === intval($pid)) { + abort(400, 'You cannot unblock yourself'); + } + + $profile = Profile::findOrFail($id); + + abort_if($profile->moved_to_profile_id, 422, 'Cannot unblock an account that has migrated!'); + + $filter = UserFilter::whereUserId($pid) + ->whereFilterableId($profile->id) + ->whereFilterableType('App\Profile') + ->whereFilterType('block') + ->first(); + + if ($filter) { + $filter->delete(); + UserFilterService::unblock($pid, $profile->id); + } + RelationshipService::refresh($pid, $id); + + $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer); + $res = $this->fractal->createData($resource)->toArray(); + + return $this->json($res); + } + + /** + * GET /api/v1/custom_emojis + * + * Return custom emoji + * + * @return array + */ + public function customEmojis() + { + return response(CustomEmojiService::all())->header('Content-Type', 'application/json'); + } + + /** + * GET /api/v1/domain_blocks + * + * Return empty array + * + * @return array + */ + public function accountDomainBlocks(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + return response()->json([]); + } + + /** + * GET /api/v1/endorsements + * + * Return empty array + * + * @return array + */ + public function accountEndorsements(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + return response()->json([]); + } + + /** + * GET /api/v1/favourites + * + * Returns collection of liked statuses + * + * @return \App\Transformer\Api\StatusTransformer + */ + public function accountFavourites(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1', + ]); + + $user = $request->user(); + $maxId = $request->input('max_id'); + $minId = $request->input('min_id'); + $limit = $request->input('limit') ?? 10; + if ($limit > 40) { + $limit = 40; + } + + $res = Like::whereProfileId($user->profile_id) + ->when($maxId, function ($q, $maxId) { + return $q->where('id', '<', $maxId); + }) + ->when($minId, function ($q, $minId) { + return $q->where('id', '>', $minId); + }) + ->orderByDesc('id') + ->limit($limit) + ->get() + ->map(function ($like) { + $status = StatusService::getMastodon($like['status_id'], false); + $status['favourited'] = true; + $status['like_id'] = $like->id; + $status['liked_at'] = str_replace('+00:00', 'Z', $like->created_at->format(DATE_RFC3339_EXTENDED)); + + return $status; + }) + ->filter(function ($status) { + return $status && isset($status['id'], $status['like_id']); + }) + ->values(); + + if ($res->count()) { + $ids = $res->map(function ($status) { + return $status['like_id']; + })->filter(); + + $max = $ids->min() - 1; + $min = $ids->max(); + + $baseUrl = config('app.url').'/api/v1/favourites?limit='.$limit.'&'; + if ($maxId) { + $link = '<'.$baseUrl.'max_id='.$max.'>; rel="next",<'.$baseUrl.'min_id='.$min.'>; rel="prev"'; + } else { + $link = '<'.$baseUrl.'max_id='.$max.'>; rel="next"'; + } + + return $this->json($res, 200, ['Link' => $link]); + } else { + return $this->json($res); + } + } + + /** + * POST /api/v1/statuses/{id}/favourite + * + * @param int $id + * @return \App\Transformer\Api\StatusTransformer + */ + public function statusFavouriteById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); + + $napi = $request->has(self::PF_API_ENTITY_KEY); + $status = $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false); + + abort_unless($status, 404); + + abort_if(isset($status['moved'], $status['moved']['id']), 422, 'Cannot like a post from an account that has migrated'); + + if ($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) { + $domain = parse_url($status['account']['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + + $spid = $status['account']['id']; + + AccountService::setLastActive($user->id); + + if (intval($spid) !== intval($user->profile_id)) { + if ($status['visibility'] == 'private') { + abort_if(! FollowerService::follows($user->profile_id, $spid), 403); + } else { + abort_if(! in_array($status['visibility'], ['public', 'unlisted']), 403); + } + } + + abort_if( + Like::whereProfileId($user->profile_id) + ->where('created_at', '>', now()->subDay()) + ->count() >= Like::MAX_PER_DAY, + 429 + ); + + $blocks = UserFilterService::blocks($spid); + if ($blocks && in_array($user->profile_id, $blocks)) { + abort(422); + } + + $like = Like::firstOrCreate([ + 'profile_id' => $user->profile_id, + 'status_id' => $status['id'], + ]); + + if ($like->wasRecentlyCreated == true) { + $like->status_profile_id = $spid; + $like->is_comment = ! empty($status['in_reply_to_id']); + $like->save(); + Status::findOrFail($status['id'])->update([ + 'likes_count' => ($status['favourites_count'] ?? 0) + 1, + ]); + LikePipeline::dispatch($like)->onQueue('feed'); + } + + $status['favourited'] = true; + $status['favourites_count'] = $status['favourites_count'] + 1; + $status['bookmarked'] = BookmarkService::get($user->profile_id, $status['id']); + $status['reblogged'] = ReblogService::get($user->profile_id, $status['id']); + + return $this->json($status); + } + + /** + * POST /api/v1/statuses/{id}/unfavourite + * + * @param int $id + * @return \App\Transformer\Api\StatusTransformer + */ + public function statusUnfavouriteById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); + + $napi = $request->has(self::PF_API_ENTITY_KEY); + $status = $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false); + + abort_unless($status && isset($status['account']), 404); + abort_if(isset($status['moved'], $status['moved']['id']), 422, 'Cannot unlike a post from an account that has migrated'); + + if ($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) { + $domain = parse_url($status['account']['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + + $spid = $status['account']['id']; + + AccountService::setLastActive($user->id); + + if (intval($spid) !== intval($user->profile_id)) { + if ($status['visibility'] == 'private') { + abort_if(! FollowerService::follows($user->profile_id, $spid), 403); + } else { + abort_if(! in_array($status['visibility'], ['public', 'unlisted']), 403); + } + } + + $like = Like::whereProfileId($user->profile_id) + ->whereStatusId($status['id']) + ->first(); + + if ($like) { + $like->forceDelete(); + $ogStatus = Status::find($status['id']); + if ($ogStatus) { + $ogStatus->likes_count = $ogStatus->likes_count > 1 ? $ogStatus->likes_count - 1 : 0; + $ogStatus->save(); + } + } + + StatusService::del($status['id']); + + $status['favourited'] = false; + $status['favourites_count'] = isset($ogStatus) ? $ogStatus->likes_count : $status['favourites_count'] - 1; + $status['bookmarked'] = BookmarkService::get($user->profile_id, $status['id']); + $status['reblogged'] = ReblogService::get($user->profile_id, $status['id']); + + return $this->json($status); + } + + /** + * GET /api/v1/filters + * + * Return empty response since we filter server side + * + * @return array + */ + public function accountFilters(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + return response()->json([]); + } + + /** + * GET /api/v1/follow_requests + * + * Return array of Accounts that have sent follow requests + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountFollowRequests(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1|max:100', + ]); + + $user = $request->user(); + + $res = FollowRequest::whereFollowingId($user->profile->id) + ->limit($request->input('limit', 40)) + ->pluck('follower_id') + ->map(function ($id) { + return AccountService::getMastodon($id, true); + }) + ->filter(function ($acct) { + return $acct && isset($acct['id']); + }) + ->values(); + + return $this->json($res); + } + + /** + * POST /api/v1/follow_requests/{id}/authorize + * + * @param int $id + * @return null + */ + public function accountFollowRequestAccept(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); + + $pid = $request->user()->profile_id; + $target = AccountService::getMastodon($id); + + abort_if(isset($target['moved'], $target['moved']['id']), 422, 'Cannot accept a request from an account that has migrated!'); + + if (! $target) { + return response()->json(['error' => 'Record not found'], 404); + } + + if ($target && strpos($target['acct'], '@') != -1) { + $domain = parse_url($target['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + + $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); + + if (! $followRequest) { + return response()->json(['error' => 'Record not found'], 404); + } + + $follower = $followRequest->follower; + $follow = new Follower; + $follow->profile_id = $follower->id; + $follow->following_id = $pid; + $follow->save(); + + $profile = Profile::findOrFail($pid); + $profile->followers_count++; + $profile->save(); + AccountService::del($profile->id); + + $profile = Profile::findOrFail($follower->id); + $profile->following_count++; + $profile->save(); + AccountService::del($profile->id); + + if ($follower->domain != null && $follower->private_key === null) { + FollowAcceptPipeline::dispatch($followRequest)->onQueue('follow'); + } else { + FollowPipeline::dispatch($follow); + $followRequest->delete(); + } + + RelationshipService::refresh($pid, $id); + $res = RelationshipService::get($pid, $id); + $res['followed_by'] = true; + + return $this->json($res); + } + + /** + * POST /api/v1/follow_requests/{id}/reject + * + * @param int $id + * @return null + */ + public function accountFollowRequestReject(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); + + $pid = $request->user()->profile_id; + $target = AccountService::getMastodon($id); + + if (! $target) { + return response()->json(['error' => 'Record not found'], 404); + } + + abort_if(isset($target['moved'], $target['moved']['id']), 422, 'Cannot reject a request from an account that has migrated!'); + + $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); + + if (! $followRequest) { + return response()->json(['error' => 'Record not found'], 404); + } + + $follower = $followRequest->follower; + + if ($follower->domain != null && $follower->private_key === null) { + FollowRejectPipeline::dispatch($followRequest)->onQueue('follow'); + } else { + $followRequest->delete(); + } + + RelationshipService::refresh($pid, $id); + $res = RelationshipService::get($pid, $id); + + return $this->json($res); + } + + /** + * GET /api/v1/suggestions + * + * Return empty array as we don't support suggestions + * + * @return null + */ + public function accountSuggestions(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + // todo + + return response()->json([]); + } + + /** + * GET /api/v1/instance + * + * Information about the server. + * + * @return Instance + */ + public function instance(Request $request) + { + $res = Cache::remember('api:v1:instance-data-response-v1', 1800, function () { + $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () { + if (config_cache('instance.admin.pid')) { + return AccountService::getMastodon(config_cache('instance.admin.pid'), true); + } + $admin = User::whereIsAdmin(true)->first(); + + return $admin && isset($admin->profile_id) ? + AccountService::getMastodon($admin->profile_id, true) : + null; + }); + + $stats = Cache::remember('api:v1:instance-data:stats:v0', 43200, function () { + return [ + 'user_count' => (int) User::count(), + 'status_count' => (int) StatusService::totalLocalStatuses(), + 'domain_count' => (int) Instance::count(), + ]; + }); + + $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { + return config_cache('app.rules') ? + collect(json_decode(config_cache('app.rules'), true)) + ->map(function ($rule, $key) { + $id = $key + 1; + + return [ + 'id' => "{$id}", + 'text' => $rule, + ]; + }) + ->toArray() : []; + }); + + return [ + 'uri' => config('pixelfed.domain.app'), + 'title' => config_cache('app.name'), + 'short_description' => config_cache('app.short_description'), + 'description' => config_cache('app.description'), + 'email' => config('instance.email'), + 'version' => '3.5.3 (compatible; Pixelfed '.config('pixelfed.version').')', + 'urls' => [ + 'streaming_api' => null, + ], + 'stats' => $stats, + 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), + 'languages' => [config('app.locale')], + 'registrations' => (bool) config_cache('pixelfed.open_registration'), + 'approval_required' => (bool) config_cache('instance.curated_registration.enabled'), + 'contact_account' => $contact, + 'rules' => $rules, + 'configuration' => [ + 'media_attachments' => [ + 'image_matrix_limit' => 16777216, + 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, + 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')), + 'video_frame_rate_limit' => 120, + 'video_matrix_limit' => 2304000, + 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, + ], + 'polls' => [ + 'max_characters_per_option' => 50, + 'max_expiration' => 2629746, + 'max_options' => 4, + 'min_expiration' => 300, + ], + 'statuses' => [ + 'characters_reserved_per_url' => 23, + 'max_characters' => (int) config_cache('pixelfed.max_caption_length'), + 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), + ], + ], + ]; }); - return $this->json($profiles); - } - - /** - * GET /api/v1/blocks - * - * - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountBlocks(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40', - 'page' => 'nullable|integer|min:1|max:10' - ]); - - $user = $request->user(); - $limit = $request->input('limit') ?? 40; - - $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id') - ->whereUserId($user->profile_id) - ->whereFilterableType('App\Profile') - ->whereFilterType('block') - ->orderByDesc('id') - ->simplePaginate($limit) - ->pluck('filterable_id') - ->map(function($id) { - return AccountService::get($id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values(); - - return $this->json($blocked); - } - - /** - * POST /api/v1/accounts/{id}/block - * - * @param integer $id - * - * @return \App\Transformer\Api\RelationshipTransformer - */ - public function accountBlockById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - $pid = $user->profile_id ?? $user->profile->id; - - if(intval($id) === intval($pid)) { - abort(400, 'You cannot block yourself'); - } - - $profile = Profile::findOrFail($id); - - if($profile->user && $profile->user->is_admin == true) { - abort(400, 'You cannot block an admin'); - } - - $count = UserFilterService::blockCount($pid); - $maxLimit = intval(config('instance.user_filters.max_user_blocks')); - if($count == 0) { - $filterCount = UserFilter::whereUserId($pid) - ->whereFilterType('block') - ->get() - ->map(function($rec) { - return AccountService::get($rec->filterable_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->count(); - abort_if($filterCount >= $maxLimit, 422, AccountController::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts'); - } else { - abort_if($count >= $maxLimit, 422, AccountController::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts'); - } - - $followed = Follower::whereProfileId($profile->id)->whereFollowingId($pid)->first(); - if($followed) { - $followed->delete(); - $profile->following_count = Follower::whereProfileId($profile->id)->count(); - $profile->save(); - $selfProfile = $user->profile; - $selfProfile->followers_count = Follower::whereFollowingId($pid)->count(); - $selfProfile->save(); - FollowerService::remove($profile->id, $pid); - AccountService::del($pid); - AccountService::del($profile->id); - } - - $following = Follower::whereProfileId($pid)->whereFollowingId($profile->id)->first(); - if($following) { - $following->delete(); - $profile->followers_count = Follower::whereFollowingId($profile->id)->count(); - $profile->save(); - $selfProfile = $user->profile; - $selfProfile->following_count = Follower::whereProfileId($pid)->count(); - $selfProfile->save(); - FollowerService::remove($pid, $profile->pid); - AccountService::del($pid); - AccountService::del($profile->id); - } - - Notification::whereProfileId($pid) - ->whereActorId($profile->id) - ->get() - ->map(function($n) use($pid) { - NotificationService::del($pid, $n['id']); - $n->forceDelete(); - }); - - $filter = UserFilter::firstOrCreate([ - 'user_id' => $pid, - 'filterable_id' => $profile->id, - 'filterable_type' => 'App\Profile', - 'filter_type' => 'block', - ]); - - UserFilterService::block($pid, $id); - RelationshipService::refresh($pid, $id); - $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - - return $this->json($res); - } - - /** - * POST /api/v1/accounts/{id}/unblock - * - * @param integer $id - * - * @return \App\Transformer\Api\RelationshipTransformer - */ - public function accountUnblockById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - $pid = $user->profile_id ?? $user->profile->id; - - if(intval($id) === intval($pid)) { - abort(400, 'You cannot unblock yourself'); - } - - $profile = Profile::findOrFail($id); - - $filter = UserFilter::whereUserId($pid) - ->whereFilterableId($profile->id) - ->whereFilterableType('App\Profile') - ->whereFilterType('block') - ->first(); - - if($filter) { - $filter->delete(); - UserFilterService::unblock($pid, $profile->id); - RelationshipService::refresh($pid, $id); - } - - $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - - return $this->json($res); - } - - /** - * GET /api/v1/custom_emojis - * - * Return custom emoji - * - * @return array - */ - public function customEmojis() - { - return response(CustomEmojiService::all())->header('Content-Type', 'application/json'); - } - - /** - * GET /api/v1/domain_blocks - * - * Return empty array - * - * @return array - */ - public function accountDomainBlocks(Request $request) - { - abort_if(!$request->user(), 403); - return response()->json([]); - } - - /** - * GET /api/v1/endorsements - * - * Return empty array - * - * @return array - */ - public function accountEndorsements(Request $request) - { - abort_if(!$request->user(), 403); - return response()->json([]); - } - - /** - * GET /api/v1/favourites - * - * Returns collection of liked statuses - * - * @return \App\Transformer\Api\StatusTransformer - */ - public function accountFavourites(Request $request) - { - abort_if(!$request->user(), 403); - $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:20' - ]); - - $user = $request->user(); - $maxId = $request->input('max_id'); - $minId = $request->input('min_id'); - $limit = $request->input('limit') ?? 10; - - $res = Like::whereProfileId($user->profile_id) - ->when($maxId, function($q, $maxId) { - return $q->where('id', '<', $maxId); - }) - ->when($minId, function($q, $minId) { - return $q->where('id', '>', $minId); - }) - ->orderByDesc('id') - ->limit($limit) - ->get() - ->map(function($like) { - $status = StatusService::getMastodon($like['status_id'], false); - $status['favourited'] = true; - $status['like_id'] = $like->id; - $status['liked_at'] = str_replace('+00:00', 'Z', $like->created_at->format(DATE_RFC3339_EXTENDED)); - return $status; - }) - ->filter(function($status) { - return $status && isset($status['id'], $status['like_id']); - }) - ->values(); - - if($res->count()) { - $ids = $res->map(function($status) { - return $status['like_id']; - }); - $max = $ids->max(); - $min = $ids->min(); - - $baseUrl = config('app.url') . '/api/v1/favourites?limit=' . $limit . '&'; - $link = '<'.$baseUrl.'max_id='.$max.'>; rel="next",<'.$baseUrl.'min_id='.$min.'>; rel="prev"'; - return $this->json($res, 200, ['Link' => $link]); - } else { - return $this->json($res); - } - } - - /** - * POST /api/v1/statuses/{id}/favourite - * - * @param integer $id - * - * @return \App\Transformer\Api\StatusTransformer - */ - public function statusFavouriteById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - - $status = StatusService::getMastodon($id, false); - - abort_unless($status, 400); - - $spid = $status['account']['id']; - - if(intval($spid) !== intval($user->profile_id)) { - if($status['visibility'] == 'private') { - abort_if(!FollowerService::follows($user->profile_id, $spid), 403); - } else { - abort_if(!in_array($status['visibility'], ['public','unlisted']), 403); - } - } - - abort_if( - Like::whereProfileId($user->profile_id) - ->where('created_at', '>', now()->subDay()) - ->count() >= Like::MAX_PER_DAY, - 429 - ); - - $blocks = UserFilterService::blocks($spid); - if($blocks && in_array($user->profile_id, $blocks)) { - abort(422); - } - - $like = Like::firstOrCreate([ - 'profile_id' => $user->profile_id, - 'status_id' => $status['id'] - ]); - - if($like->wasRecentlyCreated == true) { - $like->status_profile_id = $spid; - $like->is_comment = !empty($status['in_reply_to_id']); - $like->save(); - Status::findOrFail($status['id'])->update([ - 'likes_count' => ($status['favourites_count'] ?? 0) + 1 - ]); - LikePipeline::dispatch($like)->onQueue('feed'); - } - - $status['favourited'] = true; - $status['favourites_count'] = $status['favourites_count'] + 1; - return $this->json($status); - } - - /** - * POST /api/v1/statuses/{id}/unfavourite - * - * @param integer $id - * - * @return \App\Transformer\Api\StatusTransformer - */ - public function statusUnfavouriteById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - - $status = Status::findOrFail($id); - - if(intval($status->profile_id) !== intval($user->profile_id)) { - if($status->scope == 'private') { - abort_if(!$status->profile->followedBy($user->profile), 403); - } else { - abort_if(!in_array($status->scope, ['public','unlisted']), 403); - } - } - - $like = Like::whereProfileId($user->profile_id) - ->whereStatusId($status->id) - ->first(); - - if($like) { - $like->forceDelete(); - $status->likes_count = $status->likes_count > 1 ? $status->likes_count - 1 : 0; - $status->save(); - } - - StatusService::del($status->id); - - $res = StatusService::getMastodon($status->id, false); - $res['favourited'] = false; - return $this->json($res); - } - - /** - * GET /api/v1/filters - * - * Return empty response since we filter server side - * - * @return array - */ - public function accountFilters(Request $request) - { - abort_if(!$request->user(), 403); - - return response()->json([]); - } - - /** - * GET /api/v1/follow_requests - * - * Return array of Accounts that have sent follow requests - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountFollowRequests(Request $request) - { - abort_if(!$request->user(), 403); - $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:100' - ]); - - $user = $request->user(); - - $res = FollowRequest::whereFollowingId($user->profile->id) - ->limit($request->input('limit', 40)) - ->pluck('follower_id') - ->map(function($id) { - return AccountService::getMastodon($id, true); - }) - ->filter(function($acct) { - return $acct && isset($acct['id']); - }) - ->values(); - - return $this->json($res); - } - - /** - * POST /api/v1/follow_requests/{id}/authorize - * - * @param integer $id - * - * @return null - */ - public function accountFollowRequestAccept(Request $request, $id) - { - abort_if(!$request->user(), 403); - $pid = $request->user()->profile_id; - $target = AccountService::getMastodon($id); - - if(!$target) { - return response()->json(['error' => 'Record not found'], 404); - } - - $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); - - if(!$followRequest) { - return response()->json(['error' => 'Record not found'], 404); - } - - $follower = $followRequest->follower; - $follow = new Follower(); - $follow->profile_id = $follower->id; - $follow->following_id = $pid; - $follow->save(); - - $profile = Profile::findOrFail($pid); - $profile->followers_count++; - $profile->save(); - AccountService::del($profile->id); - - $profile = Profile::findOrFail($follower->id); - $profile->following_count++; - $profile->save(); - AccountService::del($profile->id); - - if($follower->domain != null && $follower->private_key === null) { - FollowAcceptPipeline::dispatch($followRequest)->onQueue('follow'); - } else { - FollowPipeline::dispatch($follow); - $followRequest->delete(); - } - - RelationshipService::refresh($pid, $id); - $res = RelationshipService::get($pid, $id); - $res['followed_by'] = true; - return $this->json($res); - } - - /** - * POST /api/v1/follow_requests/{id}/reject - * - * @param integer $id - * - * @return null - */ - public function accountFollowRequestReject(Request $request, $id) - { - abort_if(!$request->user(), 403); - $pid = $request->user()->profile_id; - $target = AccountService::getMastodon($id); - - if(!$target) { - return response()->json(['error' => 'Record not found'], 404); - } - - $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); - - if(!$followRequest) { - return response()->json(['error' => 'Record not found'], 404); - } - - $follower = $followRequest->follower; - - if($follower->domain != null && $follower->private_key === null) { - FollowRejectPipeline::dispatch($followRequest)->onQueue('follow'); - } else { - $followRequest->delete(); - } - - RelationshipService::refresh($pid, $id); - $res = RelationshipService::get($pid, $id); - return $this->json($res); - } - - /** - * GET /api/v1/suggestions - * - * Return empty array as we don't support suggestions - * - * @return null - */ - public function accountSuggestions(Request $request) - { - abort_if(!$request->user(), 403); - - // todo - - return response()->json([]); - } - - /** - * GET /api/v1/instance - * - * Information about the server. - * - * @return Instance - */ - public function instance(Request $request) - { - $res = Cache::remember('api:v1:instance-data-response-v1', 1800, function () { - $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () { - if(config_cache('instance.admin.pid')) { - return AccountService::getMastodon(config_cache('instance.admin.pid'), true); - } - $admin = User::whereIsAdmin(true)->first(); - return $admin && isset($admin->profile_id) ? - AccountService::getMastodon($admin->profile_id, true) : - null; - }); - - $stats = Cache::remember('api:v1:instance-data:stats', 43200, function () { - return [ - 'user_count' => User::count(), - 'status_count' => Status::whereNull('uri')->count(), - 'domain_count' => Instance::count(), - ]; - }); - - $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { - return config_cache('app.rules') ? - collect(json_decode(config_cache('app.rules'), true)) - ->map(function($rule, $key) { - $id = $key + 1; - return [ - 'id' => "{$id}", - 'text' => $rule - ]; - }) - ->toArray() : []; - }); - - return [ - 'uri' => config('pixelfed.domain.app'), - 'title' => config('app.name'), - 'short_description' => config_cache('app.short_description'), - 'description' => config_cache('app.description'), - 'email' => config('instance.email'), - 'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') .')', - 'urls' => [ - 'streaming_api' => 'wss://' . config('pixelfed.domain.app') - ], - 'stats' => $stats, - 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), - 'languages' => [config('app.locale')], - 'registrations' => (bool) config_cache('pixelfed.open_registration'), - 'approval_required' => false, - 'contact_account' => $contact, - 'rules' => $rules, - 'configuration' => [ - 'media_attachments' => [ - 'image_matrix_limit' => 16777216, - 'image_size_limit' => config('pixelfed.max_photo_size') * 1024, - 'supported_mime_types' => explode(',', config('pixelfed.media_types')), - 'video_frame_rate_limit' => 120, - 'video_matrix_limit' => 2304000, - 'video_size_limit' => config('pixelfed.max_photo_size') * 1024, - ], - 'polls' => [ - 'max_characters_per_option' => 50, - 'max_expiration' => 2629746, - 'max_options' => 4, - 'min_expiration' => 300 - ], - 'statuses' => [ - 'characters_reserved_per_url' => 23, - 'max_characters' => (int) config('pixelfed.max_caption_length'), - 'max_media_attachments' => (int) config('pixelfed.max_album_length') - ] - ] - ]; - }); - - return $this->json($res); - } - - /** - * GET /api/v1/lists - * - * Return empty array as we don't support lists - * - * @return null - */ - public function accountLists(Request $request) - { - abort_if(!$request->user(), 403); - - return response()->json([]); - } - - /** - * GET /api/v1/accounts/{id}/lists - * - * @param integer $id - * - * @return null - */ - public function accountListsById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - return response()->json([]); - } - - /** - * POST /api/v1/media - * - * - * @return MediaTransformer - */ - public function mediaUpload(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'file.*' => [ - 'required_without:file', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'file' => [ - 'required_without:file.*', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'filter_name' => 'nullable|string|max:24', - 'filter_class' => 'nullable|alpha_dash|max:24', - 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length') - ]); - - $user = $request->user(); - - if($user->last_active_at == null) { - return []; - } - - if(empty($request->file('file'))) { - return response('', 422); - } - - $limitKey = 'compose:rate-limit:media-upload:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); - - return $dailyLimit >= 1250; - }); - abort_if($limitReached == true, 429); - - $profile = $user->profile; - - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { - return Media::whereUserId($user->id)->sum('size') / 1000; - }); - $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { - abort(403, 'Account size limit reached.'); - } - } - - $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; - $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; - - $photo = $request->file('file'); - - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), $mimes) == false) { - abort(403, 'Invalid or unsupported mime type.'); - } - - $storagePath = MediaPathService::get($user, 2); - $path = $photo->storePublicly($storagePath); - $hash = \hash_file('sha256', $photo); - $license = null; - $mime = $photo->getMimeType(); - - // if($photo->getMimeType() == 'image/heic') { - // abort_if(config('image.driver') !== 'imagick', 422, 'Invalid media type'); - // abort_if(!in_array('HEIC', \Imagick::queryformats()), 422, 'Unsupported media type'); - // $oldPath = $path; - // $path = str_replace('.heic', '.jpg', $path); - // $mime = 'image/jpeg'; - // \Image::make($photo)->save(storage_path("app/{$path}")); - // @unlink(storage_path("app/{$oldPath}")); - // } - - $settings = UserSetting::whereUserId($user->id)->first(); - - if($settings && !empty($settings->compose_settings)) { - $compose = $settings->compose_settings; - - if(isset($compose['default_license']) && $compose['default_license'] != 1) { - $license = $compose['default_license']; - } - } - - abort_if(MediaBlocklistService::exists($hash) == true, 451); - - $media = new Media(); - $media->status_id = null; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $photo->getSize(); - $media->mime = $mime; - $media->caption = $request->input('description'); - $media->filter_class = $filterClass; - $media->filter_name = $filterName; - if($license) { - $media->license = $license; - } - $media->save(); - - switch ($media->mime) { - case 'image/jpeg': - case 'image/png': - ImageOptimize::dispatch($media)->onQueue('mmo'); - break; - - case 'video/mp4': - VideoThumbnail::dispatch($media)->onQueue('mmo'); - $preview_url = '/storage/no-preview.png'; - $url = '/storage/no-preview.png'; - break; - } - - Cache::forget($limitKey); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - $res['preview_url'] = $media->url(). '?v=' . time(); - $res['url'] = $media->url(). '?v=' . time(); - return $this->json($res); - } - - /** - * PUT /api/v1/media/{id} - * - * @param integer $id - * - * @return MediaTransformer - */ - public function mediaUpdate(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length') - ]); - - $user = $request->user(); - - $media = Media::whereUserId($user->id) - ->whereProfileId($user->profile_id) - ->findOrFail($id); - - $executed = RateLimiter::attempt( - 'media:update:'.$user->id, - 10, - function() use($media, $request) { - $caption = Purify::clean($request->input('description')); - - if($caption != $media->caption) { - $media->caption = $caption; - $media->save(); - - if($media->status_id) { - MediaService::del($media->status_id); - StatusService::del($media->status_id); - } - } - }); - - if(!$executed) { - return response()->json([ - 'error' => 'Too many attempts. Try again in a few minutes.' - ], 429); - }; - - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - return $this->json($fractal->createData($resource)->toArray()); - } - - /** - * GET /api/v1/media/{id} - * - * @param integer $id - * - * @return MediaTransformer - */ - public function mediaGet(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - - $media = Media::whereUserId($user->id) - ->whereNull('status_id') - ->findOrFail($id); - - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - return $this->json($res); - } - - /** - * POST /api/v2/media - * - * - * @return MediaTransformer - */ - public function mediaUploadV2(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'file.*' => [ - 'required_without:file', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'file' => [ - 'required_without:file.*', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'filter_name' => 'nullable|string|max:24', - 'filter_class' => 'nullable|alpha_dash|max:24', - 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'), - 'replace_id' => 'sometimes' - ]); - - $user = $request->user(); - - if($user->last_active_at == null) { - return []; - } - - if(empty($request->file('file'))) { - return response('', 422); - } - - $limitKey = 'compose:rate-limit:media-upload:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); - - return $dailyLimit >= 1250; - }); - abort_if($limitReached == true, 429); - - $profile = $user->profile; - - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { - return Media::whereUserId($user->id)->sum('size') / 1000; - }); - $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { - abort(403, 'Account size limit reached.'); - } - } - - $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; - $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; - - $photo = $request->file('file'); - - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), $mimes) == false) { - abort(403, 'Invalid or unsupported mime type.'); - } - - $storagePath = MediaPathService::get($user, 2); - $path = $photo->storePublicly($storagePath); - $hash = \hash_file('sha256', $photo); - $license = null; - $mime = $photo->getMimeType(); - - $settings = UserSetting::whereUserId($user->id)->first(); - - if($settings && !empty($settings->compose_settings)) { - $compose = $settings->compose_settings; - - if(isset($compose['default_license']) && $compose['default_license'] != 1) { - $license = $compose['default_license']; - } - } - - abort_if(MediaBlocklistService::exists($hash) == true, 451); - - if($request->has('replace_id')) { - $rpid = $request->input('replace_id'); - $removeMedia = Media::whereNull('status_id') - ->whereUserId($user->id) - ->whereProfileId($profile->id) - ->where('created_at', '>', now()->subHours(2)) - ->find($rpid); - if($removeMedia) { - $dateTime = Carbon::now(); - MediaDeletePipeline::dispatch($removeMedia) - ->onQueue('mmo') - ->delay($dateTime->addMinutes(15)); - } - } - - $media = new Media(); - $media->status_id = null; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $photo->getSize(); - $media->mime = $mime; - $media->caption = $request->input('description'); - $media->filter_class = $filterClass; - $media->filter_name = $filterName; - if($license) { - $media->license = $license; - } - $media->save(); - - switch ($media->mime) { - case 'image/jpeg': - case 'image/png': - ImageOptimize::dispatch($media)->onQueue('mmo'); - break; - - case 'video/mp4': - VideoThumbnail::dispatch($media)->onQueue('mmo'); - $preview_url = '/storage/no-preview.png'; - $url = '/storage/no-preview.png'; - break; - } - - Cache::forget($limitKey); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - $res['preview_url'] = $media->url(). '?v=' . time(); - $res['url'] = null; - return $this->json($res, 202); - } - - /** - * GET /api/v1/mutes - * - * - * @return AccountTransformer - */ - public function accountMutes(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40' - ]); - - $user = $request->user(); - $limit = $request->input('limit', 40); - - $mutes = UserFilter::whereUserId($user->profile_id) - ->whereFilterableType('App\Profile') - ->whereFilterType('mute') - ->orderByDesc('id') - ->simplePaginate($limit) - ->pluck('filterable_id') - ->map(function($id) { - return AccountService::get($id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values(); - - return $this->json($mutes); - } - - /** - * POST /api/v1/accounts/{id}/mute - * - * @param integer $id - * - * @return RelationshipTransformer - */ - public function accountMuteById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - $pid = $user->profile_id; - - if(intval($pid) === intval($id)) { + return $this->json($res); + } + + /** + * GET /api/v1/lists + * + * Return empty array as we don't support lists + * + * @return null + */ + public function accountLists(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + return response()->json([]); + } + + /** + * GET /api/v1/accounts/{id}/lists + * + * @param int $id + * @return null + */ + public function accountListsById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + return response()->json([]); + } + + /** + * POST /api/v1/media + * + * + * @return MediaTransformer + */ + public function mediaUpload(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $this->validate($request, [ + 'file.*' => [ + 'required_without:file', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + ], + 'file' => [ + 'required_without:file.*', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + ], + 'filter_name' => 'nullable|string|max:24', + 'filter_class' => 'nullable|alpha_dash|max:24', + 'description' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'), + ]); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + + AccountService::setLastActive($user->id); + + if ($user->last_active_at == null) { + return []; + } + + if (empty($request->file('file'))) { + return response('', 422); + } + + $limitKey = 'compose:rate-limit:media-upload:'.$user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { + $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); + + return $dailyLimit >= 1250; + }); + abort_if($limitReached == true, 429); + + $profile = $user->profile; + + $accountSize = UserStorageService::get($user->id); + abort_if($accountSize === -1, 403, 'Invalid request.'); + $photo = $request->file('file'); + $fileSize = $photo->getSize(); + $sizeInKbs = (int) ceil($fileSize / 1000); + $updatedAccountSize = (int) $accountSize + (int) $sizeInKbs; + + if ((bool) config_cache('pixelfed.enforce_account_limit') == true) { + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($updatedAccountSize >= $limit) { + abort(403, 'Account size limit reached.'); + } + } + + $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; + $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; + + $mimes = explode(',', config_cache('pixelfed.media_types')); + if (in_array($photo->getMimeType(), $mimes) == false) { + abort(403, 'Invalid or unsupported mime type.'); + } + + $storagePath = MediaPathService::get($user, 2); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + $license = null; + $mime = $photo->getMimeType(); + + // if($photo->getMimeType() == 'image/heic') { + // abort_if(config('image.driver') !== 'imagick', 422, 'Invalid media type'); + // abort_if(!in_array('HEIC', \Imagick::queryformats()), 422, 'Unsupported media type'); + // $oldPath = $path; + // $path = str_replace('.heic', '.jpg', $path); + // $mime = 'image/jpeg'; + // \Image::make($photo)->save(storage_path("app/{$path}")); + // @unlink(storage_path("app/{$oldPath}")); + // } + + $settings = UserSetting::whereUserId($user->id)->first(); + + if ($settings && ! empty($settings->compose_settings)) { + $compose = $settings->compose_settings; + + if (isset($compose['default_license']) && $compose['default_license'] != 1) { + $license = $compose['default_license']; + } + } + + abort_if(MediaBlocklistService::exists($hash) == true, 451); + + $media = new Media; + $media->status_id = null; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->mime = $mime; + $media->caption = $request->input('description') ?? ''; + $media->filter_class = $filterClass; + $media->filter_name = $filterName; + if ($license) { + $media->license = $license; + } + $media->save(); + + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + ImageOptimize::dispatch($media)->onQueue('mmo'); + break; + + case 'video/mp4': + VideoThumbnail::dispatch($media)->onQueue('mmo'); + $preview_url = '/storage/no-preview.png'; + $url = '/storage/no-preview.png'; + break; + } + + $user->storage_used = (int) $updatedAccountSize; + $user->storage_used_updated_at = now(); + $user->save(); + + Cache::forget($limitKey); + $resource = new Fractal\Resource\Item($media, new MediaTransformer); + $res = $this->fractal->createData($resource)->toArray(); + $res['preview_url'] = $media->url().'?v='.time(); + $res['url'] = $media->url().'?v='.time(); + + return $this->json($res); + } + + /** + * PUT /api/v1/media/{id} + * + * @param int $id + * @return MediaTransformer + */ + public function mediaUpdate(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $this->validate($request, [ + 'description' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'), + ]); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + + AccountService::setLastActive($user->id); + + $media = Media::whereUserId($user->id) + ->whereProfileId($user->profile_id) + ->findOrFail($id); + + $executed = RateLimiter::attempt( + 'media:update:'.$user->id, + 10, + function () use ($media, $request) { + $caption = Purify::clean($request->input('description')); + + if ($caption != $media->caption) { + $media->caption = $caption; + $media->save(); + + if ($media->status_id) { + MediaService::del($media->status_id); + StatusService::del($media->status_id); + } + } + }); + + if (! $executed) { + return response()->json([ + 'error' => 'Too many attempts. Try again in a few minutes.', + ], 429); + } + + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($media, new MediaTransformer); + + return $this->json($fractal->createData($resource)->toArray()); + } + + /** + * GET /api/v1/media/{id} + * + * @param int $id + * @return MediaTransformer + */ + public function mediaGet(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + AccountService::setLastActive($user->id); + + $media = Media::whereUserId($user->id) + ->whereNull('status_id') + ->findOrFail($id); + + $resource = new Fractal\Resource\Item($media, new MediaTransformer); + $res = $this->fractal->createData($resource)->toArray(); + + return $this->json($res); + } + + /** + * POST /api/v2/media + * + * + * @return MediaTransformer + */ + public function mediaUploadV2(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $this->validate($request, [ + 'file.*' => [ + 'required_without:file', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + ], + 'file' => [ + 'required_without:file.*', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + ], + 'filter_name' => 'nullable|string|max:24', + 'filter_class' => 'nullable|alpha_dash|max:24', + 'description' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'), + 'replace_id' => 'sometimes', + ]); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + + if ($user->last_active_at == null) { + return []; + } + + AccountService::setLastActive($user->id); + + if (empty($request->file('file'))) { + return response('', 422); + } + + $limitKey = 'compose:rate-limit:media-upload:'.$user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { + $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); + + return $dailyLimit >= 1250; + }); + abort_if($limitReached == true, 429); + + $profile = $user->profile; + + $accountSize = UserStorageService::get($user->id); + abort_if($accountSize === -1, 403, 'Invalid request.'); + $photo = $request->file('file'); + $fileSize = $photo->getSize(); + $sizeInKbs = (int) ceil($fileSize / 1000); + $updatedAccountSize = (int) $accountSize + (int) $sizeInKbs; + + if ((bool) config_cache('pixelfed.enforce_account_limit') == true) { + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($updatedAccountSize >= $limit) { + abort(403, 'Account size limit reached.'); + } + } + + $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; + $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; + + $mimes = explode(',', config_cache('pixelfed.media_types')); + if (in_array($photo->getMimeType(), $mimes) == false) { + abort(403, 'Invalid or unsupported mime type.'); + } + + $storagePath = MediaPathService::get($user, 2); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + $license = null; + $mime = $photo->getMimeType(); + + $settings = UserSetting::whereUserId($user->id)->first(); + + if ($settings && ! empty($settings->compose_settings)) { + $compose = $settings->compose_settings; + + if (isset($compose['default_license']) && $compose['default_license'] != 1) { + $license = $compose['default_license']; + } + } + + abort_if(MediaBlocklistService::exists($hash) == true, 451); + + if ($request->has('replace_id')) { + $rpid = $request->input('replace_id'); + $removeMedia = Media::whereNull('status_id') + ->whereUserId($user->id) + ->whereProfileId($profile->id) + ->where('created_at', '>', now()->subHours(2)) + ->find($rpid); + if ($removeMedia) { + $dateTime = Carbon::now(); + MediaDeletePipeline::dispatch($removeMedia) + ->onQueue('mmo') + ->delay($dateTime->addMinutes(15)); + } + } + + $media = new Media; + $media->status_id = null; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->mime = $mime; + $media->caption = $request->input('description') ?? ''; + $media->filter_class = $filterClass; + $media->filter_name = $filterName; + if ($license) { + $media->license = $license; + } + $media->save(); + + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + ImageOptimize::dispatch($media)->onQueue('mmo'); + break; + + case 'video/mp4': + VideoThumbnail::dispatch($media)->onQueue('mmo'); + $preview_url = '/storage/no-preview.png'; + $url = '/storage/no-preview.png'; + break; + } + + $user->storage_used = (int) $updatedAccountSize; + $user->storage_used_updated_at = now(); + $user->save(); + + Cache::forget($limitKey); + $resource = new Fractal\Resource\Item($media, new MediaTransformer); + $res = $this->fractal->createData($resource)->toArray(); + $res['preview_url'] = $media->url().'?v='.time(); + $res['url'] = null; + + return $this->json($res, 202); + } + + /** + * GET /api/v1/mutes + * + * + * @return AccountTransformer + */ + public function accountMutes(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1', + ]); + + $user = $request->user(); + $limit = $request->input('limit', 40); + if ($limit > 80) { + $limit = 80; + } + + $mutes = UserFilter::whereUserId($user->profile_id) + ->whereFilterableType('App\Profile') + ->whereFilterType('mute') + ->orderByDesc('id') + ->simplePaginate($limit) + ->withQueryString(); + + $res = $mutes->pluck('filterable_id') + ->map(function ($id) { + return AccountService::get($id, true); + }) + ->filter(function ($account) { + return $account && isset($account['id']); + }) + ->values(); + + $baseUrl = config('app.url').'/api/v1/mutes?limit='.$limit.'&'; + $next = $mutes->nextPageUrl(); + $prev = $mutes->previousPageUrl(); + + if ($next && ! $prev) { + $link = '<'.$next.'>; rel="next"'; + } + + if (! $next && $prev) { + $link = '<'.$prev.'>; rel="prev"'; + } + + if ($next && $prev) { + $link = '<'.$next.'>; rel="next",<'.$prev.'>; rel="prev"'; + } + $headers = isset($link) ? ['Link' => $link] : []; + + return $this->json($res, 200, $headers); + } + + /** + * POST /api/v1/accounts/{id}/mute + * + * @param int $id + * @return RelationshipTransformer + */ + public function accountMuteById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $user = $request->user(); + $pid = $user->profile_id; + + if (intval($pid) === intval($id)) { return $this->json(['error' => 'You cannot mute yourself'], 500); } - $account = Profile::findOrFail($id); + $account = Profile::findOrFail($id); - $count = UserFilterService::muteCount($pid); - $maxLimit = intval(config('instance.user_filters.max_user_mutes')); - if($count == 0) { - $filterCount = UserFilter::whereUserId($pid) - ->whereFilterType('mute') - ->get() - ->map(function($rec) { - return AccountService::get($rec->filterable_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->count(); - abort_if($filterCount >= $maxLimit, 422, AccountController::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts'); - } else { - abort_if($count >= $maxLimit, 422, AccountController::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts'); - } + abort_if($account->moved_to_profile_id, 422, 'Cannot mute an account that has migrated!'); - $filter = UserFilter::firstOrCreate([ - 'user_id' => $pid, - 'filterable_id' => $account->id, - 'filterable_type' => 'App\Profile', - 'filter_type' => 'mute', - ]); + if ($account && $account->domain) { + $domain = $account->domain; + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } - RelationshipService::refresh($pid, $id); + $count = UserFilterService::muteCount($pid); + $maxLimit = (int) config_cache('instance.user_filters.max_user_mutes'); + if ($count == 0) { + $filterCount = UserFilter::whereUserId($pid) + ->whereFilterType('mute') + ->get() + ->map(function ($rec) { + return AccountService::get($rec->filterable_id, true); + }) + ->filter(function ($account) { + return $account && isset($account['id']); + }) + ->values() + ->count(); + abort_if($filterCount >= $maxLimit, 422, AccountController::FILTER_LIMIT_MUTE_TEXT.$maxLimit.' accounts'); + } else { + abort_if($count >= $maxLimit, 422, AccountController::FILTER_LIMIT_MUTE_TEXT.$maxLimit.' accounts'); + } - $resource = new Fractal\Resource\Item($account, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - return $this->json($res); - } + $filter = UserFilter::firstOrCreate([ + 'user_id' => $pid, + 'filterable_id' => $account->id, + 'filterable_type' => 'App\Profile', + 'filter_type' => 'mute', + ]); - /** - * POST /api/v1/accounts/{id}/unmute - * - * @param integer $id - * - * @return RelationshipTransformer - */ - public function accountUnmuteById(Request $request, $id) - { - abort_if(!$request->user(), 403); + RelationshipService::refresh($pid, $id); - $user = $request->user(); - $pid = $user->profile_id; + $resource = new Fractal\Resource\Item($account, new RelationshipTransformer); + $res = $this->fractal->createData($resource)->toArray(); - if(intval($pid) === intval($id)) { + return $this->json($res); + } + + /** + * POST /api/v1/accounts/{id}/unmute + * + * @param int $id + * @return RelationshipTransformer + */ + public function accountUnmuteById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $user = $request->user(); + $pid = $user->profile_id; + + if (intval($pid) === intval($id)) { return $this->json(['error' => 'You cannot unmute yourself'], 500); } - $profile = Profile::findOrFail($id); + $profile = Profile::findOrFail($id); - $filter = UserFilter::whereUserId($pid) - ->whereFilterableId($profile->id) - ->whereFilterableType('App\Profile') - ->whereFilterType('mute') - ->first(); + abort_if($profile->moved_to_profile_id, 422, 'Cannot unmute an account that has migrated!'); - if($filter) { - $filter->delete(); - UserFilterService::unmute($pid, $profile->id); - RelationshipService::refresh($pid, $id); - } + $filter = UserFilter::whereUserId($pid) + ->whereFilterableId($profile->id) + ->whereFilterableType('App\Profile') + ->whereFilterType('mute') + ->first(); - $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - return $this->json($res); - } - - /** - * GET /api/v1/notifications - * - * - * @return NotificationTransformer - */ - public function accountNotifications(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:100', - 'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, - 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, - 'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, - ]); - - $pid = $request->user()->profile_id; - $limit = $request->input('limit', 20); - - $since = $request->input('since_id'); - $min = $request->input('min_id'); - $max = $request->input('max_id'); - - if(!$since && !$min && !$max) { - $min = 1; - } - - $maxId = null; - $minId = null; - - if($max) { - $res = NotificationService::getMaxMastodon($pid, $max, $limit); - $ids = NotificationService::getRankedMaxId($pid, $max, $limit); - if(!empty($ids)) { - $maxId = max($ids); - $minId = min($ids); - } - } else { - $res = NotificationService::getMinMastodon($pid, $min ?? $since, $limit); - $ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit); - if(!empty($ids)) { - $maxId = max($ids); - $minId = min($ids); - } - } - - if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) { - Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600); - NotificationService::warmCache($pid, 400, true); - } - - $baseUrl = config('app.url') . '/api/v1/notifications?limit=' . $limit . '&'; - - if($minId == $maxId) { - $minId = null; - } - - if($maxId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; - } - - if($minId) { - $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } - - if($maxId && $minId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } - - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res, 200, $headers); - } - - /** - * GET /api/v1/timelines/home - * - * - * @return StatusTransformer - */ - public function timelineHome(Request $request) - { - $this->validate($request,[ - 'page' => 'sometimes|integer|max:40', - 'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, - 'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'sometimes|integer|min:1|max:100', - 'include_reblogs' => 'sometimes', - ]); - - $napi = $request->has(self::PF_API_ENTITY_KEY); - $page = $request->input('page'); - $min = $request->input('min_id'); - $max = $request->input('max_id'); - $limit = $request->input('limit') ?? 20; - $pid = $request->user()->profile_id; - $includeReblogs = $request->filled('include_reblogs'); - $nullFields = $includeReblogs ? - ['in_reply_to_id'] : - ['in_reply_to_id', 'reblog_of_id']; - $inTypes = $includeReblogs ? - ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'share'] : - ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']; - - $following = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { - $following = Follower::whereProfileId($pid)->pluck('following_id'); - return $following->push($pid)->toArray(); - }); - - if($min || $max) { - $dir = $min ? '>' : '<'; - $id = $min ?? $max; - $res = Status::select( - 'id', - 'profile_id', - 'type', - 'visibility', - 'in_reply_to_id', - 'reblog_of_id' - ) - ->where('id', $dir, $id) - ->whereNull($nullFields) - ->whereIntegerInRaw('profile_id', $following) - ->whereIn('type', $inTypes) - ->whereIn('visibility',['public', 'unlisted', 'private']) - ->orderByDesc('id') - ->take(($limit * 2)) - ->get() - ->map(function($s) use($pid, $napi) { - try { - $account = $napi ? AccountService::get($s['profile_id'], true) : AccountService::getMastodon($s['profile_id'], true); - if(!$account) { - return false; - } - $status = $napi ? StatusService::get($s['id'], false) : StatusService::getMastodon($s['id'], false); - if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { - return false; - } - } catch(\Exception $e) { - return false; - } - - $status['account'] = $account; - - if($pid) { - $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); - $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } - return $status; - }) - ->filter(function($status) { - return $status && isset($status['account']); - }) - ->map(function($status) use($pid) { - if(!empty($status['reblog'])) { - $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); - $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } - - return $status; - }) - ->take($limit) - ->values(); - } else { - $res = Status::select( - 'id', - 'profile_id', - 'type', - 'visibility', - 'in_reply_to_id', - 'reblog_of_id', - ) - ->whereNull($nullFields) - ->whereIntegerInRaw('profile_id', $following) - ->whereIn('type', $inTypes) - ->whereIn('visibility',['public', 'unlisted', 'private']) - ->orderByDesc('id') - ->take(($limit * 2)) - ->get() - ->map(function($s) use($pid, $napi) { - try { - $account = $napi ? AccountService::get($s['profile_id'], true) : AccountService::getMastodon($s['profile_id'], true); - if(!$account) { - return false; - } - $status = $napi ? StatusService::get($s['id'], false) : StatusService::getMastodon($s['id'], false); - if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { - return false; - } - } catch(\Exception $e) { - return false; - } - - $status['account'] = $account; - - if($pid) { - $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); - $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } - return $status; - }) - ->filter(function($status) { - return $status && isset($status['account']); - }) - ->map(function($status) use($pid) { - if(!empty($status['reblog'])) { - $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); - $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } - - return $status; - }) - ->take($limit) - ->values(); - } - - $baseUrl = config('app.url') . '/api/v1/timelines/home?limit=' . $limit . '&'; - $minId = $res->map(function($s) { - return ['id' => $s['id']]; - })->min('id'); - $maxId = $res->map(function($s) { - return ['id' => $s['id']]; - })->max('id'); - - if($minId == $maxId) { - $minId = null; - } - - if($maxId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; - } - - if($minId) { - $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } - - if($maxId && $minId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } - - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res->toArray(), 200, $headers); - } - - /** - * GET /api/v1/timelines/public - * - * - * @return StatusTransformer - */ - public function timelinePublic(Request $request) - { - $this->validate($request,[ - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|max:100', - 'remote' => 'sometimes', - 'local' => 'sometimes' - ]); - - $napi = $request->has(self::PF_API_ENTITY_KEY); - $min = $request->input('min_id'); - $max = $request->input('max_id'); - $limit = $request->input('limit') ?? 20; - $user = $request->user(); - $remote = ($request->has('remote') && $request->input('remote') == true) || ($request->filled('local') && $request->input('local') != true); - $filtered = $user ? UserFilterService::filters($user->profile_id) : []; - - if((!$request->has('local') || $remote) && config('instance.timeline.network.cached')) { - Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() { - if(NetworkTimelineService::count() == 0) { - NetworkTimelineService::warmCache(true, config('instance.timeline.network.cache_dropoff')); - } - }); - - if ($max) { - $feed = NetworkTimelineService::getRankedMaxId($max, $limit + 5); - } else if ($min) { - $feed = NetworkTimelineService::getRankedMinId($min, $limit + 5); - } else { - $feed = NetworkTimelineService::get(0, $limit + 5); - } - } else { - Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { - if(PublicTimelineService::count() == 0) { - PublicTimelineService::warmCache(true, 400); - } - }); - - if ($max) { - $feed = PublicTimelineService::getRankedMaxId($max, $limit + 5); - } else if ($min) { - $feed = PublicTimelineService::getRankedMinId($min, $limit + 5); - } else { - $feed = PublicTimelineService::get(0, $limit + 5); - } + if ($filter) { + $filter->delete(); + UserFilterService::unmute($pid, $profile->id); } - $res = collect($feed) - ->filter(function($k) use($min, $max) { - if(!$min && !$max) { - return true; - } + RelationshipService::refresh($pid, $id); - if($min) { - return $min != $k; - } + $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer); + $res = $this->fractal->createData($resource)->toArray(); - if($max) { - return $max != $k; - } - }) - ->map(function($k) use($user, $napi) { - try { - $status = $napi ? StatusService::get($k) : StatusService::getMastodon($k); - if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { - return false; - } - } catch(\Exception $e) { - return false; - } + return $this->json($res); + } - $account = $napi ? AccountService::get($status['account']['id'], true) : AccountService::getMastodon($status['account']['id'], true); - if(!$account) { - return false; - } + /** + * GET /api/v1/notifications + * + * + * @return NotificationTransformer + */ + public function accountNotifications(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - $status['account'] = $account; + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1', + 'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, + 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, + 'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, + 'types[]' => 'sometimes|array', + 'types[].*' => 'string|in:mention,reblog,follow,favourite', + 'type' => 'sometimes|string|in:mention,reblog,follow,favourite', + '_pe' => 'sometimes', + ]); - if($user) { - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $status['id']); - $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $status['id']); - } - return $status; - }) - ->filter(function($s) use($filtered) { - return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; - }) - ->take($limit) - ->values(); + $pid = $request->user()->profile_id; + $limit = $request->input('limit', 20); + $ogLimit = $request->input('limit', 20); + if ($limit > 40) { + $limit = 40; + $ogLimit = 40; + } - $baseUrl = config('app.url') . '/api/v1/timelines/public?limit=' . $limit . '&'; - if($remote) { - $baseUrl .= 'remote=1&'; - } - $minId = $res->map(function($s) { - return ['id' => $s['id']]; - })->min('id'); - $maxId = $res->map(function($s) { - return ['id' => $s['id']]; - })->max('id'); + $since = $request->input('since_id'); + $min = $request->input('min_id'); + $max = $request->input('max_id'); + $pe = $request->filled('_pe'); - if($minId == $maxId) { - $minId = null; - } + if (! $since && ! $min && ! $max) { + $min = 1; + } - if($maxId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; - } + if ($since) { + $min = $since + 1; + } - if($minId) { - $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } + $types = $request->input('types'); - if($maxId && $minId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } + if ($request->has('types')) { + $limit = 150; + } - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res->toArray(), 200, $headers); - } + $maxId = null; + $minId = null; + AccountService::setLastActive($request->user()->id); - /** - * GET /api/v1/conversations - * - * Not implemented - * - * @return array - */ - public function conversations(Request $request) - { - abort_if(!$request->user(), 403); - $this->validate($request, [ - 'limit' => 'min:1|max:40', - 'scope' => 'nullable|in:inbox,sent,requests' - ]); + $res = $max ? + NotificationService::getMaxMastodon($pid, $max, $limit) : + NotificationService::getMinMastodon($pid, $min ?? $since, $limit); + $ids = $max ? + NotificationService::getRankedMaxId($pid, $max, $limit) : + NotificationService::getRankedMinId($pid, $min ?? $since, $limit); + if (! empty($ids)) { + $maxId = max($ids); + $minId = min($ids); + } - $limit = $request->input('limit', 20); - $scope = $request->input('scope', 'inbox'); - $pid = $request->user()->profile_id; + if (empty($res)) { + if (! Cache::has('pf:services:notifications:hasSynced:'.$pid)) { + Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600); + NotificationService::warmCache($pid, 400, true); + } + } - if(config('database.default') == 'pgsql') { - $dms = DirectMessage::when($scope === 'inbox', function($q, $scope) use($pid) { - return $q->whereIsHidden(false)->where('to_id', $pid)->orWhere('from_id', $pid); - }) - ->when($scope === 'sent', function($q, $scope) use($pid) { - return $q->whereFromId($pid)->groupBy(['to_id', 'id']); - }) - ->when($scope === 'requests', function($q, $scope) use($pid) { - return $q->whereToId($pid)->whereIsHidden(true); - }); - } else { - $dms = Conversation::when($scope === 'inbox', function($q, $scope) use($pid) { - return $q->whereIsHidden(false) - ->where('to_id', $pid) - ->orWhere('from_id', $pid) - ->orderByDesc('status_id') - ->groupBy(['to_id', 'from_id']); - }) - ->when($scope === 'sent', function($q, $scope) use($pid) { - return $q->whereFromId($pid)->groupBy('to_id'); - }) - ->when($scope === 'requests', function($q, $scope) use($pid) { - return $q->whereToId($pid)->whereIsHidden(true); - }); - } + if ($request->has('types')) { + $typesParams = collect($types)->implode('&types[]='); + $baseUrl = config('app.url').'/api/v1/notifications?types[]='.$typesParams.'&limit='.$ogLimit.'&'; + } else { + $baseUrl = config('app.url').'/api/v1/notifications?limit='.$ogLimit.'&'; + } - $dms = $dms->orderByDesc('status_id') - ->simplePaginate($limit) - ->map(function($dm) use($pid) { - $from = $pid == $dm->to_id ? $dm->from_id : $dm->to_id; - $res = [ - 'id' => $dm->id, - 'unread' => false, - 'accounts' => [ - AccountService::getMastodon($from, true) - ], - 'last_status' => StatusService::getDirectMessage($dm->status_id) - ]; - return $res; - }) - ->filter(function($dm) { - if(!$dm || empty($dm['last_status']) || !isset($dm['accounts']) || !count($dm['accounts']) || !isset($dm['accounts'][0]) || !isset($dm['accounts'][0]['id'])) { - return false; - } - return true; - }) - ->unique(function($item, $key) { - return $item['accounts'][0]['id']; - }) - ->values(); + if ($minId == $maxId) { + $minId = null; + } - return $this->json($dms); - } + $res = collect($res) + ->map(function ($n) use ($pe) { + if (! $pe) { + if ($n['type'] == 'comment') { + $n['type'] = 'mention'; - /** - * GET /api/v1/statuses/{id} - * - * @param integer $id - * - * @return StatusTransformer - */ - public function statusById(Request $request, $id) - { - abort_if(!$request->user(), 403); + return $n; + } - $pid = $request->user()->profile_id; + return $n; + } - $res = $request->has(self::PF_API_ENTITY_KEY) ? StatusService::get($id, false) : StatusService::getMastodon($id, false); - if(!$res || !isset($res['visibility'])) { - abort(404); - } + return $n; + }) + ->filter(function ($n) use ($pe) { + if (in_array($n['type'], ['mention', 'reblog', 'favourite'])) { + return isset($n['status'], $n['status']['id']); + } - $scope = $res['visibility']; - if(!in_array($scope, ['public', 'unlisted'])) { - if($scope === 'private') { - if(intval($res['account']['id']) !== intval($pid)) { - abort_unless(FollowerService::follows($pid, $res['account']['id']), 403); - } - } else { - abort(400, 'Invalid request'); - } - } + if (! $pe) { + if (in_array($n['type'], [ + 'tagged', + 'modlog', + 'story:react', + 'story:comment', + 'group:comment', + 'group:join:approved', + 'group:join:rejected', + ])) { + return false; + } - if(!empty($res['reblog']) && isset($res['reblog']['id'])) { + return isset($n['account'], $n['account']['id']); + } + + return true; + }) + ->filter(function ($n) use ($types) { + if (! $types) { + return true; + } + + return in_array($n['type'], $types); + }) + ->take($ogLimit) + ->values(); + + if ($maxId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; + } + + if ($minId) { + $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } + + if ($maxId && $minId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } + + $headers = isset($link) ? ['Link' => $link] : []; + + return $this->json($res, 200, $headers); + } + + /** + * GET /api/v1/timelines/home + * + * + * @return StatusTransformer + */ + public function timelineHome(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ + 'page' => 'sometimes|integer|max:40', + 'min_id' => 'sometimes|integer|min:0|max:'.PHP_INT_MAX, + 'max_id' => 'sometimes|integer|min:0|max:'.PHP_INT_MAX, + 'limit' => 'sometimes|integer|min:1', + 'include_reblogs' => 'sometimes', + ]); + + $napi = $request->has(self::PF_API_ENTITY_KEY); + $page = $request->input('page'); + $min = $request->input('min_id'); + $max = $request->input('max_id'); + $limit = $request->input('limit') ?? 20; + if ($limit > 40) { + $limit = 40; + } + $pid = $request->user()->profile_id; + $includeReblogs = $request->filled('include_reblogs') ? $request->boolean('include_reblogs') : false; + $nullFields = $includeReblogs ? + ['in_reply_to_id'] : + ['in_reply_to_id', 'reblog_of_id']; + $inTypes = $includeReblogs ? + ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'share'] : + ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']; + AccountService::setLastActive($request->user()->id); + + if (config('exp.cached_home_timeline')) { + $paddedLimit = $includeReblogs ? $limit + 10 : $limit + 50; + if ($min || $max) { + if ($request->has('min_id')) { + $res = HomeTimelineService::getRankedMinId($pid, $min ?? 0, $paddedLimit); + } else { + $res = HomeTimelineService::getRankedMaxId($pid, $max ?? 0, $paddedLimit); + } + } else { + $res = HomeTimelineService::get($pid, 0, $paddedLimit); + } + + if (! $res) { + $res = Cache::has('pf:services:apiv1:home:cached:coldbootcheck:'.$pid); + if (! $res) { + Cache::set('pf:services:apiv1:home:cached:coldbootcheck:'.$pid, 1, 86400); + FeedWarmCachePipeline::dispatchSync($pid); + + return response()->json([], 206); + } else { + Cache::set('pf:services:apiv1:home:cached:coldbootcheck:'.$pid, 1, 86400); + + return response()->json([], 206); + } + } + + $res = collect($res) + ->map(function ($id) use ($napi) { + return $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false); + }) + ->filter(function ($res) { + return $res && isset($res['account']); + }) + ->filter(function ($s) use ($includeReblogs) { + return $includeReblogs ? true : $s['reblog'] == null; + }) + ->take($limit) + ->map(function ($status) use ($pid) { + if ($pid) { + $status['favourited'] = (bool) LikeService::liked($pid, $status['id']); + $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } + + return $status; + }) + ->values(); + + $baseUrl = config('app.url').'/api/v1/timelines/home?limit='.$limit.'&'; + $minId = $res->map(function ($s) { + return ['id' => $s['id']]; + })->min('id'); + $maxId = $res->map(function ($s) { + return ['id' => $s['id']]; + })->max('id'); + + if ($minId == $maxId) { + $minId = null; + } + + if ($maxId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; + } + + if ($minId) { + $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } + + if ($maxId && $minId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } + + $headers = isset($link) ? ['Link' => $link] : []; + + return $this->json($res->toArray(), 200, $headers); + } + + $following = Cache::remember('profile:following:'.$pid, 1209600, function () use ($pid) { + $following = Follower::whereProfileId($pid)->pluck('following_id'); + + return $following->push($pid)->toArray(); + }); + + $muted = UserFilterService::mutes($pid); + + if ($muted && count($muted)) { + $following = array_diff($following, $muted); + } + + if ($min || $max) { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + $res = Status::select( + 'id', + 'profile_id', + 'type', + 'visibility', + 'in_reply_to_id', + 'reblog_of_id' + ) + ->where('id', $dir, $id) + ->whereNull($nullFields) + ->whereIntegerInRaw('profile_id', $following) + ->whereIn('type', $inTypes) + ->whereIn('visibility', ['public', 'unlisted', 'private']) + ->orderByDesc('id') + ->take(($limit * 2)) + ->get() + ->map(function ($s) use ($pid, $napi) { + try { + $account = $napi ? AccountService::get($s['profile_id'], true) : AccountService::getMastodon($s['profile_id'], true); + if (! $account) { + return false; + } + $status = $napi ? StatusService::get($s['id'], false) : StatusService::getMastodon($s['id'], false); + if (! $status || ! isset($status['account']) || ! isset($status['account']['id'])) { + return false; + } + } catch (\Exception $e) { + return false; + } + + $status['account'] = $account; + + if ($pid) { + $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); + $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } + + return $status; + }) + ->filter(function ($status) { + return $status && isset($status['account']); + }) + ->map(function ($status) use ($pid) { + if (! empty($status['reblog'])) { + $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); + $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } + + return $status; + }) + ->take($limit) + ->values(); + } else { + $res = Status::select( + 'id', + 'profile_id', + 'type', + 'visibility', + 'in_reply_to_id', + 'reblog_of_id', + ) + ->whereNull($nullFields) + ->whereIntegerInRaw('profile_id', $following) + ->whereIn('type', $inTypes) + ->whereIn('visibility', ['public', 'unlisted', 'private']) + ->orderByDesc('id') + ->take(($limit * 2)) + ->get() + ->map(function ($s) use ($pid, $napi) { + try { + $account = $napi ? AccountService::get($s['profile_id'], true) : AccountService::getMastodon($s['profile_id'], true); + if (! $account) { + return false; + } + $status = $napi ? StatusService::get($s['id'], false) : StatusService::getMastodon($s['id'], false); + if (! $status || ! isset($status['account']) || ! isset($status['account']['id'])) { + return false; + } + } catch (\Exception $e) { + return false; + } + + $status['account'] = $account; + + if ($pid) { + $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); + $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } + + return $status; + }) + ->filter(function ($status) { + return $status && isset($status['account']); + }) + ->map(function ($status) use ($pid) { + if (! empty($status['reblog'])) { + $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); + $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } + + return $status; + }) + ->take($limit) + ->values(); + } + + $baseUrl = config('app.url').'/api/v1/timelines/home?limit='.$limit.'&'; + $minId = $res->map(function ($s) { + return ['id' => $s['id']]; + })->min('id'); + $maxId = $res->map(function ($s) { + return ['id' => $s['id']]; + })->max('id'); + + if ($minId == $maxId) { + $minId = null; + } + + if ($maxId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; + } + + if ($minId) { + $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } + + if ($maxId && $minId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } + + $headers = isset($link) ? ['Link' => $link] : []; + + return $this->json($res->toArray(), 200, $headers); + } + + /** + * GET /api/v1/timelines/public + * + * + * @return StatusTransformer + */ + public function timelinePublic(Request $request) + { + $this->validate($request, [ + 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'limit' => 'sometimes|integer|min:1', + 'remote' => 'sometimes', + 'local' => 'sometimes', + ]); + + $napi = $request->has(self::PF_API_ENTITY_KEY); + $min = $request->input('min_id'); + $max = $request->input('max_id'); + if ($max == 0) { + $min = 1; + } + $minOrMax = $request->anyFilled(['max_id', 'min_id']); + $limit = $request->input('limit') ?? 20; + if ($limit > 40) { + $limit = 40; + } + $user = $request->user(); + + $remote = $request->has('remote') && $request->boolean('remote'); + $local = $request->boolean('local'); + $userRoleKey = $remote ? 'can-view-network-feed' : 'can-view-public-feed'; + if ($user->has_roles && ! UserRoleService::can($userRoleKey, $user->id)) { + return []; + } + $filtered = $user ? UserFilterService::filters($user->profile_id) : []; + AccountService::setLastActive($user->id); + $domainBlocks = UserFilterService::domainBlocks($user->profile_id); + $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); + $amin = SnowflakeService::byDate(now()->subDays(config('federation.network_timeline_days_falloff'))); + $asf = AdminShadowFilterService::getHideFromPublicFeedsList(); + if ($local && $remote) { + $feed = Status::select( + 'id', + 'uri', + 'type', + 'scope', + 'created_at', + 'profile_id', + 'in_reply_to_id', + 'reblog_of_id' + ) + ->when($minOrMax, function ($q, $minOrMax) use ($min, $max) { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + + return $q->where('id', $dir, $id); + }) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->when($hideNsfw, function ($q, $hideNsfw) { + return $q->where('is_nsfw', false); + }) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereScope('public') + ->where('id', '>', $amin) + ->orderByDesc('id') + ->limit(($limit * 2)) + ->pluck('id') + ->values() + ->toArray(); + } elseif ($remote && ! $local) { + if (config('instance.timeline.network.cached')) { + Cache::remember('api:v1:timelines:network:cache_check', 10368000, function () { + if (NetworkTimelineService::count() == 0) { + NetworkTimelineService::warmCache(true, config('instance.timeline.network.cache_dropoff')); + } + }); + + if ($max) { + $feed = NetworkTimelineService::getRankedMaxId($max, $limit + 5); + } elseif ($min) { + $feed = NetworkTimelineService::getRankedMinId($min, $limit + 5); + } else { + $feed = NetworkTimelineService::get(0, $limit + 5); + } + } else { + $feed = Status::select( + 'id', + 'uri', + 'type', + 'scope', + 'local', + 'created_at', + 'profile_id', + 'in_reply_to_id', + 'reblog_of_id' + ) + ->when($minOrMax, function ($q, $minOrMax) use ($min, $max) { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + + return $q->where('id', $dir, $id); + }) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->when($hideNsfw, function ($q, $hideNsfw) { + return $q->where('is_nsfw', false); + }) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereLocal(false) + ->whereScope('public') + ->where('id', '>', $amin) + ->orderByDesc('id') + ->limit(($limit * 2)) + ->pluck('id') + ->values() + ->toArray(); + } + } else { + if (config('instance.timeline.local.cached')) { + Cache::remember('api:v1:timelines:public:cache_check', 10368000, function () { + if (PublicTimelineService::count() == 0) { + PublicTimelineService::warmCache(true, 400); + } + }); + + if ($max) { + $feed = PublicTimelineService::getRankedMaxId($max, $limit + 5); + } elseif ($min) { + $feed = PublicTimelineService::getRankedMinId($min, $limit + 5); + } else { + $feed = PublicTimelineService::get(0, $limit + 5); + } + } else { + $feed = Status::select( + 'id', + 'uri', + 'type', + 'scope', + 'local', + 'created_at', + 'profile_id', + 'in_reply_to_id', + 'reblog_of_id' + ) + ->when($minOrMax, function ($q, $minOrMax) use ($min, $max) { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + + return $q->where('id', $dir, $id); + }) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->when($hideNsfw, function ($q, $hideNsfw) { + return $q->where('is_nsfw', false); + }) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereLocal(true) + ->whereScope('public') + ->where('id', '>', $amin) + ->orderByDesc('id') + ->limit(($limit * 2)) + ->pluck('id') + ->values() + ->toArray(); + } + } + + $res = collect($feed) + ->filter(function ($k) use ($min, $max) { + if (! $min && ! $max) { + return true; + } + + if ($min) { + return $min != $k; + } + + if ($max) { + return $max != $k; + } + }) + ->map(function ($k) use ($user, $napi) { + try { + $status = $napi ? StatusService::get($k) : StatusService::getMastodon($k); + if (! $status || ! isset($status['account']) || ! isset($status['account']['id'])) { + return false; + } + } catch (\Exception $e) { + return false; + } + + $account = $napi ? AccountService::get($status['account']['id'], true) : AccountService::getMastodon($status['account']['id'], true); + if (! $account) { + return false; + } + + $status['account'] = $account; + + if ($user) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $status['id']); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $status['id']); + } + + return $status; + }) + ->filter(function ($s) use ($filtered) { + return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; + }) + ->filter(function ($s) use ($domainBlocks) { + if (! $domainBlocks || ! count($domainBlocks)) { + return $s; + } + $domain = strtolower(parse_url($s['url'], PHP_URL_HOST)); + + return ! in_array($domain, $domainBlocks); + }) + ->filter(function ($s) use ($asf, $user) { + if (! $asf || count($asf) === 0) { + return true; + } + + if (in_array($s['account']['id'], $asf)) { + if ($user->profile_id == $s['account']['id']) { + return true; + } + + return false; + } + + return true; + }) + ->take($limit) + ->values(); + + $baseUrl = config('app.url').'/api/v1/timelines/public?limit='.$limit.'&'; + if ($remote) { + $baseUrl .= 'remote=1&'; + } + if ($local) { + $baseUrl .= 'local=1&'; + } + $minId = $res->map(function ($s) { + return ['id' => $s['id']]; + })->min('id'); + $maxId = $res->map(function ($s) { + return ['id' => $s['id']]; + })->max('id'); + + if ($minId == $maxId) { + $minId = null; + } + + if ($maxId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; + } + + if ($minId) { + $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } + + if ($maxId && $minId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } + + $headers = isset($link) ? ['Link' => $link] : []; + + return $this->json($res->toArray(), 200, $headers); + } + + /** + * GET /api/v1/conversations + * + * Not implemented + * + * @return array + */ + public function conversations(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ + 'limit' => 'min:1|max:40', + 'scope' => 'nullable|in:inbox,sent,requests', + ]); + + $limit = $request->input('limit', 20); + $scope = $request->input('scope', 'inbox'); + $user = $request->user(); + if ($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id)) { + return []; + } + $pid = $user->profile_id; + + if (config('database.default') == 'pgsql') { + $dms = DirectMessage::when($scope === 'inbox', function ($q, $scope) use ($pid) { + return $q->whereIsHidden(false)->where('to_id', $pid)->orWhere('from_id', $pid); + }) + ->when($scope === 'sent', function ($q, $scope) use ($pid) { + return $q->whereFromId($pid)->groupBy(['to_id', 'id']); + }) + ->when($scope === 'requests', function ($q, $scope) use ($pid) { + return $q->whereToId($pid)->whereIsHidden(true); + }); + } else { + $dms = Conversation::when($scope === 'inbox', function ($q, $scope) use ($pid) { + return $q->whereIsHidden(false) + ->where('to_id', $pid) + ->orWhere('from_id', $pid) + ->orderByDesc('status_id') + ->groupBy(['to_id', 'from_id']); + }) + ->when($scope === 'sent', function ($q, $scope) use ($pid) { + return $q->whereFromId($pid)->groupBy('to_id'); + }) + ->when($scope === 'requests', function ($q, $scope) use ($pid) { + return $q->whereToId($pid)->whereIsHidden(true); + }); + } + + $dms = $dms->orderByDesc('status_id') + ->simplePaginate($limit) + ->map(function ($dm) use ($pid) { + $from = $pid == $dm->to_id ? $dm->from_id : $dm->to_id; + $res = [ + 'id' => $dm->id, + 'unread' => false, + 'accounts' => [ + AccountService::getMastodon($from, true), + ], + 'last_status' => StatusService::getDirectMessage($dm->status_id), + ]; + + return $res; + }) + ->filter(function ($dm) { + if (! $dm || empty($dm['last_status']) || ! isset($dm['accounts']) || ! count($dm['accounts']) || ! isset($dm['accounts'][0]) || ! isset($dm['accounts'][0]['id'])) { + return false; + } + + return true; + }) + ->unique(function ($item, $key) { + return $item['accounts'][0]['id']; + }) + ->values(); + + return $this->json($dms); + } + + /** + * GET /api/v1/statuses/{id} + * + * @param int $id + * @return StatusTransformer + */ + public function statusById(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + AccountService::setLastActive($request->user()->id); + $pid = $request->user()->profile_id; + + $res = $request->has(self::PF_API_ENTITY_KEY) ? StatusService::get($id, false) : StatusService::getMastodon($id, false); + if (! $res || ! isset($res['visibility'])) { + abort(404); + } + + if ($res && isset($res['account'], $res['account']['acct'], $res['account']['url']) && strpos($res['account']['acct'], '@') != -1) { + $domain = parse_url($res['account']['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + + $scope = $res['visibility']; + if (! in_array($scope, ['public', 'unlisted'])) { + if ($scope === 'private') { + if (intval($res['account']['id']) !== intval($pid)) { + abort_unless(FollowerService::follows($pid, $res['account']['id']), 403); + } + } else { + abort(400, 'Invalid request'); + } + } + + if (! empty($res['reblog']) && isset($res['reblog']['id'])) { $res['reblog']['favourited'] = (bool) LikeService::liked($pid, $res['reblog']['id']); $res['reblog']['reblogged'] = (bool) ReblogService::get($pid, $res['reblog']['id']); $res['reblog']['bookmarked'] = BookmarkService::get($pid, $res['reblog']['id']); } - $res['favourited'] = LikeService::liked($pid, $res['id']); - $res['reblogged'] = ReblogService::get($pid, $res['id']); - $res['bookmarked'] = BookmarkService::get($pid, $res['id']); - - return $this->json($res); - } - - /** - * GET /api/v1/statuses/{id}/context - * - * @param integer $id - * - * @return StatusTransformer - */ - public function statusContext(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - $pid = $user->profile_id; - $status = StatusService::getMastodon($id, false); - - if(!$status || !isset($status['account'])) { - return response('', 404); - } - - if(intval($status['account']['id']) !== intval($user->profile_id)) { - if($status['visibility'] == 'private') { - if(!FollowerService::follows($user->profile_id, $status['account']['id'])) { - return response('', 404); - } - } else { - if(!in_array($status['visibility'], ['public','unlisted'])) { - return response('', 404); - } - } - } - - $ancestors = []; - $descendants = []; - - if($status['in_reply_to_id']) { - $ancestors[] = StatusService::getMastodon($status['in_reply_to_id'], false); - } - - if($status['replies_count']) { - $filters = UserFilterService::filters($pid); - - $descendants = DB::table('statuses') - ->where('in_reply_to_id', $id) - ->limit(20) - ->pluck('id') - ->map(function($sid) { - return StatusService::getMastodon($sid, false); - }) - ->filter(function($post) use($filters) { - return $post && isset($post['account'], $post['account']['id']) && !in_array($post['account']['id'], $filters); - }) - ->map(function($status) use($pid) { - $status['favourited'] = LikeService::liked($pid, $status['id']); - $status['reblogged'] = ReblogService::get($pid, $status['id']); - return $status; - }) - ->values(); - } - - $res = [ - 'ancestors' => $ancestors, - 'descendants' => $descendants - ]; - - return $this->json($res); - } - - /** - * GET /api/v1/statuses/{id}/card - * - * @param integer $id - * - * @return StatusTransformer - */ - public function statusCard(Request $request, $id) - { - abort_if(!$request->user(), 403); - $res = []; - return response()->json($res); - } - - /** - * GET /api/v1/statuses/{id}/reblogged_by - * - * @param integer $id - * - * @return AccountTransformer - */ - public function statusRebloggedBy(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:80' - ]); - - $limit = $request->input('limit', 10); - $user = $request->user(); - $pid = $user->profile_id; - $status = Status::findOrFail($id); - $account = AccountService::get($status->profile_id, true); - abort_if(!$account, 404); - $author = intval($status->profile_id) === intval($pid) || $user->is_admin; - $napi = $request->has(self::PF_API_ENTITY_KEY); - - abort_if( - !$status->type || - !in_array($status->type, ['photo','photo:album', 'photo:video:album', 'reply', 'text', 'video', 'video:album']), - 404, - ); - - if(!$author) { - if($status->scope == 'private') { - abort_if(!FollowerService::follows($pid, $status->profile_id), 403); - } else { - abort_if(!in_array($status->scope, ['public','unlisted']), 403); - } - - if($request->has('cursor')) { - return $this->json([]); - } - } - - $res = Status::where('reblog_of_id', $status->id) - ->orderByDesc('id') - ->cursorPaginate($limit) - ->withQueryString(); - - if(!$res) { - return $this->json([]); - } - - $headers = []; - if($author && $res->hasPages()) { - $links = ''; - if($res->onFirstPage()) { - if($res->nextPageUrl()) { - $links = '<' . $res->nextPageUrl() .'>; rel="prev"'; - } - } else { - if($res->previousPageUrl()) { - $links = '<' . $res->previousPageUrl() .'>; rel="next"'; - } - - if($res->nextPageUrl()) { - if(!empty($links)) { - $links .= ', '; - } - $links .= '<' . $res->nextPageUrl() .'>; rel="prev"'; - } - } - - $headers = ['Link' => $links]; - } - - $res = $res->map(function($status) use($pid, $napi) { - $account = $napi ? AccountService::get($status->profile_id, true) : AccountService::getMastodon($status->profile_id, true); - if(!$account) { - return false; - } - if($napi) { - $account['follows'] = $status->profile_id == $pid ? null : FollowerService::follows($pid, $status->profile_id); - } - return $account; - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values(); - - return $this->json($res, 200, $headers); - } - - /** - * GET /api/v1/statuses/{id}/favourited_by - * - * @param integer $id - * - * @return AccountTransformer - */ - public function statusFavouritedBy(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:80' - ]); - - $limit = $request->input('limit', 10); - $user = $request->user(); - $pid = $user->profile_id; - $status = Status::findOrFail($id); - $account = AccountService::get($status->profile_id, true); - abort_if(!$account, 404); - $author = intval($status->profile_id) === intval($pid) || $user->is_admin; - $napi = $request->has(self::PF_API_ENTITY_KEY); - - abort_if( - !$status->type || - !in_array($status->type, ['photo','photo:album', 'photo:video:album', 'reply', 'text', 'video', 'video:album']), - 404, - ); - - if(!$author) { - if($status->scope == 'private') { - abort_if(!FollowerService::follows($pid, $status->profile_id), 403); - } else { - abort_if(!in_array($status->scope, ['public','unlisted']), 403); - } - - if($request->has('cursor')) { - return $this->json([]); - } - } - - $res = Like::where('status_id', $status->id) - ->orderByDesc('id') - ->cursorPaginate($limit) - ->withQueryString(); - - if(!$res) { - return $this->json([]); - } - - $headers = []; - if($author && $res->hasPages()) { - $links = ''; - - if($res->onFirstPage()) { - if($res->nextPageUrl()) { - $links = '<' . $res->nextPageUrl() .'>; rel="prev"'; - } - } else { - if($res->previousPageUrl()) { - $links = '<' . $res->previousPageUrl() .'>; rel="next"'; - } - - if($res->nextPageUrl()) { - if(!empty($links)) { - $links .= ', '; - } - $links .= '<' . $res->nextPageUrl() .'>; rel="prev"'; - } - } - - $headers = ['Link' => $links]; - } - - $res = $res->map(function($like) use($pid, $napi) { - $account = $napi ? AccountService::get($like->profile_id, true) : AccountService::getMastodon($like->profile_id, true); - if(!$account) { - return false; - } - - if($napi) { - $account['follows'] = $like->profile_id == $pid ? null : FollowerService::follows($pid, $like->profile_id); - } - return $account; - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values(); - - return $this->json($res, 200, $headers); - } - - /** - * POST /api/v1/statuses - * - * - * @return StatusTransformer - */ - public function statusCreate(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'status' => 'nullable|string', - 'in_reply_to_id' => 'nullable', - 'media_ids' => 'sometimes|array|max:' . config_cache('pixelfed.max_album_length'), - 'sensitive' => 'nullable', - 'visibility' => 'string|in:private,unlisted,public', - 'spoiler_text' => 'sometimes|max:140', - 'place_id' => 'sometimes|integer|min:1|max:128769', - 'collection_ids' => 'sometimes|array|max:3', - 'comments_disabled' => 'sometimes|boolean', - ]); - - if($request->hasHeader('idempotency-key')) { - $key = 'pf:api:v1:status:idempotency-key:' . $request->user()->id . ':' . hash('sha1', $request->header('idempotency-key')); - $exists = Cache::has($key); - abort_if($exists, 400, 'Duplicate idempotency key.'); - Cache::put($key, 1, 3600); - } - - if(config('costar.enabled') == true) { - $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null && $request->status) { - $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($request->status, $kw) == true) { - abort(400, 'Invalid object. Contains banned keyword.'); - } - } - } - } - - if(!$request->filled('media_ids') && !$request->filled('in_reply_to_id')) { - abort(403, 'Empty statuses are not allowed'); - } - - $ids = $request->input('media_ids'); - $in_reply_to_id = $request->input('in_reply_to_id'); - - $user = $request->user(); - $profile = $user->profile; - - $limitKey = 'compose:rate-limit:store:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Status::whereProfileId($user->profile_id) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->where('created_at', '>', now()->subDays(1)) - ->count(); - - return $dailyLimit >= 1000; - }); - - abort_if($limitReached == true, 429); - - $visibility = $profile->is_private ? 'private' : ( - $profile->unlisted == true && - $request->input('visibility', 'public') == 'public' ? - 'unlisted' : - $request->input('visibility', 'public')); - - if($user->last_active_at == null) { - return []; - } - - $content = strip_tags($request->input('status')); - $rendered = Autolink::create()->autolink($content); - $cw = $user->profile->cw == true ? true : $request->input('sensitive', false); - $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null; - - if($in_reply_to_id) { - $parent = Status::findOrFail($in_reply_to_id); - if($parent->comments_disabled) { - return $this->json("Comments have been disabled on this post", 422); - } - $blocks = UserFilterService::blocks($parent->profile_id); - abort_if(in_array($profile->id, $blocks), 422, 'Cannot reply to this post at this time.'); - - $status = new Status; - $status->caption = $content; - $status->rendered = $rendered; - $status->scope = $visibility; - $status->visibility = $visibility; - $status->profile_id = $user->profile_id; - $status->is_nsfw = $cw; - $status->cw_summary = $spoilerText; - $status->in_reply_to_id = $parent->id; - $status->in_reply_to_profile_id = $parent->profile_id; - $status->save(); - StatusService::del($parent->id); - Cache::forget('status:replies:all:' . $parent->id); - } - - if($ids) { - if(Media::whereUserId($user->id) - ->whereNull('status_id') - ->find($ids) - ->count() == 0 - ) { - abort(400, 'Invalid media_ids'); - } - - if(!$in_reply_to_id) { - $status = new Status; - $status->caption = $content; - $status->rendered = $rendered; - $status->profile_id = $user->profile_id; - $status->is_nsfw = $cw; - $status->cw_summary = $spoilerText; - $status->scope = 'draft'; - $status->visibility = 'draft'; - if($request->has('place_id')) { - $status->place_id = $request->input('place_id'); - } - $status->save(); - } - - $mimes = []; - - foreach($ids as $k => $v) { - if($k + 1 > config_cache('pixelfed.max_album_length')) { - continue; - } - $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v); - if($m->profile_id !== $user->profile_id || $m->status_id) { - abort(403, 'Invalid media id'); - } - $m->order = $k + 1; - $m->status_id = $status->id; - $m->save(); - array_push($mimes, $m->mime); - } - - if(empty($mimes)) { - $status->delete(); - abort(400, 'Invalid media ids'); - } - - if($request->has('comments_disabled') && $request->input('comments_disabled')) { - $status->comments_disabled = true; - } - - $status->scope = $visibility; - $status->visibility = $visibility; - $status->type = StatusController::mimeTypeCheck($mimes); - $status->save(); - } - - if(!$status) { - abort(500, 'An error occured.'); - } - - NewStatusPipeline::dispatch($status); - if($status->in_reply_to_id) { - CommentPipeline::dispatch($parent, $status); - } - Cache::forget('user:account:id:'.$user->id); - Cache::forget('_api:statuses:recent_9:'.$user->profile_id); - Cache::forget('profile:status_count:'.$user->profile_id); - Cache::forget($user->storageUsedKey()); - Cache::forget('profile:embed:' . $status->profile_id); - Cache::forget($limitKey); - - if($request->has('collection_ids') && $ids) { - $collections = Collection::whereProfileId($user->profile_id) - ->find($request->input('collection_ids')) - ->each(function($collection) use($status) { - $count = $collection->items()->count(); - $item = CollectionItem::firstOrCreate([ - 'collection_id' => $collection->id, - 'object_type' => 'App\Status', - 'object_id' => $status->id - ],[ - 'order' => $count, - ]); - - CollectionService::addItem( - $collection->id, - $status->id, - $count - ); + $res['favourited'] = LikeService::liked($pid, $res['id']); + $res['reblogged'] = ReblogService::get($pid, $res['id']); + $res['bookmarked'] = BookmarkService::get($pid, $res['id']); + + return $this->json($res); + } + + /** + * GET /api/v1/statuses/{id}/context + * + * @param int $id + * @return StatusTransformer + */ + public function statusContext(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $user = $request->user(); + $pid = $user->profile_id; + $status = StatusService::getMastodon($id, false); + $pe = $request->has(self::PF_API_ENTITY_KEY); + + if (! $status || ! isset($status['account'])) { + return response('', 404); + } + + if ($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) { + $domain = parse_url($status['account']['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + + if (intval($status['account']['id']) !== intval($user->profile_id)) { + if ($status['visibility'] == 'private') { + if (! FollowerService::follows($user->profile_id, $status['account']['id'])) { + return response('', 404); + } + } else { + if (! in_array($status['visibility'], ['public', 'unlisted'])) { + return response('', 404); + } + } + } + + $ancestors = []; + $descendants = []; + + if ($status['in_reply_to_id']) { + $ancestors[] = $pe ? + StatusService::get($status['in_reply_to_id'], false) : + StatusService::getMastodon($status['in_reply_to_id'], false); + } + + if ($status['replies_count']) { + $filters = UserFilterService::filters($pid); + + $descendants = DB::table('statuses') + ->where('in_reply_to_id', $id) + ->limit(20) + ->pluck('id') + ->map(function ($sid) use ($pe) { + return $pe ? + StatusService::get($sid, false) : + StatusService::getMastodon($sid, false); + }) + ->filter(function ($post) use ($filters) { + return $post && isset($post['account'], $post['account']['id']) && ! in_array($post['account']['id'], $filters); + }) + ->map(function ($status) use ($pid) { + $status['favourited'] = LikeService::liked($pid, $status['id']); + $status['reblogged'] = ReblogService::get($pid, $status['id']); + + return $status; + }) + ->values(); + } + + $res = [ + 'ancestors' => $ancestors, + 'descendants' => $descendants, + ]; + + return $this->json($res); + } + + /** + * GET /api/v1/statuses/{id}/card + * + * @param int $id + * @return StatusTransformer + */ + public function statusCard(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $res = []; + + return response()->json($res); + } + + /** + * GET /api/v1/statuses/{id}/reblogged_by + * + * @param int $id + * @return AccountTransformer + */ + public function statusRebloggedBy(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1|max:80', + ]); + + $limit = $request->input('limit', 10); + $user = $request->user(); + $pid = $user->profile_id; + $status = Status::findOrFail($id); + $account = AccountService::get($status->profile_id, true); + abort_if(! $account, 404); + abort_if(isset($account['moved'], $account['moved']['id']), 404, 'Account moved'); + if ($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $author = intval($status->profile_id) === intval($pid) || $user->is_admin; + $napi = $request->has(self::PF_API_ENTITY_KEY); + + abort_if( + ! $status->type || + ! in_array($status->type, ['photo', 'photo:album', 'photo:video:album', 'reply', 'text', 'video', 'video:album']), + 404, + ); + + if (! $author) { + if ($status->scope == 'private') { + abort_if(! FollowerService::follows($pid, $status->profile_id), 403); + } else { + abort_if(! in_array($status->scope, ['public', 'unlisted']), 403); + } + + if ($request->has('cursor')) { + return $this->json([]); + } + } + + $res = Status::where('reblog_of_id', $status->id) + ->orderByDesc('id') + ->cursorPaginate($limit) + ->withQueryString(); + + if (! $res) { + return $this->json([]); + } + + $headers = []; + if ($author && $res->hasPages()) { + $links = ''; + if ($res->onFirstPage()) { + if ($res->nextPageUrl()) { + $links = '<'.$res->nextPageUrl().'>; rel="prev"'; + } + } else { + if ($res->previousPageUrl()) { + $links = '<'.$res->previousPageUrl().'>; rel="next"'; + } + + if ($res->nextPageUrl()) { + if (! empty($links)) { + $links .= ', '; + } + $links .= '<'.$res->nextPageUrl().'>; rel="prev"'; + } + } + + $headers = ['Link' => $links]; + } + + $res = $res->map(function ($status) use ($pid, $napi) { + $account = $napi ? AccountService::get($status->profile_id, true) : AccountService::getMastodon($status->profile_id, true); + if (! $account) { + return false; + } + if ($napi) { + $account['follows'] = $status->profile_id == $pid ? null : FollowerService::follows($pid, $status->profile_id); + } + + return $account; + }) + ->filter(function ($account) { + return $account && isset($account['id']); + }) + ->values(); + + return $this->json($res, 200, $headers); + } + + /** + * GET /api/v1/statuses/{id}/favourited_by + * + * @param int $id + * @return AccountTransformer + */ + public function statusFavouritedBy(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1', + ]); + + $limit = $request->input('limit', 40); + if ($limit > 80) { + $limit = 80; + } + $user = $request->user(); + $pid = $user->profile_id; + $status = Status::findOrFail($id); + $account = AccountService::get($status->profile_id, true); + abort_if(! $account, 404); + abort_if(isset($account['moved'], $account['moved']['id']), 404, 'Account moved'); + if ($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $author = intval($status->profile_id) === intval($pid) || $user->is_admin; + $napi = $request->has(self::PF_API_ENTITY_KEY); + + abort_if( + ! $status->type || + ! in_array($status->type, ['photo', 'photo:album', 'photo:video:album', 'reply', 'text', 'video', 'video:album']), + 404, + ); + + if (! $author) { + if ($status->scope == 'private') { + abort_if(! FollowerService::follows($pid, $status->profile_id), 403); + } else { + abort_if(! in_array($status->scope, ['public', 'unlisted']), 403); + } + + if ($request->has('cursor')) { + return $this->json([]); + } + } + + $res = Like::where('status_id', $status->id) + ->orderByDesc('id') + ->cursorPaginate($limit) + ->withQueryString(); + + if (! $res) { + return $this->json([]); + } + + $headers = []; + if ($author && $res->hasPages()) { + $links = ''; + + if ($res->onFirstPage()) { + if ($res->nextPageUrl()) { + $links = '<'.$res->nextPageUrl().'>; rel="prev"'; + } + } else { + if ($res->previousPageUrl()) { + $links = '<'.$res->previousPageUrl().'>; rel="next"'; + } + + if ($res->nextPageUrl()) { + if (! empty($links)) { + $links .= ', '; + } + $links .= '<'.$res->nextPageUrl().'>; rel="prev"'; + } + } + + $headers = ['Link' => $links]; + } + + $res = $res->map(function ($like) use ($pid, $napi) { + $account = $napi ? AccountService::get($like->profile_id, true) : AccountService::getMastodon($like->profile_id, true); + if (! $account) { + return false; + } + + if ($napi) { + $account['follows'] = $like->profile_id == $pid ? null : FollowerService::follows($pid, $like->profile_id); + } + + return $account; + }) + ->filter(function ($account) { + return $account && isset($account['id']); + }) + ->values(); + + return $this->json($res, 200, $headers); + } + + /** + * POST /api/v1/statuses + * + * + * @return StatusTransformer + */ + public function statusCreate(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $this->validate($request, [ + 'status' => 'nullable|string|max:'.(int) config_cache('pixelfed.max_caption_length'), + 'in_reply_to_id' => 'nullable', + 'media_ids' => 'sometimes|array|max:'.(int) config_cache('pixelfed.max_album_length'), + 'sensitive' => 'nullable', + 'visibility' => 'string|in:private,unlisted,public', + 'spoiler_text' => 'sometimes|max:140', + 'place_id' => 'sometimes|integer|min:1|max:128769', + 'collection_ids' => 'sometimes|array|max:3', + 'comments_disabled' => 'sometimes|boolean', + ]); + + if ($request->hasHeader('idempotency-key')) { + $key = 'pf:api:v1:status:idempotency-key:'.$request->user()->id.':'.hash('sha1', $request->header('idempotency-key')); + $exists = Cache::has($key); + abort_if($exists, 400, 'Duplicate idempotency key.'); + Cache::put($key, 1, 3600); + } + + if (config('costar.enabled') == true) { + $blockedKeywords = config('costar.keyword.block'); + if ($blockedKeywords !== null && $request->status) { + $keywords = config('costar.keyword.block'); + foreach ($keywords as $kw) { + if (Str::contains($request->status, $kw) == true) { + abort(400, 'Invalid object. Contains banned keyword.'); + } + } + } + } + + if (! $request->filled('media_ids') && ! $request->filled('in_reply_to_id')) { + abort(403, 'Empty statuses are not allowed'); + } + + $ids = $request->input('media_ids'); + $in_reply_to_id = $request->input('in_reply_to_id'); + + $user = $request->user(); + + if ($user->has_roles) { + if ($in_reply_to_id != null) { + abort_if(! UserRoleService::can('can-comment', $user->id), 403, 'Invalid permissions for this action'); + } else { + abort_if(! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + } + } + + $profile = $user->profile; + + $limitKey = 'compose:rate-limit:store:'.$user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { + $minId = SnowflakeService::byDate(now()->subDays(1)); + $dailyLimit = Status::whereProfileId($user->profile_id) + ->where('id', '>', $minId) + ->count(); + + return $dailyLimit >= 1000; + }); + + abort_if($limitReached == true, 429); + + $visibility = $profile->is_private ? 'private' : ( + $profile->unlisted == true && + $request->input('visibility', 'public') == 'public' ? + 'unlisted' : + $request->input('visibility', 'public')); + + if ($user->last_active_at == null) { + return []; + } + + $defaultCaption = config_cache('database.default') === 'mysql' ? null : ""; + $content = $request->filled('status') ? strip_tags($request->input('status')) : $defaultCaption; + $cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false); + $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null; + + if ($in_reply_to_id) { + $parent = Status::findOrFail($in_reply_to_id); + if ($parent->comments_disabled) { + return $this->json('Comments have been disabled on this post', 422); + } + $blocks = UserFilterService::blocks($parent->profile_id); + abort_if(in_array($profile->id, $blocks), 422, 'Cannot reply to this post at this time.'); + + $status = new Status; + $status->caption = $content; + $status->rendered = $defaultCaption; + $status->scope = $visibility; + $status->visibility = $visibility; + $status->profile_id = $user->profile_id; + $status->is_nsfw = $cw; + $status->cw_summary = $spoilerText; + $status->in_reply_to_id = $parent->id; + $status->in_reply_to_profile_id = $parent->profile_id; + $status->save(); + StatusService::del($parent->id); + Cache::forget('status:replies:all:'.$parent->id); + } + + if ($ids) { + if (Media::whereUserId($user->id) + ->whereNull('status_id') + ->find($ids) + ->count() == 0 + ) { + abort(400, 'Invalid media_ids'); + } + + if (! $in_reply_to_id) { + $status = new Status; + $status->caption = $content; + $status->rendered = $defaultCaption; + $status->profile_id = $user->profile_id; + $status->is_nsfw = $cw; + $status->cw_summary = $spoilerText; + $status->scope = 'draft'; + $status->visibility = 'draft'; + if ($request->has('place_id')) { + $status->place_id = $request->input('place_id'); + } + $status->save(); + } + + $mimes = []; + + foreach ($ids as $k => $v) { + if ($k + 1 > (int) config_cache('pixelfed.max_album_length')) { + continue; + } + $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v); + if ($m->profile_id !== $user->profile_id || $m->status_id) { + abort(403, 'Invalid media id'); + } + $m->order = $k + 1; + $m->status_id = $status->id; + $m->save(); + array_push($mimes, $m->mime); + } + + if (empty($mimes)) { + $status->delete(); + abort(400, 'Invalid media ids'); + } + + if ($request->has('comments_disabled') && $request->input('comments_disabled')) { + $status->comments_disabled = true; + } + + $status->scope = $visibility; + $status->visibility = $visibility; + $status->type = StatusController::mimeTypeCheck($mimes); + $status->save(); + } + + if (! $status) { + abort(500, 'An error occured.'); + } + + NewStatusPipeline::dispatch($status); + if ($status->in_reply_to_id) { + CommentPipeline::dispatch($parent, $status); + } + Cache::forget('user:account:id:'.$user->id); + Cache::forget('_api:statuses:recent_9:'.$user->profile_id); + Cache::forget('profile:status_count:'.$user->profile_id); + Cache::forget($user->storageUsedKey()); + Cache::forget('profile:embed:'.$status->profile_id); + Cache::forget($limitKey); + + if ($request->has('collection_ids') && $ids) { + $collections = Collection::whereProfileId($user->profile_id) + ->find($request->input('collection_ids')) + ->each(function ($collection) use ($status) { + $count = $collection->items()->count(); + $item = CollectionItem::firstOrCreate([ + 'collection_id' => $collection->id, + 'object_type' => 'App\Status', + 'object_id' => $status->id, + ], [ + 'order' => $count, + ]); + + CollectionService::addItem( + $collection->id, + $status->id, + $count + ); $collection->updated_at = now(); $collection->save(); CollectionService::setCollection($collection->id, $collection); - }); - } + }); + } - $res = StatusService::getMastodon($status->id, false); - $res['favourited'] = false; - $res['language'] = 'en'; - $res['bookmarked'] = false; - $res['card'] = null; - return $this->json($res); - } + $res = StatusService::getMastodon($status->id, false); + $res['favourited'] = false; + $res['language'] = 'en'; + $res['bookmarked'] = false; + $res['card'] = null; - /** - * DELETE /api/v1/statuses - * - * @param integer $id - * - * @return null - */ - public function statusDelete(Request $request, $id) - { - abort_if(!$request->user(), 403); + return $this->json($res); + } - $status = Status::whereProfileId($request->user()->profile->id) - ->findOrFail($id); + /** + * DELETE /api/v1/statuses + * + * @param int $id + * @return null + */ + public function statusDelete(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); - $resource = new Fractal\Resource\Item($status, new StatusTransformer()); + AccountService::setLastActive($request->user()->id); + $status = Status::whereProfileId($request->user()->profile->id) + ->findOrFail($id); - Cache::forget('profile:status_count:'.$status->profile_id); - StatusDelete::dispatch($status); + $resource = new Fractal\Resource\Item($status, new StatusTransformer); - $res = $this->fractal->createData($resource)->toArray(); - $res['text'] = $res['content']; - unset($res['content']); + Cache::forget('profile:status_count:'.$status->profile_id); + StatusDelete::dispatch($status); - return $this->json($res); - } + $res = $this->fractal->createData($resource)->toArray(); + $res['text'] = $res['content']; + unset($res['content']); - /** - * POST /api/v1/statuses/{id}/reblog - * - * @param integer $id - * - * @return StatusTransformer - */ - public function statusShare(Request $request, $id) - { - abort_if(!$request->user(), 403); + return $this->json($res); + } - $user = $request->user(); - $status = Status::whereScope('public')->findOrFail($id); + /** + * POST /api/v1/statuses/{id}/reblog + * + * @param int $id + * @return StatusTransformer + */ + public function statusShare(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); - if(intval($status->profile_id) !== intval($user->profile_id)) { - if($status->scope == 'private') { - abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); - } else { - abort_if(!in_array($status->scope, ['public','unlisted']), 403); - } + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); + AccountService::setLastActive($user->id); + $status = Status::whereScope('public')->findOrFail($id); + $account = AccountService::get($status->profile_id); + abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot share a post from an account that has migrated'); + if ($status && ($status->uri || $status->url || $status->object_url)) { + $url = $status->uri ?? $status->url ?? $status->object_url; + $domain = parse_url($url, PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + if (intval($status->profile_id) !== intval($user->profile_id)) { + if ($status->scope == 'private') { + abort_if(! FollowerService::follows($user->profile_id, $status->profile_id), 403); + } else { + abort_if(! in_array($status->scope, ['public', 'unlisted']), 403); + } - $blocks = UserFilterService::blocks($status->profile_id); - if($blocks && in_array($user->profile_id, $blocks)) { - abort(422); - } - } + $blocks = UserFilterService::blocks($status->profile_id); + if ($blocks && in_array($user->profile_id, $blocks)) { + abort(422); + } + } - $share = Status::firstOrCreate([ - 'profile_id' => $user->profile_id, - 'reblog_of_id' => $status->id, - 'type' => 'share', - 'in_reply_to_profile_id' => $status->profile_id, - 'scope' => 'public', - 'visibility' => 'public' - ]); + $defaultCaption = config_cache('database.default') === 'mysql' ? null : ""; + $share = Status::firstOrCreate([ + 'caption' => $defaultCaption, + 'rendered' => $defaultCaption, + 'profile_id' => $user->profile_id, + 'reblog_of_id' => $status->id, + 'type' => 'share', + 'in_reply_to_profile_id' => $status->profile_id, + 'scope' => 'public', + 'visibility' => 'public', + ]); - SharePipeline::dispatch($share)->onQueue('low'); + SharePipeline::dispatch($share)->onQueue('low'); - StatusService::del($status->id); - ReblogService::add($user->profile_id, $status->id); - $res = StatusService::getMastodon($status->id); - $res['reblogged'] = true; + StatusService::del($status->id); + ReblogService::add($user->profile_id, $status->id); + $res = StatusService::getMastodon($status->id); + $res['reblogged'] = true; + $res['favourited'] = LikeService::liked($user->profile_id, $status->id); + $res['bookmarked'] = BookmarkService::get($user->profile_id, $status->id); - return $this->json($res); - } + return $this->json($res); + } - /** - * POST /api/v1/statuses/{id}/unreblog - * - * @param integer $id - * - * @return StatusTransformer - */ - public function statusUnshare(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * POST /api/v1/statuses/{id}/unreblog + * + * @param int $id + * @return StatusTransformer + */ + public function statusUnshare(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); - $user = $request->user(); - $status = Status::whereScope('public')->findOrFail($id); + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); + AccountService::setLastActive($user->id); + $status = Status::whereScope('public')->findOrFail($id); + $account = AccountService::get($status->profile_id); + abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot unshare a post from an account that has migrated'); - if(intval($status->profile_id) !== intval($user->profile_id)) { - if($status->scope == 'private') { - abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); - } else { - abort_if(!in_array($status->scope, ['public','unlisted']), 403); - } - } + if (intval($status->profile_id) !== intval($user->profile_id)) { + if ($status->scope == 'private') { + abort_if(! FollowerService::follows($user->profile_id, $status->profile_id), 403); + } else { + abort_if(! in_array($status->scope, ['public', 'unlisted']), 403); + } + } - $reblog = Status::whereProfileId($user->profile_id) - ->whereReblogOfId($status->id) - ->first(); + $reblog = Status::whereProfileId($user->profile_id) + ->whereReblogOfId($status->id) + ->first(); - if(!$reblog) { - $res = StatusService::getMastodon($status->id); - $res['reblogged'] = false; - return $this->json($res); - } + if (! $reblog) { + $res = StatusService::getMastodon($status->id); + $res['reblogged'] = false; - UndoSharePipeline::dispatch($reblog)->onQueue('low'); - ReblogService::del($user->profile_id, $status->id); + return $this->json($res); + } - $res = StatusService::getMastodon($status->id); - $res['reblogged'] = false; + UndoSharePipeline::dispatch($reblog)->onQueue('low'); + ReblogService::del($user->profile_id, $status->id); - return $this->json($res); - } + $res = StatusService::getMastodon($status->id); + $res['reblogged'] = false; + $res['favourited'] = LikeService::liked($user->profile_id, $status->id); + $res['bookmarked'] = BookmarkService::get($user->profile_id, $status->id); - /** - * GET /api/v1/timelines/tag/{hashtag} - * - * @param string $hashtag - * - * @return StatusTransformer - */ - public function timelineHashtag(Request $request, $hashtag) - { - abort_if(!$request->user(), 403); + return $this->json($res); + } - $this->validate($request,[ - 'page' => 'nullable|integer|max:40', - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|max:100', - 'only_media' => 'sometimes|boolean', - '_pe' => 'sometimes' - ]); + /** + * GET /api/v1/timelines/tag/{hashtag} + * + * @param string $hashtag + * @return StatusTransformer + */ + public function timelineHashtag(Request $request, $hashtag) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - if(config('database.default') === 'pgsql') { - $tag = Hashtag::where('name', 'ilike', $hashtag) - ->orWhere('slug', 'ilike', $hashtag) - ->first(); - } else { - $tag = Hashtag::whereName($hashtag) - ->orWhere('slug', $hashtag) - ->first(); - } + $this->validate($request, [ + 'page' => 'nullable|integer|max:40', + 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'limit' => 'sometimes|integer|min:1', + 'only_media' => 'sometimes', + '_pe' => 'sometimes', + ]); - if(!$tag) { - return response()->json([]); - } + $user = $request->user(); + abort_if( + $user->has_roles && ! UserRoleService::can('can-view-hashtag-feed', $user->id), + 403, + 'Invalid permissions for this action' + ); - if($tag->is_banned == true) { - return $this->json([]); - } + if (config('database.default') === 'pgsql') { + $tag = Hashtag::where('name', 'ilike', $hashtag) + ->orWhere('slug', 'ilike', $hashtag) + ->first(); + } else { + $tag = Hashtag::whereName($hashtag) + ->orWhere('slug', $hashtag) + ->first(); + } - $min = $request->input('min_id'); - $max = $request->input('max_id'); - $limit = $request->input('limit', 20); - $onlyMedia = $request->input('only_media', true); - $pe = $request->has(self::PF_API_ENTITY_KEY); + if (! $tag) { + return response()->json([]); + } - if($min || $max) { - $minMax = SnowflakeService::byDate(now()->subMonths(6)); - if($min && intval($min) < $minMax) { - return []; - } - if($max && intval($max) < $minMax) { - return []; - } - } + if ($tag->is_banned == true) { + return $this->json([]); + } - $filters = UserFilterService::filters($request->user()->profile_id); + $min = $request->input('min_id'); + $max = $request->input('max_id'); + $limit = $request->input('limit', 20); + if ($limit > 40) { + $limit = 40; + } + $onlyMedia = $request->boolean('only_media', true); + $pe = $request->has(self::PF_API_ENTITY_KEY); + $pid = $request->user()->profile_id; - if(!$min && !$max) { - $id = 1; - $dir = '>'; - } else { - $dir = $min ? '>' : '<'; - $id = $min ?? $max; - } + if ($min || $max) { + $minMax = SnowflakeService::byDate(now()->subMonths(6)); + if ($min && intval($min) < $minMax) { + return []; + } + if ($max && intval($max) < $minMax) { + return []; + } + } - $res = StatusHashtag::whereHashtagId($tag->id) - ->whereStatusVisibility('public') - ->where('status_id', $dir, $id) - ->orderBy('status_id', 'desc') - ->limit($limit) - ->pluck('status_id') - ->map(function ($i) use($pe) { - return $pe ? StatusService::get($i) : StatusService::getMastodon($i); - }) - ->filter(function($i) use($onlyMedia) { - if(!$i) { - return false; - } - if($onlyMedia && !isset($i['media_attachments']) || !count($i['media_attachments'])) { - return false; - } - return $i && isset($i['account']); - }) - ->filter(function($i) use($filters) { - return !in_array($i['account']['id'], $filters); - }) - ->values() - ->toArray(); + $filters = UserFilterService::filters($pid); + $domainBlocks = UserFilterService::domainBlocks($pid); - return $this->json($res); - } + if (! $min && ! $max) { + $id = 1; + $dir = '>'; + } else { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + } - /** - * GET /api/v1/bookmarks - * - * - * - * @return StatusTransformer - */ - public function bookmarks(Request $request) - { - abort_if(!$request->user(), 403); + $res = StatusHashtag::whereHashtagId($tag->id) + ->where('status_id', $dir, $id) + ->orderBy('status_id', 'desc') + ->limit(100) + ->pluck('status_id') + ->map(function ($i) use ($pe) { + return $pe ? StatusService::get($i, false) : StatusService::getMastodon($i, false); + }) + ->filter(function ($i) use ($onlyMedia, $pid) { + if (! $i || ! isset($i['account'], $i['account']['id'])) { + return false; + } + if ($i['visibility'] === 'unlisted') { + if ((int) $i['account']['id'] !== $pid) { + return false; + } + } + // if ($i['visibility'] === 'private') { + // if ((int) $i['account']['id'] !== $pid) { + // return FollowerService::follows($pid, $i['account']['id'], true); + // } + // } + if ($onlyMedia == true) { + if (! isset($i['media_attachments']) || ! count($i['media_attachments'])) { + return false; + } + } - $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40', - 'max_id' => 'nullable|integer|min:0', - 'since_id' => 'nullable|integer|min:0', - 'min_id' => 'nullable|integer|min:0' - ]); + return $i && isset($i['account'], $i['url']); + }) + ->filter(function ($i) use ($filters, $domainBlocks) { + $domain = strtolower(parse_url($i['url'], PHP_URL_HOST)); - $pe = $request->has('_pe'); - $pid = $request->user()->profile_id; - $limit = $request->input('limit') ?? 20; - $max_id = $request->input('max_id'); - $since_id = $request->input('since_id'); - $min_id = $request->input('min_id'); + return ! in_array($i['account']['id'], $filters) && ! in_array($domain, $domainBlocks); + }) + ->take($limit) + ->values() + ->toArray(); - $dir = $min_id ? '>' : '<'; - $id = $min_id ?? $max_id; + return $this->json($res); + } - $bookmarkQuery = Bookmark::whereProfileId($pid) + /** + * GET /api/v1/bookmarks + * + * + * + * @return StatusTransformer + */ + public function bookmarks(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1', + 'max_id' => 'nullable|integer|min:0', + 'since_id' => 'nullable|integer|min:0', + 'min_id' => 'nullable|integer|min:0', + ]); + + $pe = $request->has('_pe'); + $pid = $request->user()->profile_id; + $limit = $request->input('limit') ?? 20; + if ($limit > 40) { + $limit = 40; + } + $max_id = $request->input('max_id'); + $since_id = $request->input('since_id'); + $min_id = $request->input('min_id'); + + $dir = $min_id ? '>' : '<'; + $id = $min_id ?? $max_id; + + $bookmarkQuery = Bookmark::whereProfileId($pid) ->orderByDesc('id') ->cursorPaginate($limit); - $bookmarks = $bookmarkQuery->map(function($bookmark) use($pid, $pe) { - $status = $pe ? StatusService::get($bookmark->status_id, false) : StatusService::getMastodon($bookmark->status_id, false); + $bookmarks = $bookmarkQuery->map(function ($bookmark) use ($pid, $pe) { + $status = $pe ? StatusService::get($bookmark->status_id, false) : StatusService::getMastodon($bookmark->status_id, false); - if($status) { - $status['bookmarked'] = true; - $status['favourited'] = LikeService::liked($pid, $status['id']); - $status['reblogged'] = ReblogService::get($pid, $status['id']); - } - return $status; - }) - ->filter() - ->values() - ->toArray(); + if ($status) { + $status['bookmarked'] = true; + $status['favourited'] = LikeService::liked($pid, $status['id']); + $status['reblogged'] = ReblogService::get($pid, $status['id']); + } + + return $status; + }) + ->filter() + ->values() + ->toArray(); $links = null; $headers = []; - if($bookmarkQuery->nextCursor()) { + if ($bookmarkQuery->nextCursor()) { $links .= '<'.$bookmarkQuery->nextPageUrl().'&limit='.$limit.'>; rel="next"'; } - if($bookmarkQuery->previousCursor()) { - if($links != null) { + if ($bookmarkQuery->previousCursor()) { + if ($links != null) { $links .= ', '; } $links .= '<'.$bookmarkQuery->previousPageUrl().'&limit='.$limit.'>; rel="prev"'; } - if($links) { + if ($links) { $headers = ['Link' => $links]; } - return $this->json($bookmarks, 200, $headers); - } + return $this->json($bookmarks, 200, $headers); + } - /** - * POST /api/v1/statuses/{id}/bookmark - * - * - * - * @return StatusTransformer - */ - public function bookmarkStatus(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * POST /api/v1/statuses/{id}/bookmark + * + * + * + * @return StatusTransformer + */ + public function bookmarkStatus(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); - $status = Status::findOrFail($id); - $pid = $request->user()->profile_id; + $status = Status::findOrFail($id); + $user = $request->user(); + $pid = $request->user()->profile_id; + $account = AccountService::get($status->profile_id); + abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot bookmark a post from an account that has migrated'); + abort_if($user->has_roles && ! UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action'); + abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); + abort_if(! in_array($status->scope, ['public', 'unlisted', 'private']), 404); + abort_if(! in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']), 404); - abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); - abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); - abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); + if ($status->scope == 'private') { + abort_if( + $pid !== $status->profile_id && ! FollowerService::follows($pid, $status->profile_id), + 404, + 'Error: You cannot bookmark private posts from accounts you do not follow.' + ); + } - if($status->scope == 'private') { - abort_if( - $pid !== $status->profile_id && !FollowerService::follows($pid, $status->profile_id), - 404, - 'Error: You cannot bookmark private posts from accounts you do not follow.' - ); - } + Bookmark::firstOrCreate([ + 'status_id' => $status->id, + 'profile_id' => $pid, + ]); - Bookmark::firstOrCreate([ - 'status_id' => $status->id, - 'profile_id' => $pid - ]); + BookmarkService::add($pid, $status->id); - BookmarkService::add($pid, $status->id); + $res = StatusService::getMastodon($status->id, false); + $res['bookmarked'] = true; - $res = StatusService::getMastodon($status->id, false); - $res['bookmarked'] = true; + return $this->json($res); + } - return $this->json($res); - } + /** + * POST /api/v1/statuses/{id}/unbookmark + * + * + * + * @return StatusTransformer + */ + public function unbookmarkStatus(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); - /** - * POST /api/v1/statuses/{id}/unbookmark - * - * - * - * @return StatusTransformer - */ - public function unbookmarkStatus(Request $request, $id) - { - abort_if(!$request->user(), 403); + $status = Status::findOrFail($id); + $pid = $request->user()->profile_id; + $user = $request->user(); - $status = Status::findOrFail($id); - $pid = $request->user()->profile_id; + abort_if($user->has_roles && ! UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action'); + abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); + abort_if(! in_array($status->scope, ['public', 'unlisted', 'private']), 404); + abort_if(! in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']), 404); - abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); - abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); - abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); + $bookmark = Bookmark::whereStatusId($status->id) + ->whereProfileId($pid) + ->first(); - $bookmark = Bookmark::whereStatusId($status->id) - ->whereProfileId($pid) - ->first(); + if ($bookmark) { + BookmarkService::del($pid, $status->id); + $bookmark->delete(); + } + $res = StatusService::getMastodon($status->id, false); + $res['bookmarked'] = false; - if($bookmark) { - BookmarkService::del($pid, $status->id); - $bookmark->delete(); - } - $res = StatusService::getMastodon($status->id, false); - $res['bookmarked'] = false; + return $this->json($res); + } - return $this->json($res); - } + /** + * GET /api/v1/discover/posts + * + * + * @return array + */ + public function discoverPosts(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - /** - * GET /api/v1/discover/posts - * - * - * @return array - */ - public function discoverPosts(Request $request) - { - abort_if(!$request->user(), 403); + $this->validate($request, [ + 'limit' => 'integer|min:1|max:40', + ]); - $this->validate($request, [ - 'limit' => 'integer|min:1|max:40' - ]); - - $limit = $request->input('limit', 40); - $pid = $request->user()->profile_id; - $filters = UserFilterService::filters($pid); - $forYou = DiscoverService::getForYou(); - $posts = $forYou->take(50)->map(function($post) { - return StatusService::getMastodon($post); - }) - ->filter(function($post) use($filters) { - return $post && - isset($post['account']) && - isset($post['account']['id']) && - !in_array($post['account']['id'], $filters); - }) - ->take(12) - ->values(); - return $this->json(compact('posts')); - } - - /** - * GET /api/v2/statuses/{id}/replies - * - * - * @return array - */ - public function statusReplies(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'int|min:1|max:10', - 'sort' => 'in:all,newest,popular' - ]); - - $limit = $request->input('limit', 3); - $pid = $request->user()->profile_id; - $status = StatusService::getMastodon($id, false); - - abort_if(!$status, 404); - - if($status['visibility'] == 'private') { - if($pid != $status['account']['id']) { - abort_unless(FollowerService::follows($pid, $status['account']['id']), 404); - } - } - - $sortBy = $request->input('sort', 'all'); - - if($sortBy == 'all' && isset($status['replies_count']) && $status['replies_count'] && $request->has('refresh_cache')) { - if(!Cache::has('status:replies:all-rc:' . $id)) { - Cache::forget('status:replies:all:' . $id); - Cache::put('status:replies:all-rc:' . $id, true, 300); - } - } - - if($sortBy == 'all' && !$request->has('cursor')) { - $ids = Cache::remember('status:replies:all:' . $id, 3600, function() use($id) { - return DB::table('statuses') - ->where('in_reply_to_id', $id) - ->orderBy('id') - ->cursorPaginate(3); - }); - } else { - $ids = DB::table('statuses') - ->where('in_reply_to_id', $id) - ->when($sortBy, function($q, $sortBy) { - if($sortBy === 'all') { - return $q->orderBy('id'); - } - - if($sortBy === 'newest') { - return $q->orderByDesc('created_at'); - } - - if($sortBy === 'popular') { - return $q->orderByDesc('likes_count'); - } - }) - ->cursorPaginate($limit); - } - - $filters = UserFilterService::filters($pid); - $data = $ids->filter(function($post) use($filters) { - return !in_array($post->profile_id, $filters); - }) - ->map(function($post) use($pid) { - $status = StatusService::get($post->id, false); - - if(!$status || !isset($status['id'])) { - return false; - } - - $status['favourited'] = LikeService::liked($pid, $post->id); - return $status; - }) - ->map(function($post) { - if(isset($post['account']) && isset($post['account']['id'])) { - $account = AccountService::get($post['account']['id'], true); - $post['account'] = $account; - } - return $post; - }) - ->filter(function($post) { - return $post && isset($post['id']) && isset($post['account']) && isset($post['account']['id']); - }) - ->values(); - - $res = [ - 'data' => $data, - 'next' => $ids->nextPageUrl() - ]; - - return $this->json($res); - } - - /** - * GET /api/v2/statuses/{id}/state - * - * - * @return array - */ - public function statusState(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $status = Status::findOrFail($id); - $pid = $request->user()->profile_id; - abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); - - return $this->json(StatusService::getState($status->id, $pid)); - } - - /** - * GET /api/v1.1/discover/accounts/popular - * - * - * @return array - */ - public function discoverAccountsPopular(Request $request) - { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - - $ids = Cache::remember('api:v1.1:discover:accounts:popular', 86400, function() { - return DB::table('profiles') - ->where('is_private', false) - ->whereNull('status') - ->orderByDesc('profiles.followers_count') - ->limit(20) - ->get(); - }); - - $ids = $ids->map(function($profile) { - return AccountService::get($profile->id, true); - }) - ->filter(function($profile) use($pid) { - return $profile && isset($profile['id']); - }) - ->filter(function($profile) use($pid) { - return $profile['id'] != $pid; - }) - ->map(function($profile) { - $ids = collect(ProfileStatusService::get($profile['id'], 0, 9)) - ->map(function($id) { - return StatusService::get($id, true); - }) - ->filter(function($post) { - return $post && isset($post['id']); - }) - ->take(3) - ->values(); - $profile['recent_posts'] = $ids; - return $profile; + $limit = $request->input('limit', 40); + $pid = $request->user()->profile_id; + $filters = UserFilterService::filters($pid); + $forYou = DiscoverService::getForYou(); + $posts = $forYou->take(50)->map(function ($post) { + return StatusService::getMastodon($post); }) - ->take(6) - ->values(); + ->filter(function ($post) use ($filters) { + return $post && + isset($post['account']) && + isset($post['account']['id']) && + ! in_array($post['account']['id'], $filters); + }) + ->take(12) + ->values(); - return $this->json($ids); - } + return $this->json(compact('posts')); + } - /** - * GET /api/v1/preferences - * - * - * @return array - */ - public function getPreferences(Request $request) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v2/statuses/{id}/replies + * + * + * @return array + */ + public function statusReplies(Request $request, $id) + { + abort_if(! $request->user(), 403); - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1', + 'sort' => 'in:all,newest,popular', + ]); - return $this->json([ - 'posting:default:visibility' => $account['locked'] ? 'private' : 'public', - 'posting:default:sensitive' => false, - 'posting:default:language' => null, - 'reading:expand:media' => 'default', - 'reading:expand:spoilers' => false - ]); - } + $limit = $request->input('limit', 3); + if ($limit > 10) { + $limit = 10; + } + $pid = $request->user()->profile_id; + $status = StatusService::getMastodon($id, false); + abort_if(! $status, 404); + abort_if(isset($status['account'], $account['account']['moved']['id']), 404, 'Account moved'); - /** - * GET /api/v1/trends - * - * - * @return array - */ - public function getTrends(Request $request) - { - abort_if(!$request->user(), 403); + if ($status['visibility'] == 'private') { + if ($pid != $status['account']['id']) { + abort_unless(FollowerService::follows($pid, $status['account']['id']), 404); + } + } - return $this->json([]); - } + $sortBy = $request->input('sort', 'all'); - /** - * GET /api/v1/announcements - * - * - * @return array - */ - public function getAnnouncements(Request $request) - { - abort_if(!$request->user(), 403); + if ($sortBy == 'all' && isset($status['replies_count']) && $status['replies_count'] && $request->has('refresh_cache')) { + if (! Cache::has('status:replies:all-rc:'.$id)) { + Cache::forget('status:replies:all:'.$id); + Cache::put('status:replies:all-rc:'.$id, true, 300); + } + } - return $this->json([]); - } + if ($sortBy == 'all' && ! $request->has('cursor')) { + $ids = Cache::remember('status:replies:all:'.$id, 3600, function () use ($id) { + return DB::table('statuses') + ->where('in_reply_to_id', $id) + ->orderBy('id') + ->cursorPaginate(3); + }); + } else { + $ids = DB::table('statuses') + ->where('in_reply_to_id', $id) + ->when($sortBy, function ($q, $sortBy) { + if ($sortBy === 'all') { + return $q->orderBy('id'); + } - /** - * GET /api/v1/markers - * - * - * @return array - */ - public function getMarkers(Request $request) - { - abort_if(!$request->user(), 403); + if ($sortBy === 'newest') { + return $q->orderByDesc('created_at'); + } - $type = $request->input('timeline'); - if(is_array($type)) { - $type = $type[0]; - } - if(!$type || !in_array($type, ['home', 'notifications'])) { - return $this->json([]); - } - $pid = $request->user()->profile_id; - return $this->json(MarkerService::get($pid, $type)); - } + if ($sortBy === 'popular') { + return $q->orderByDesc('likes_count'); + } + }) + ->cursorPaginate($limit); + } - /** - * POST /api/v1/markers - * - * - * @return array - */ - public function setMarkers(Request $request) - { - abort_if(!$request->user(), 403); + $filters = UserFilterService::filters($pid); + $data = $ids->filter(function ($post) use ($filters) { + return ! in_array($post->profile_id, $filters); + }) + ->map(function ($post) use ($pid) { + $status = StatusService::get($post->id, false); - $pid = $request->user()->profile_id; - $home = $request->input('home[last_read_id]'); - $notifications = $request->input('notifications[last_read_id]'); + if (! $status || ! isset($status['id'])) { + return false; + } - if($home) { - return $this->json(MarkerService::set($pid, 'home', $home)); - } + $status['favourited'] = LikeService::liked($pid, $post->id); - if($notifications) { - return $this->json(MarkerService::set($pid, 'notifications', $notifications)); - } + return $status; + }) + ->map(function ($post) { + if (isset($post['account']) && isset($post['account']['id'])) { + $account = AccountService::get($post['account']['id'], true); + $post['account'] = $account; + } - return $this->json([]); - } + return $post; + }) + ->filter(function ($post) { + return $post && isset($post['id']) && isset($post['account']) && isset($post['account']['id']); + }) + ->values(); - /** - * GET /api/v1/followed_tags - * - * - * @return array - */ - public function getFollowedTags(Request $request) - { - abort_if(!$request->user(), 403); + $res = [ + 'data' => $data, + 'next' => $ids->nextPageUrl(), + ]; - $account = AccountService::get($request->user()->profile_id); + return $this->json($res); + } - $this->validate($request, [ - 'cursor' => 'sometimes', - 'limit' => 'sometimes|integer|min:1|max:200' - ]); - $limit = $request->input('limit', 100); + /** + * GET /api/v2/statuses/{id}/state + * + * + * @return array + */ + public function statusState(Request $request, $id) + { + abort_if(! $request->user(), 403); - $res = HashtagFollow::whereProfileId($account['id']) - ->orderByDesc('id') - ->cursorPaginate($limit)->withQueryString(); + $status = StatusService::get($id, false, true); + abort_if(! $status, 404); + abort_if(! in_array($status['visibility'], ['public', 'unlisted', 'private']), 404); - $pagination = false; - $prevPage = $res->nextPageUrl(); - $nextPage = $res->previousPageUrl(); - if($nextPage && $prevPage) { - $pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"'; - } else if($nextPage && !$prevPage) { - $pagination = '<' . $nextPage . '>; rel="next"'; - } else if(!$nextPage && $prevPage) { - $pagination = '<' . $prevPage . '>; rel="prev"'; - } + return $this->json(StatusService::getState($status['id'], $request->user()->profile_id)); + } - if($pagination) { - return response()->json(FollowedTagResource::collection($res)->collection) - ->header('Link', $pagination); - } - return response()->json(FollowedTagResource::collection($res)->collection); - } + /** + * GET /api/v1.1/discover/accounts/popular + * + * + * @return array + */ + public function discoverAccountsPopular(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - /** - * POST /api/v1/tags/:id/follow - * - * - * @return object - */ - public function followHashtag(Request $request, $id) - { - abort_if(!$request->user(), 403); + $pid = $request->user()->profile_id; - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); + $ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () { + return DB::table('profiles') + ->where('is_private', false) + ->whereNull('status') + ->orderByDesc('profiles.followers_count') + ->limit(30) + ->get(); + }); + $filters = UserFilterService::filters($pid); + $asf = AdminShadowFilterService::getHideFromPublicFeedsList(); + $ids = $ids->map(function ($profile) { + return AccountService::get($profile->id, true); + }) + ->filter(function ($profile) { + return $profile && isset($profile['id'], $profile['locked']) && ! $profile['locked']; + }) + ->filter(function ($profile) use ($pid) { + return $profile['id'] != $pid; + }) + ->filter(function ($profile) use ($pid) { + return ! FollowerService::follows($pid, $profile['id'], true); + }) + ->filter(function ($profile) use ($asf) { + return ! in_array($profile['id'], $asf); + }) + ->filter(function ($profile) use ($filters) { + return ! in_array($profile['id'], $filters); + }) + ->take(16) + ->values(); - $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; - $tag = Hashtag::where('name', $operator, $id) - ->orWhere('slug', $operator, $id) - ->first(); + return $this->json($ids); + } - abort_if(!$tag, 422, 'Unknown hashtag'); + /** + * GET /api/v1/preferences + * + * + * @return array + */ + public function getPreferences(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - abort_if( - HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT, - 422, - 'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.' - ); + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); - $follows = HashtagFollow::updateOrCreate( - [ - 'profile_id' => $account['id'], - 'hashtag_id' => $tag->id - ], - [ - 'user_id' => $request->user()->id - ] - ); + return $this->json([ + 'posting:default:visibility' => $account['locked'] ? 'private' : 'public', + 'posting:default:sensitive' => false, + 'posting:default:language' => null, + 'reading:expand:media' => 'default', + 'reading:expand:spoilers' => false, + ]); + } - HashtagService::follow($pid, $tag->id); + /** + * GET /api/v1/trends + * + * + * @return array + */ + public function getTrends(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - return response()->json(FollowedTagResource::make($follows)->toArray($request)); - } + return $this->json([]); + } - /** - * POST /api/v1/tags/:id/unfollow - * - * - * @return object - */ - public function unfollowHashtag(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v1/announcements + * + * + * @return array + */ + public function getAnnouncements(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); + return $this->json([]); + } - $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; - $tag = Hashtag::where('name', $operator, $id) - ->orWhere('slug', $operator, $id) - ->first(); + /** + * GET /api/v1/markers + * + * + * @return array + */ + public function getMarkers(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - abort_if(!$tag, 422, 'Unknown hashtag'); + $type = $request->input('timeline'); + if (is_array($type)) { + $type = $type[0]; + } + if (! $type || ! in_array($type, ['home', 'notifications'])) { + return $this->json([]); + } + $pid = $request->user()->profile_id; - $follows = HashtagFollow::whereProfileId($pid) - ->whereHashtagId($tag->id) - ->first(); + return $this->json(MarkerService::get($pid, $type)); + } - if(!$follows) { - return [ - 'name' => $tag->name, - 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, - 'history' => [], - 'following' => false - ]; - } + /** + * POST /api/v1/markers + * + * + * @return array + */ + public function setMarkers(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); - if($follows) { - HashtagService::unfollow($pid, $tag->id); - $follows->delete(); - } + $pid = $request->user()->profile_id; + $home = $request->input('home[last_read_id]'); + $notifications = $request->input('notifications[last_read_id]'); - $res = FollowedTagResource::make($follows)->toArray($request); - $res['following'] = false; - return response()->json($res); - } + if ($home) { + return $this->json(MarkerService::set($pid, 'home', $home)); + } - /** - * GET /api/v1/tags/:id - * - * - * @return object - */ - public function getHashtag(Request $request, $id) - { - abort_if(!$request->user(), 403); + if ($notifications) { + return $this->json(MarkerService::set($pid, 'notifications', $notifications)); + } - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); - $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; - $tag = Hashtag::where('name', $operator, $id) - ->orWhere('slug', $operator, $id) - ->first(); + return $this->json([]); + } - if(!$tag) { - return [ - 'name' => $id, - 'url' => config('app.url') . '/i/web/hashtag/' . $id, - 'history' => [], - 'following' => false - ]; - } + /** + * GET /api/v1/instance/peers + * + * + * @return array + */ + public function instancePeers(Request $request) + { + if ((bool) config('instance.show_peers') == false) { + return $this->json([]); + } - $res = [ - 'name' => $tag->name, - 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, - 'history' => [], - 'following' => HashtagService::isFollowing($pid, $tag->id) - ]; - - if($request->has(self::PF_API_ENTITY_KEY)) { - $res['count'] = HashtagService::count($tag->id); - } - - return $this->json($res); - } + return $this->json( + Cache::remember(InstanceService::CACHE_KEY_API_PEERS_LIST, now()->addHours(24), function () { + return Instance::whereNotNull('nodeinfo_last_fetched') + ->whereBanned(false) + ->where('nodeinfo_last_fetched', '>', now()->subDays(8)) + ->pluck('domain'); + }) + ); + } } diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php index 298deb705..bb82521b6 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php @@ -2,891 +2,929 @@ namespace App\Http\Controllers\Api; -use Cache; -use DB; -use App\Http\Controllers\Controller; -use Illuminate\Http\Request; -use League\Fractal; -use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\AccountLog; use App\EmailVerification; -use App\Follower; +use App\Http\Controllers\Controller; +use App\Http\Controllers\StatusController; +use App\Http\Resources\StatusStateless; +use App\Jobs\ImageOptimizePipeline\ImageOptimize; +use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail; +use App\Jobs\StatusPipeline\NewStatusPipeline; +use App\Jobs\StatusPipeline\RemoteStatusDelete; +use App\Jobs\StatusPipeline\StatusDelete; +use App\Jobs\VideoPipeline\VideoThumbnail; +use App\Mail\ConfirmAppEmail; +use App\Mail\PasswordChange; +use App\Media; use App\Place; -use App\Status; -use App\Report; use App\Profile; +use App\Report; +use App\Rules\ExpoPushTokenRule; +use App\Services\AccountService; +use App\Services\BouncerService; +use App\Services\EmailService; +use App\Services\FollowerService; +use App\Services\MediaBlocklistService; +use App\Services\MediaPathService; +use App\Services\NetworkTimelineService; +use App\Services\NotificationAppGatewayService; +use App\Services\ProfileStatusService; +use App\Services\PublicTimelineService; +use App\Services\PushNotificationService; +use App\Services\StatusService; +use App\Services\UserStorageService; +use App\Status; use App\StatusArchived; use App\User; use App\UserSetting; -use App\Services\AccountService; -use App\Services\StatusService; -use App\Services\ProfileStatusService; -use App\Services\LikeService; -use App\Services\ReblogService; -use App\Services\PublicTimelineService; -use App\Services\NetworkTimelineService; use App\Util\Lexer\RestrictedNames; -use App\Services\BouncerService; -use App\Services\EmailService; -use Illuminate\Support\Str; +use Cache; +use DB; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; -use Jenssegers\Agent\Agent; -use Mail; -use App\Mail\PasswordChange; -use App\Mail\ConfirmAppEmail; -use App\Http\Resources\StatusStateless; -use App\Jobs\StatusPipeline\StatusDelete; -use App\Jobs\StatusPipeline\RemoteStatusDelete; -use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail; use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Support\Str; +use Jenssegers\Agent\Agent; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; +use Mail; +use Purify; class ApiV1Dot1Controller extends Controller { - protected $fractal; - - public function __construct() - { - $this->fractal = new Fractal\Manager(); - $this->fractal->setSerializer(new ArraySerializer()); - } - - public function json($res, $code = 200, $headers = []) - { - return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); - } - - public function error($msg, $code = 400, $extra = [], $headers = []) - { - $res = [ - "msg" => $msg, - "code" => $code - ]; - return response()->json(array_merge($res, $extra), $code, $headers, JSON_UNESCAPED_SLASHES); - } - - public function report(Request $request) - { - $user = $request->user(); - - abort_if(!$user, 403); - abort_if($user->status != null, 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $report_type = $request->input('report_type'); - $object_id = $request->input('object_id'); - $object_type = $request->input('object_type'); - - $types = [ - 'spam', - 'sensitive', - 'abusive', - 'underage', - 'violence', - 'copyright', - 'impersonation', - 'scam', - 'terrorism' - ]; - - if (!$report_type || !$object_id || !$object_type) { - return $this->error("Invalid or missing parameters", 400, ["error_code" => "ERROR_INVALID_PARAMS"]); - } - - if (!in_array($report_type, $types)) { - return $this->error("Invalid report type", 400, ["error_code" => "ERROR_TYPE_INVALID"]); - } - - if ($object_type === "user" && $object_id == $user->profile_id) { - return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]); - } - - $rpid = null; - - switch ($object_type) { - case 'post': - $object = Status::find($object_id); - if (!$object) { - return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]); - } - $object_type = 'App\Status'; - $exists = Report::whereUserId($user->id) - ->whereObjectId($object->id) - ->whereObjectType('App\Status') - ->count(); - - $rpid = $object->profile_id; - break; - - case 'user': - $object = Profile::find($object_id); - if (!$object) { - return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]); - } - $object_type = 'App\Profile'; - $exists = Report::whereUserId($user->id) - ->whereObjectId($object->id) - ->whereObjectType('App\Profile') - ->count(); - $rpid = $object->id; - break; - - default: - return $this->error("Invalid report type", 400, ["error_code" => "ERROR_REPORT_OBJECT_TYPE_INVALID"]); - break; - } - - if ($exists !== 0) { - return $this->error("Duplicate report", 400, ["error_code" => "ERROR_REPORT_DUPLICATE"]); - } - - if ($object->profile_id == $user->profile_id) { - return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]); - } - - $report = new Report; - $report->profile_id = $user->profile_id; - $report->user_id = $user->id; - $report->object_id = $object->id; - $report->object_type = $object_type; - $report->reported_profile_id = $rpid; - $report->type = $report_type; - $report->save(); - - if(config('instance.reports.email.enabled')) { - ReportNotifyAdminViaEmail::dispatch($report)->onQueue('default'); - } - - $res = [ - "msg" => "Successfully sent report", - "code" => 200 - ]; - return $this->json($res); - } - - /** - * DELETE /api/v1.1/accounts/avatar - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function deleteAvatar(Request $request) - { - $user = $request->user(); - - abort_if(!$user, 403); - abort_if($user->status != null, 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $avatar = $user->profile->avatar; - - if( $avatar->media_path == 'public/avatars/default.png' || - $avatar->media_path == 'public/avatars/default.jpg' - ) { - return AccountService::get($user->profile_id); - } - - if(is_file(storage_path('app/' . $avatar->media_path))) { - @unlink(storage_path('app/' . $avatar->media_path)); - } - - $avatar->media_path = 'public/avatars/default.jpg'; - $avatar->change_count = $avatar->change_count + 1; - $avatar->save(); - - Cache::forget('avatar:' . $user->profile_id); - Cache::forget("avatar:{$user->profile_id}"); - Cache::forget('user:account:id:'.$user->id); - AccountService::del($user->profile_id); - - return AccountService::get($user->profile_id); - } - - /** - * GET /api/v1.1/accounts/{id}/posts - * - * @return \App\Transformer\Api\StatusTransformer - */ - public function accountPosts(Request $request, $id) - { - $user = $request->user(); - - abort_if(!$user, 403); - abort_if($user->status != null, 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $account = AccountService::get($id); - - if(!$account || $account['username'] !== $request->input('username')) { - return $this->json([]); - } - - $posts = ProfileStatusService::get($id); - - if(!$posts) { - return $this->json([]); - } - - $res = collect($posts) - ->map(function($id) { - return StatusService::get($id); - }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->toArray(); - - return $this->json($res); - } - - /** - * POST /api/v1.1/accounts/change-password - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountChangePassword(Request $request) - { - $user = $request->user(); - abort_if(!$user, 403); - abort_if($user->status != null, 403); - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $this->validate($request, [ - 'current_password' => 'bail|required|current_password', - 'new_password' => 'required|min:' . config('pixelfed.min_password_length', 8), - 'confirm_password' => 'required|same:new_password' - ],[ - 'current_password' => 'The password you entered is incorrect' - ]); - - $user->password = bcrypt($request->input('new_password')); - $user->save(); - - $log = new AccountLog; - $log->user_id = $user->id; - $log->item_id = $user->id; - $log->item_type = 'App\User'; - $log->action = 'account.edit.password'; - $log->message = 'Password changed'; - $log->link = null; - $log->ip_address = $request->ip(); - $log->user_agent = $request->userAgent(); - $log->save(); - - Mail::to($request->user())->send(new PasswordChange($user)); - - return $this->json(AccountService::get($user->profile_id)); - } - - /** - * GET /api/v1.1/accounts/login-activity - * - * @return array - */ - public function accountLoginActivity(Request $request) - { - $user = $request->user(); - abort_if(!$user, 403); - abort_if($user->status != null, 403); - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - $agent = new Agent(); - $currentIp = $request->ip(); - - $activity = AccountLog::whereUserId($user->id) - ->whereAction('auth.login') - ->orderBy('created_at', 'desc') - ->groupBy('ip_address') - ->limit(10) - ->get() - ->map(function($item) use($agent, $currentIp) { - $agent->setUserAgent($item->user_agent); - return [ - 'id' => $item->id, - 'action' => $item->action, - 'ip' => $item->ip_address, - 'ip_current' => $item->ip_address === $currentIp, - 'is_mobile' => $agent->isMobile(), - 'device' => $agent->device(), - 'browser' => $agent->browser(), - 'platform' => $agent->platform(), - 'created_at' => $item->created_at->format('c') - ]; - }); - - return $this->json($activity); - } - - /** - * GET /api/v1.1/accounts/two-factor - * - * @return array - */ - public function accountTwoFactor(Request $request) - { - $user = $request->user(); - abort_if(!$user, 403); - abort_if($user->status != null, 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $res = [ - 'active' => (bool) $user->{'2fa_enabled'}, - 'setup_at' => $user->{'2fa_setup_at'} - ]; - return $this->json($res); - } - - /** - * GET /api/v1.1/accounts/emails-from-pixelfed - * - * @return array - */ - public function accountEmailsFromPixelfed(Request $request) - { - $user = $request->user(); - abort_if(!$user, 403); - abort_if($user->status != null, 403); - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - $from = config('mail.from.address'); - - $emailVerifications = EmailVerification::whereUserId($user->id) - ->orderByDesc('id') - ->where('created_at', '>', now()->subDays(14)) - ->limit(10) - ->get() - ->map(function($mail) use($user, $from) { - return [ - 'type' => 'Email Verification', - 'subject' => 'Confirm Email', - 'to_address' => $user->email, - 'from_address' => $from, - 'created_at' => str_replace('@', 'at', $mail->created_at->format('M j, Y @ g:i:s A')) - ]; - }) - ->toArray(); - - $passwordResets = DB::table('password_resets') - ->whereEmail($user->email) - ->where('created_at', '>', now()->subDays(14)) - ->orderByDesc('created_at') - ->limit(10) - ->get() - ->map(function($mail) use($user, $from) { - return [ - 'type' => 'Password Reset', - 'subject' => 'Reset Password Notification', - 'to_address' => $user->email, - 'from_address' => $from, - 'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A')) - ]; - }) - ->toArray(); - - $passwordChanges = AccountLog::whereUserId($user->id) - ->whereAction('account.edit.password') - ->where('created_at', '>', now()->subDays(14)) - ->orderByDesc('created_at') - ->limit(10) - ->get() - ->map(function($mail) use($user, $from) { - return [ - 'type' => 'Password Change', - 'subject' => 'Password Change', - 'to_address' => $user->email, - 'from_address' => $from, - 'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A')) - ]; - }) - ->toArray(); - - $res = collect([]) - ->merge($emailVerifications) - ->merge($passwordResets) - ->merge($passwordChanges) - ->sortByDesc('created_at') - ->values(); - - return $this->json($res); - } - - /** - * GET /api/v1.1/accounts/apps-and-applications - * - * @return array - */ - public function accountApps(Request $request) - { - $user = $request->user(); - abort_if(!$user, 403); - abort_if($user->status != null, 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $res = $user->tokens->sortByDesc('created_at')->take(10)->map(function($token, $key) use($request) { - return [ - 'id' => $token->id, - 'current_session' => $request->user()->token()->id == $token->id, - 'name' => $token->client->name, - 'scopes' => $token->scopes, - 'revoked' => $token->revoked, - 'created_at' => str_replace('@', 'at', now()->parse($token->created_at)->format('M j, Y @ g:i:s A')), - 'expires_at' => str_replace('@', 'at', now()->parse($token->expires_at)->format('M j, Y @ g:i:s A')) - ]; - }); - - return $this->json($res); - } - - public function inAppRegistrationPreFlightCheck(Request $request) - { - return [ - 'open' => (bool) config_cache('pixelfed.open_registration'), - 'iara' => config('pixelfed.allow_app_registration') - ]; - } - - public function inAppRegistration(Request $request) - { - abort_if($request->user(), 404); - abort_unless(config_cache('pixelfed.open_registration'), 404); - abort_unless(config('pixelfed.allow_app_registration'), 404); - abort_unless($request->hasHeader('X-PIXELFED-APP'), 403); - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $rl = RateLimiter::attempt('pf:apiv1.1:iar:'.$request->ip(), config('pixelfed.app_registration_rate_limit_attempts', 3), function(){}, config('pixelfed.app_registration_rate_limit_decay', 1800)); - abort_if(!$rl, 400, 'Too many requests'); - - $this->validate($request, [ - 'email' => [ - 'required', - 'string', - 'email', - 'max:255', - 'unique:users', - function ($attribute, $value, $fail) { - $banned = EmailService::isBanned($value); - if($banned) { - return $fail('Email is invalid.'); - } - }, - ], - 'username' => [ - 'required', - 'min:2', - 'max:15', - 'unique:users', - function ($attribute, $value, $fail) { - $dash = substr_count($value, '-'); - $underscore = substr_count($value, '_'); - $period = substr_count($value, '.'); - - if(ends_with($value, ['.php', '.js', '.css'])) { - return $fail('Username is invalid.'); - } - - if(($dash + $underscore + $period) > 1) { - return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); - } - - if (!ctype_alnum($value[0])) { - return $fail('Username is invalid. Must start with a letter or number.'); - } - - if (!ctype_alnum($value[strlen($value) - 1])) { - return $fail('Username is invalid. Must end with a letter or number.'); - } - - $val = str_replace(['_', '.', '-'], '', $value); - if(!ctype_alnum($val)) { - return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); - } - - $restricted = RestrictedNames::get(); - if (in_array(strtolower($value), array_map('strtolower', $restricted))) { - return $fail('Username cannot be used.'); - } - }, - ], - 'password' => 'required|string|min:8', - ]); - - $email = $request->input('email'); - $username = $request->input('username'); - $password = $request->input('password'); - - if(config('database.default') == 'pgsql') { - $username = strtolower($username); - $email = strtolower($email); - } - - $user = new User; - $user->name = $username; - $user->username = $username; - $user->email = $email; - $user->password = Hash::make($password); - $user->register_source = 'app'; - $user->app_register_ip = $request->ip(); - $user->app_register_token = Str::random(40); - $user->save(); - - $rtoken = Str::random(64); - - $verify = new EmailVerification(); - $verify->user_id = $user->id; - $verify->email = $user->email; - $verify->user_token = $user->app_register_token; - $verify->random_token = $rtoken; - $verify->save(); - - $params = http_build_query([ - 'ut' => $user->app_register_token, - 'rt' => $rtoken, - 'ea' => base64_encode($user->email) - ]); - $appUrl = url('/api/v1.1/auth/iarer?'. $params); - - Mail::to($user->email)->send(new ConfirmAppEmail($verify, $appUrl)); - - return response()->json([ - 'success' => true, - ]); - } - - public function inAppRegistrationEmailRedirect(Request $request) - { - $this->validate($request, [ - 'ut' => 'required', - 'rt' => 'required', - 'ea' => 'required' - ]); - $ut = $request->input('ut'); - $rt = $request->input('rt'); - $ea = $request->input('ea'); - $params = http_build_query([ - 'ut' => $ut, - 'rt' => $rt, - 'domain' => config('pixelfed.domain.app'), - 'ea' => $ea - ]); - $url = 'pixelfed://confirm-account/'. $ut . '?' . $params; - return redirect()->away($url); - } - - public function inAppRegistrationConfirm(Request $request) - { - abort_if($request->user(), 404); - abort_unless(config_cache('pixelfed.open_registration'), 404); - abort_unless(config('pixelfed.allow_app_registration'), 404); - abort_unless($request->hasHeader('X-PIXELFED-APP'), 403); - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $rl = RateLimiter::attempt('pf:apiv1.1:iarc:'.$request->ip(), config('pixelfed.app_registration_confirm_rate_limit_attempts', 20), function(){}, config('pixelfed.app_registration_confirm_rate_limit_decay', 1800)); - abort_if(!$rl, 429, 'Too many requests'); - - $this->validate($request, [ - 'user_token' => 'required', - 'random_token' => 'required', - 'email' => 'required' - ]); - - $verify = EmailVerification::whereEmail($request->input('email')) - ->whereUserToken($request->input('user_token')) - ->whereRandomToken($request->input('random_token')) - ->first(); - - if(!$verify) { - return response()->json(['error' => 'Invalid tokens'], 403); - } - - if($verify->created_at->lt(now()->subHours(24))) { - $verify->delete(); - return response()->json(['error' => 'Invalid tokens'], 403); - } - - $user = User::findOrFail($verify->user_id); - $user->email_verified_at = now(); - $user->last_active_at = now(); - $user->save(); - - $token = $user->createToken('Pixelfed'); - - return response()->json([ - 'access_token' => $token->accessToken - ]); - } - - public function archive(Request $request, $id) - { - abort_if(!$request->user(), 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $status = Status::whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->whereProfileId($request->user()->profile_id) - ->findOrFail($id); - - if($status->scope === 'archived') { - return [200]; - } - - $archive = new StatusArchived; - $archive->status_id = $status->id; - $archive->profile_id = $status->profile_id; - $archive->original_scope = $status->scope; - $archive->save(); - - $status->scope = 'archived'; - $status->visibility = 'draft'; - $status->save(); - StatusService::del($status->id, true); - AccountService::syncPostCount($status->profile_id); - - return [200]; - } - - public function unarchive(Request $request, $id) - { - abort_if(!$request->user(), 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $status = Status::whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->whereProfileId($request->user()->profile_id) - ->findOrFail($id); - - if($status->scope !== 'archived') { - return [200]; - } - - $archive = StatusArchived::whereStatusId($status->id) - ->whereProfileId($status->profile_id) - ->firstOrFail(); - - $status->scope = $archive->original_scope; - $status->visibility = $archive->original_scope; - $status->save(); - $archive->delete(); - StatusService::del($status->id, true); - AccountService::syncPostCount($status->profile_id); - - return [200]; - } - - public function archivedPosts(Request $request) - { - abort_if(!$request->user(), 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $statuses = Status::whereProfileId($request->user()->profile_id) - ->whereScope('archived') - ->orderByDesc('id') - ->cursorPaginate(10); - - return StatusStateless::collection($statuses); - } - - public function placesById(Request $request, $id, $slug) - { - abort_if(!$request->user(), 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $place = Place::whereSlug($slug)->findOrFail($id); - - $posts = Cache::remember('pf-api:v1.1:places-by-id:' . $place->id, 3600, function() use($place) { - return Status::wherePlaceId($place->id) - ->whereNull('uri') - ->whereScope('public') - ->orderByDesc('created_at') - ->limit(60) - ->pluck('id'); - }); - - $posts = $posts->map(function($id) { - return StatusService::get($id); - }) - ->filter() - ->values(); - - return [ - 'place' => - [ - 'id' => $place->id, - 'name' => $place->name, - 'slug' => $place->slug, - 'country' => $place->country, - 'lat' => $place->lat, - 'long' => $place->long - ], - 'posts' => $posts]; - } - - public function moderatePost(Request $request, $id) - { - abort_if(!$request->user(), 403); - abort_if($request->user()->is_admin != true, 403); - - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } - - $this->validate($request, [ - 'action' => 'required|in:cw,mark-public,mark-unlisted,mark-private,mark-spammer,delete' - ]); - - $action = $request->input('action'); - $status = Status::find($id); - - if(!$status) { - return response()->json(['error' => 'Cannot find status'], 400); - } - - if($status->uri == null) { - if($status->profile->user && $status->profile->user->is_admin) { - return response()->json(['error' => 'Cannot moderate admin accounts'], 400); - } - } - - if($action == 'mark-spammer') { - $status->profile->update([ - 'unlisted' => true, - 'cw' => true, - 'no_autolink' => true - ]); - - Status::whereProfileId($status->profile_id) - ->get() - ->each(function($s) { - if(in_array($s->scope, ['public', 'unlisted'])) { - $s->scope = 'private'; - $s->visibility = 'private'; - } - $s->is_nsfw = true; - $s->save(); - StatusService::del($s->id, true); - }); - - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $status->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $status->profile_id); - Cache::forget('admin-dash:reports:spam-count'); - } else if ($action == 'cw') { - $state = $status->is_nsfw; - $status->is_nsfw = !$state; - $status->save(); - StatusService::del($status->id); - } else if ($action == 'mark-public') { - $state = $status->scope; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); - StatusService::del($status->id, true); - if($state !== 'public') { - if($status->uri) { - if($status->in_reply_to_id == null && $status->reblog_of_id == null) { - NetworkTimelineService::add($status->id); - } - } else { - if($status->in_reply_to_id == null && $status->reblog_of_id == null) { - PublicTimelineService::add($status->id); - } - } - } - } else if ($action == 'mark-unlisted') { - $state = $status->scope; - $status->scope = 'unlisted'; - $status->visibility = 'unlisted'; - $status->save(); - StatusService::del($status->id); - if($state == 'public') { - PublicTimelineService::del($status->id); - NetworkTimelineService::del($status->id); - } - } else if ($action == 'mark-private') { - $state = $status->scope; - $status->scope = 'private'; - $status->visibility = 'private'; - $status->save(); - StatusService::del($status->id); - if($state == 'public') { - PublicTimelineService::del($status->id); - NetworkTimelineService::del($status->id); - } - } else if ($action == 'delete') { - PublicTimelineService::del($status->id); - NetworkTimelineService::del($status->id); - Cache::forget('_api:statuses:recent_9:' . $status->profile_id); - Cache::forget('profile:status_count:' . $status->profile_id); - Cache::forget('profile:embed:' . $status->profile_id); - StatusService::del($status->id, true); - Cache::forget('profile:status_count:'.$status->profile_id); - $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); - return []; - } - - Cache::forget('_api:statuses:recent_9:'.$status->profile_id); - - return StatusService::get($status->id, false); - } - - public function getWebSettings(Request $request) - { - abort_if(!$request->user(), 403); - $uid = $request->user()->id; - $settings = UserSetting::firstOrCreate([ - 'user_id' => $uid + protected $fractal; + + public function __construct() + { + $this->fractal = new Fractal\Manager; + $this->fractal->setSerializer(new ArraySerializer); + } + + public function json($res, $code = 200, $headers = []) + { + return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); + } + + public function error($msg, $code = 400, $extra = [], $headers = []) + { + $res = [ + 'msg' => $msg, + 'code' => $code, + ]; + + return response()->json(array_merge($res, $extra), $code, $headers, JSON_UNESCAPED_SLASHES); + } + + public function report(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $user = $request->user(); + abort_if($user->status != null, 403); + + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $report_type = $request->input('report_type'); + $object_id = $request->input('object_id'); + $object_type = $request->input('object_type'); + + $types = [ + 'spam', + 'sensitive', + 'abusive', + 'underage', + 'violence', + 'copyright', + 'impersonation', + 'scam', + 'terrorism', + ]; + + if (! $report_type || ! $object_id || ! $object_type) { + return $this->error('Invalid or missing parameters', 400, ['error_code' => 'ERROR_INVALID_PARAMS']); + } + + if (! in_array($report_type, $types)) { + return $this->error('Invalid report type', 400, ['error_code' => 'ERROR_TYPE_INVALID']); + } + + if ($object_type === 'user' && $object_id == $user->profile_id) { + return $this->error('Cannot self report', 400, ['error_code' => 'ERROR_NO_SELF_REPORTS']); + } + + $rpid = null; + + switch ($object_type) { + case 'post': + $object = Status::find($object_id); + if (! $object) { + return $this->error('Invalid object id', 400, ['error_code' => 'ERROR_INVALID_OBJECT_ID']); + } + $object_type = 'App\Status'; + $exists = Report::whereUserId($user->id) + ->whereObjectId($object->id) + ->whereObjectType('App\Status') + ->count(); + + $rpid = $object->profile_id; + break; + + case 'user': + $object = Profile::find($object_id); + if (! $object) { + return $this->error('Invalid object id', 400, ['error_code' => 'ERROR_INVALID_OBJECT_ID']); + } + $object_type = 'App\Profile'; + $exists = Report::whereUserId($user->id) + ->whereObjectId($object->id) + ->whereObjectType('App\Profile') + ->count(); + $rpid = $object->id; + break; + + default: + return $this->error('Invalid report type', 400, ['error_code' => 'ERROR_REPORT_OBJECT_TYPE_INVALID']); + break; + } + + if ($exists !== 0) { + return $this->error('Duplicate report', 400, ['error_code' => 'ERROR_REPORT_DUPLICATE']); + } + + if ($object->profile_id == $user->profile_id) { + return $this->error('Cannot self report', 400, ['error_code' => 'ERROR_NO_SELF_REPORTS']); + } + + $report = new Report; + $report->profile_id = $user->profile_id; + $report->user_id = $user->id; + $report->object_id = $object->id; + $report->object_type = $object_type; + $report->reported_profile_id = $rpid; + $report->type = $report_type; + $report->save(); + + if (config('instance.reports.email.enabled')) { + ReportNotifyAdminViaEmail::dispatch($report)->onQueue('default'); + } + + $res = [ + 'msg' => 'Successfully sent report', + 'code' => 200, + ]; + + return $this->json($res); + } + + /** + * DELETE /api/v1.1/accounts/avatar + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function deleteAvatar(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $user = $request->user(); + abort_if($user->status != null, 403); + + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $avatar = $user->profile->avatar; + + if ($avatar->media_path == 'public/avatars/default.png' || + $avatar->media_path == 'public/avatars/default.jpg' + ) { + return AccountService::get($user->profile_id); + } + + if (is_file(storage_path('app/'.$avatar->media_path))) { + @unlink(storage_path('app/'.$avatar->media_path)); + } + + $avatar->media_path = 'public/avatars/default.jpg'; + $avatar->change_count = $avatar->change_count + 1; + $avatar->save(); + + Cache::forget('avatar:'.$user->profile_id); + Cache::forget("avatar:{$user->profile_id}"); + Cache::forget('user:account:id:'.$user->id); + AccountService::del($user->profile_id); + + return AccountService::get($user->profile_id); + } + + /** + * GET /api/v1.1/accounts/{id}/posts + * + * @return \App\Transformer\Api\StatusTransformer + */ + public function accountPosts(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $user = $request->user(); + abort_if($user->status != null, 403); + + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $account = AccountService::get($id); + + if (! $account || $account['username'] !== $request->input('username')) { + return $this->json([]); + } + + $posts = ProfileStatusService::get($id); + + if (! $posts) { + return $this->json([]); + } + + $res = collect($posts) + ->map(function ($id) { + return StatusService::get($id); + }) + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->toArray(); + + return $this->json($res); + } + + /** + * POST /api/v1.1/accounts/change-password + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountChangePassword(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $user = $request->user(); + abort_if($user->status != null, 403); + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $this->validate($request, [ + 'current_password' => 'bail|required|current_password', + 'new_password' => 'required|min:'.config('pixelfed.min_password_length', 8), + 'confirm_password' => 'required|same:new_password', + ], [ + 'current_password' => 'The password you entered is incorrect', ]); - if(!$settings->other) { + + $user->password = bcrypt($request->input('new_password')); + $user->save(); + + $log = new AccountLog; + $log->user_id = $user->id; + $log->item_id = $user->id; + $log->item_type = 'App\User'; + $log->action = 'account.edit.password'; + $log->message = 'Password changed'; + $log->link = null; + $log->ip_address = $request->ip(); + $log->user_agent = $request->userAgent(); + $log->save(); + + Mail::to($request->user())->send(new PasswordChange($user)); + + return $this->json(AccountService::get($user->profile_id)); + } + + /** + * GET /api/v1.1/accounts/login-activity + * + * @return array + */ + public function accountLoginActivity(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $user = $request->user(); + abort_if($user->status != null, 403); + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + $agent = new Agent; + $currentIp = $request->ip(); + + $activity = AccountLog::whereUserId($user->id) + ->whereAction('auth.login') + ->orderBy('created_at', 'desc') + ->groupBy('ip_address') + ->limit(10) + ->get() + ->map(function ($item) use ($agent, $currentIp) { + $agent->setUserAgent($item->user_agent); + + return [ + 'id' => $item->id, + 'action' => $item->action, + 'ip' => $item->ip_address, + 'ip_current' => $item->ip_address === $currentIp, + 'is_mobile' => $agent->isMobile(), + 'device' => $agent->device(), + 'browser' => $agent->browser(), + 'platform' => $agent->platform(), + 'created_at' => $item->created_at->format('c'), + ]; + }); + + return $this->json($activity); + } + + /** + * GET /api/v1.1/accounts/two-factor + * + * @return array + */ + public function accountTwoFactor(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $user = $request->user(); + abort_if($user->status != null, 403); + + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $res = [ + 'active' => (bool) $user->{'2fa_enabled'}, + 'setup_at' => $user->{'2fa_setup_at'}, + ]; + + return $this->json($res); + } + + /** + * GET /api/v1.1/accounts/emails-from-pixelfed + * + * @return array + */ + public function accountEmailsFromPixelfed(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $user = $request->user(); + abort_if($user->status != null, 403); + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + $from = config('mail.from.address'); + + $emailVerifications = EmailVerification::whereUserId($user->id) + ->orderByDesc('id') + ->where('created_at', '>', now()->subDays(14)) + ->limit(10) + ->get() + ->map(function ($mail) use ($user, $from) { + return [ + 'type' => 'Email Verification', + 'subject' => 'Confirm Email', + 'to_address' => $user->email, + 'from_address' => $from, + 'created_at' => str_replace('@', 'at', $mail->created_at->format('M j, Y @ g:i:s A')), + ]; + }) + ->toArray(); + + $passwordResets = DB::table('password_resets') + ->whereEmail($user->email) + ->where('created_at', '>', now()->subDays(14)) + ->orderByDesc('created_at') + ->limit(10) + ->get() + ->map(function ($mail) use ($user, $from) { + return [ + 'type' => 'Password Reset', + 'subject' => 'Reset Password Notification', + 'to_address' => $user->email, + 'from_address' => $from, + 'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A')), + ]; + }) + ->toArray(); + + $passwordChanges = AccountLog::whereUserId($user->id) + ->whereAction('account.edit.password') + ->where('created_at', '>', now()->subDays(14)) + ->orderByDesc('created_at') + ->limit(10) + ->get() + ->map(function ($mail) use ($user, $from) { + return [ + 'type' => 'Password Change', + 'subject' => 'Password Change', + 'to_address' => $user->email, + 'from_address' => $from, + 'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A')), + ]; + }) + ->toArray(); + + $res = collect([]) + ->merge($emailVerifications) + ->merge($passwordResets) + ->merge($passwordChanges) + ->sortByDesc('created_at') + ->values(); + + return $this->json($res); + } + + /** + * GET /api/v1.1/accounts/apps-and-applications + * + * @return array + */ + public function accountApps(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $user = $request->user(); + abort_if($user->status != null, 403); + + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $res = $user->tokens->sortByDesc('created_at')->take(10)->map(function ($token, $key) use ($request) { + return [ + 'id' => $token->id, + 'current_session' => $request->user()->token()->id == $token->id, + 'name' => $token->client->name, + 'scopes' => $token->scopes, + 'revoked' => $token->revoked, + 'created_at' => str_replace('@', 'at', now()->parse($token->created_at)->format('M j, Y @ g:i:s A')), + 'expires_at' => str_replace('@', 'at', now()->parse($token->expires_at)->format('M j, Y @ g:i:s A')), + ]; + }); + + return $this->json($res); + } + + public function inAppRegistrationPreFlightCheck(Request $request) + { + return [ + 'open' => (bool) config_cache('pixelfed.open_registration'), + 'iara' => (bool) config_cache('pixelfed.allow_app_registration'), + ]; + } + + public function inAppRegistration(Request $request) + { + abort_if($request->user(), 404); + abort_unless((bool) config_cache('pixelfed.open_registration'), 404); + abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404); + abort_unless($request->hasHeader('X-PIXELFED-APP'), 403); + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $rl = RateLimiter::attempt('pf:apiv1.1:iar:'.$request->ip(), config('pixelfed.app_registration_rate_limit_attempts', 3), function () {}, config('pixelfed.app_registration_rate_limit_decay', 1800)); + abort_if(! $rl, 400, 'Too many requests'); + + $this->validate($request, [ + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + 'unique:users', + function ($attribute, $value, $fail) { + $banned = EmailService::isBanned($value); + if ($banned) { + return $fail('Email is invalid.'); + } + }, + ], + 'username' => [ + 'required', + 'min:2', + 'max:15', + 'unique:users', + function ($attribute, $value, $fail) { + $dash = substr_count($value, '-'); + $underscore = substr_count($value, '_'); + $period = substr_count($value, '.'); + + if (ends_with($value, ['.php', '.js', '.css'])) { + return $fail('Username is invalid.'); + } + + if (($dash + $underscore + $period) > 1) { + return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); + } + + if (! ctype_alnum($value[0])) { + return $fail('Username is invalid. Must start with a letter or number.'); + } + + if (! ctype_alnum($value[strlen($value) - 1])) { + return $fail('Username is invalid. Must end with a letter or number.'); + } + + $val = str_replace(['_', '.', '-'], '', $value); + if (! ctype_alnum($val)) { + return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); + } + + $restricted = RestrictedNames::get(); + if (in_array(strtolower($value), array_map('strtolower', $restricted))) { + return $fail('Username cannot be used.'); + } + }, + ], + 'password' => 'required|string|min:8', + ]); + + $email = $request->input('email'); + $username = $request->input('username'); + $password = $request->input('password'); + + if (config('database.default') == 'pgsql') { + $username = strtolower($username); + $email = strtolower($email); + } + + $user = new User; + $user->name = $username; + $user->username = $username; + $user->email = $email; + $user->password = Hash::make($password); + $user->register_source = 'app'; + $user->app_register_ip = $request->ip(); + $user->app_register_token = Str::random(40); + $user->save(); + + $rtoken = Str::random(64); + + $verify = new EmailVerification; + $verify->user_id = $user->id; + $verify->email = $user->email; + $verify->user_token = $user->app_register_token; + $verify->random_token = $rtoken; + $verify->save(); + + $params = http_build_query([ + 'ut' => $user->app_register_token, + 'rt' => $rtoken, + 'ea' => base64_encode($user->email), + ]); + $appUrl = url('/api/v1.1/auth/iarer?'.$params); + + Mail::to($user->email)->send(new ConfirmAppEmail($verify, $appUrl)); + + return response()->json([ + 'success' => true, + ]); + } + + public function inAppRegistrationEmailRedirect(Request $request) + { + $this->validate($request, [ + 'ut' => 'required', + 'rt' => 'required', + 'ea' => 'required', + ]); + $ut = $request->input('ut'); + $rt = $request->input('rt'); + $ea = $request->input('ea'); + $params = http_build_query([ + 'ut' => $ut, + 'rt' => $rt, + 'domain' => config('pixelfed.domain.app'), + 'ea' => $ea, + ]); + $url = 'pixelfed://confirm-account/'.$ut.'?'.$params; + + return redirect()->away($url); + } + + public function inAppRegistrationConfirm(Request $request) + { + abort_if($request->user(), 404); + abort_unless((bool) config_cache('pixelfed.open_registration'), 404); + abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404); + abort_unless($request->hasHeader('X-PIXELFED-APP'), 403); + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $rl = RateLimiter::attempt('pf:apiv1.1:iarc:'.$request->ip(), config('pixelfed.app_registration_confirm_rate_limit_attempts', 20), function () {}, config('pixelfed.app_registration_confirm_rate_limit_decay', 1800)); + abort_if(! $rl, 429, 'Too many requests'); + + $request->validate([ + 'user_token' => 'required', + 'random_token' => 'required', + 'email' => 'required', + ]); + + $verify = EmailVerification::whereEmail($request->input('email')) + ->whereUserToken($request->input('user_token')) + ->whereRandomToken($request->input('random_token')) + ->first(); + + if (! $verify) { + return response()->json(['error' => 'Invalid tokens'], 403); + } + + if ($verify->created_at->lt(now()->subHours(24))) { + $verify->delete(); + + return response()->json(['error' => 'Invalid tokens'], 403); + } + + $user = User::findOrFail($verify->user_id); + $user->email_verified_at = now(); + $user->last_active_at = now(); + $user->save(); + + $token = $user->createToken('Pixelfed', ['read', 'write', 'follow', 'admin:read', 'admin:write', 'push']); + + return response()->json([ + 'access_token' => $token->accessToken, + ]); + } + + public function archive(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $status = Status::whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->whereProfileId($request->user()->profile_id) + ->findOrFail($id); + + if ($status->scope === 'archived') { + return [200]; + } + + $archive = new StatusArchived; + $archive->status_id = $status->id; + $archive->profile_id = $status->profile_id; + $archive->original_scope = $status->scope; + $archive->save(); + + $status->scope = 'archived'; + $status->visibility = 'draft'; + $status->save(); + StatusService::del($status->id, true); + AccountService::syncPostCount($status->profile_id); + + return [200]; + } + + public function unarchive(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $status = Status::whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->whereProfileId($request->user()->profile_id) + ->findOrFail($id); + + if ($status->scope !== 'archived') { + return [200]; + } + + $archive = StatusArchived::whereStatusId($status->id) + ->whereProfileId($status->profile_id) + ->firstOrFail(); + + $status->scope = $archive->original_scope; + $status->visibility = $archive->original_scope; + $status->save(); + $archive->delete(); + StatusService::del($status->id, true); + AccountService::syncPostCount($status->profile_id); + + return [200]; + } + + public function archivedPosts(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $statuses = Status::whereProfileId($request->user()->profile_id) + ->whereScope('archived') + ->orderByDesc('id') + ->cursorPaginate(10); + + return StatusStateless::collection($statuses); + } + + public function placesById(Request $request, $id, $slug) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $place = Place::whereSlug($slug)->findOrFail($id); + + $posts = Cache::remember('pf-api:v1.1:places-by-id:'.$place->id, 3600, function () use ($place) { + return Status::wherePlaceId($place->id) + ->whereNull('uri') + ->whereScope('public') + ->orderByDesc('created_at') + ->limit(60) + ->pluck('id'); + }); + + $posts = $posts->map(function ($id) { + return StatusService::get($id); + }) + ->filter() + ->values(); + + return [ + 'place' => [ + 'id' => $place->id, + 'name' => $place->name, + 'slug' => $place->slug, + 'country' => $place->country, + 'lat' => $place->lat, + 'long' => $place->long, + ], + 'posts' => $posts]; + } + + public function moderatePost(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_if($request->user()->is_admin != true, 403); + abort_unless($request->user()->tokenCan('admin:write'), 403); + + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + + $this->validate($request, [ + 'action' => 'required|in:cw,mark-public,mark-unlisted,mark-private,mark-spammer,delete', + ]); + + $action = $request->input('action'); + $status = Status::find($id); + + if (! $status) { + return response()->json(['error' => 'Cannot find status'], 400); + } + + if ($status->uri == null) { + if ($status->profile->user && $status->profile->user->is_admin) { + return response()->json(['error' => 'Cannot moderate admin accounts'], 400); + } + } + + if ($action == 'mark-spammer') { + $status->profile->update([ + 'unlisted' => true, + 'cw' => true, + 'no_autolink' => true, + ]); + + Status::whereProfileId($status->profile_id) + ->get() + ->each(function ($s) { + if (in_array($s->scope, ['public', 'unlisted'])) { + $s->scope = 'private'; + $s->visibility = 'private'; + } + $s->is_nsfw = true; + $s->save(); + StatusService::del($s->id, true); + }); + + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$status->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$status->profile_id); + Cache::forget('admin-dash:reports:spam-count'); + } elseif ($action == 'cw') { + $state = $status->is_nsfw; + $status->is_nsfw = ! $state; + $status->save(); + StatusService::del($status->id); + } elseif ($action == 'mark-public') { + $state = $status->scope; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + StatusService::del($status->id, true); + if ($state !== 'public') { + if ($status->uri) { + if ($status->in_reply_to_id == null && $status->reblog_of_id == null) { + NetworkTimelineService::add($status->id); + } + } else { + if ($status->in_reply_to_id == null && $status->reblog_of_id == null) { + PublicTimelineService::add($status->id); + } + } + } + } elseif ($action == 'mark-unlisted') { + $state = $status->scope; + $status->scope = 'unlisted'; + $status->visibility = 'unlisted'; + $status->save(); + StatusService::del($status->id); + if ($state == 'public') { + PublicTimelineService::del($status->id); + NetworkTimelineService::del($status->id); + } + } elseif ($action == 'mark-private') { + $state = $status->scope; + $status->scope = 'private'; + $status->visibility = 'private'; + $status->save(); + StatusService::del($status->id); + if ($state == 'public') { + PublicTimelineService::del($status->id); + NetworkTimelineService::del($status->id); + } + } elseif ($action == 'delete') { + PublicTimelineService::del($status->id); + NetworkTimelineService::del($status->id); + Cache::forget('_api:statuses:recent_9:'.$status->profile_id); + Cache::forget('profile:status_count:'.$status->profile_id); + Cache::forget('profile:embed:'.$status->profile_id); + StatusService::del($status->id, true); + Cache::forget('profile:status_count:'.$status->profile_id); + $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); + return []; } - return $settings->other; - } + + Cache::forget('_api:statuses:recent_9:'.$status->profile_id); + + return StatusService::get($status->id, false); + } + + public function getWebSettings(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $uid = $request->user()->id; + $settings = UserSetting::firstOrCreate([ + 'user_id' => $uid, + ]); + if (! $settings->other) { + return []; + } + + return $settings->other; + } public function setWebSettings(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + $this->validate($request, [ 'field' => 'required|in:enable_reblogs,hide_reblog_banner', - 'value' => 'required' + 'value' => 'required', ]); $field = $request->input('field'); $value = $request->input('value'); $settings = UserSetting::firstOrCreate([ - 'user_id' => $request->user()->id + 'user_id' => $request->user()->id, ]); - if(!$settings->other) { + if (! $settings->other) { $other = []; } else { $other = $settings->other; @@ -897,4 +935,439 @@ class ApiV1Dot1Controller extends Controller return [200]; } + + public function getMutualAccounts(Request $request, $id) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); + + $account = AccountService::get($id, true); + if (! $account || ! isset($account['id'])) { + return []; + } + $res = collect(FollowerService::mutualAccounts($request->user()->profile_id, $id)) + ->map(function ($accountId) { + return AccountService::get($accountId, true); + }) + ->filter() + ->take(24) + ->values(); + + return $this->json($res); + } + + public function accountUsernameToId(Request $request, $username) + { + abort_if(! $request->user() || ! $request->user()->token() || ! $username, 403); + abort_unless($request->user()->tokenCan('read'), 403); + $username = trim($username); + $rateLimiting = (bool) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.enabled'); + $ipRateLimiting = (bool) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.ip_enabled'); + if ($ipRateLimiting) { + $userLimit = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.ip_limit'); + $userDecay = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.ip_decay'); + $userKey = 'pf:apiv1.1:acctU2ID:byIp:'.$request->ip(); + + if (RateLimiter::tooManyAttempts($userKey, $userLimit)) { + $limits = [ + 'X-Rate-Limit-Limit' => $userLimit, + 'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit), + 'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey), + ]; + + return $this->json(['error' => 'Too many attempts!'], 429, $limits); + } + + RateLimiter::increment($userKey, $userDecay); + $limits = [ + 'X-Rate-Limit-Limit' => $userLimit, + 'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit), + 'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey), + ]; + } + if ($rateLimiting) { + $userLimit = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.limit'); + $userDecay = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.decay'); + $userKey = 'pf:apiv1.1:acctU2ID:byUid:'.$request->user()->id; + + if (RateLimiter::tooManyAttempts($userKey, $userLimit)) { + $limits = [ + 'X-Rate-Limit-Limit' => $userLimit, + 'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit), + 'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey), + ]; + + return $this->json(['error' => 'Too many attempts!'], 429, $limits); + } + + RateLimiter::increment($userKey, $userDecay); + $limits = [ + 'X-Rate-Limit-Limit' => $userLimit, + 'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit), + 'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey), + ]; + } + if (str_ends_with($username, config_cache('pixelfed.domain.app'))) { + $pre = str_starts_with($username, '@') ? substr($username, 1) : $username; + $parts = explode('@', $pre); + $username = $parts[0]; + } + $accountId = AccountService::usernameToId($username, true); + if (! $accountId) { + return []; + } + $account = AccountService::get($accountId); + + return $this->json($account, 200, $rateLimiting ? $limits : []); + } + + public function getPushState(Request $request) + { + abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found'); + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('push'), 403); + abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.'); + $user = $request->user(); + abort_if($user->status, 422, 'Cannot access this resource at this time'); + $res = [ + 'version' => PushNotificationService::PUSH_GATEWAY_VERSION, + 'username' => (string) $user->username, + 'profile_id' => (string) $user->profile_id, + 'notify_enabled' => (bool) $user->notify_enabled, + 'has_token' => (bool) $user->expo_token, + 'notify_like' => (bool) $user->notify_like, + 'notify_follow' => (bool) $user->notify_follow, + 'notify_mention' => (bool) $user->notify_mention, + 'notify_comment' => (bool) $user->notify_comment, + ]; + + return $this->json($res); + } + + public function disablePush(Request $request) + { + abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found'); + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('push'), 403); + abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.'); + abort_if($request->user()->status, 422, 'Cannot access this resource at this time'); + + $request->user()->update([ + 'notify_enabled' => false, + 'expo_token' => null, + 'notify_like' => false, + 'notify_follow' => false, + 'notify_mention' => false, + 'notify_comment' => false, + ]); + + PushNotificationService::removeMemberFromAll($request->user()->profile_id); + + $user = $request->user(); + + return $this->json([ + 'version' => PushNotificationService::PUSH_GATEWAY_VERSION, + 'username' => (string) $user->username, + 'profile_id' => (string) $user->profile_id, + 'notify_enabled' => (bool) $user->notify_enabled, + 'has_token' => (bool) $user->expo_token, + 'notify_like' => (bool) $user->notify_like, + 'notify_follow' => (bool) $user->notify_follow, + 'notify_mention' => (bool) $user->notify_mention, + 'notify_comment' => (bool) $user->notify_comment, + ]); + } + + public function comparePush(Request $request) + { + abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found'); + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('push'), 403); + abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.'); + abort_if($request->user()->status, 422, 'Cannot access this resource at this time'); + + $this->validate($request, [ + 'expo_token' => ['required', 'string', new ExpoPushTokenRule], + ]); + + $user = $request->user(); + + if (empty($user->expo_token)) { + return $this->json([ + 'version' => PushNotificationService::PUSH_GATEWAY_VERSION, + 'username' => (string) $user->username, + 'profile_id' => (string) $user->profile_id, + 'notify_enabled' => (bool) $user->notify_enabled, + 'match' => false, + 'has_existing' => false, + ]); + } + + $token = $request->input('expo_token'); + $knownToken = $user->expo_token; + $match = hash_equals($knownToken, $token); + + return $this->json([ + 'version' => PushNotificationService::PUSH_GATEWAY_VERSION, + 'username' => (string) $user->username, + 'profile_id' => (string) $user->profile_id, + 'notify_enabled' => (bool) $user->notify_enabled, + 'match' => $match, + 'has_existing' => true, + ]); + } + + public function updatePush(Request $request) + { + abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found'); + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('push'), 403); + abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.'); + abort_if($request->user()->status, 422, 'Cannot access this resource at this time'); + + $this->validate($request, [ + 'notify_enabled' => 'required', + 'token' => ['required', 'string', new ExpoPushTokenRule], + 'notify_like' => 'sometimes', + 'notify_follow' => 'sometimes', + 'notify_mention' => 'sometimes', + 'notify_comment' => 'sometimes', + ]); + + $pid = $request->user()->profile_id; + abort_if(! $pid, 422, 'An error occured'); + $expoToken = $request->input('token'); + + $existing = User::where('profile_id', '!=', $pid)->whereExpoToken($expoToken)->count(); + abort_if($existing && $existing > 5, 400, 'Push token is already used by another account'); + + $request->user()->update([ + 'notify_enabled' => $request->boolean('notify_enabled'), + 'expo_token' => $expoToken, + ]); + + if ($request->filled('notify_like')) { + $request->user()->update(['notify_like' => (bool) $request->boolean('notify_like')]); + $request->boolean('notify_like') == true ? + PushNotificationService::set('like', $pid) : + PushNotificationService::removeMember('like', $pid); + } + if ($request->filled('notify_follow')) { + $request->user()->update(['notify_follow' => (bool) $request->boolean('notify_follow')]); + $request->boolean('notify_follow') == true ? + PushNotificationService::set('follow', $pid) : + PushNotificationService::removeMember('follow', $pid); + } + if ($request->filled('notify_mention')) { + $request->user()->update(['notify_mention' => (bool) $request->boolean('notify_mention')]); + $request->boolean('notify_mention') == true ? + PushNotificationService::set('mention', $pid) : + PushNotificationService::removeMember('mention', $pid); + } + if ($request->filled('notify_comment')) { + $request->user()->update(['notify_comment' => (bool) $request->boolean('notify_comment')]); + $request->boolean('notify_comment') == true ? + PushNotificationService::set('comment', $pid) : + PushNotificationService::removeMember('comment', $pid); + } + + if ($request->boolean('notify_enabled') == false) { + PushNotificationService::removeMemberFromAll($request->user()->profile_id); + } + + $user = $request->user(); + + $res = [ + 'version' => PushNotificationService::PUSH_GATEWAY_VERSION, + 'notify_enabled' => (bool) $user->notify_enabled, + 'has_token' => (bool) $user->expo_token, + 'notify_like' => (bool) $user->notify_like, + 'notify_follow' => (bool) $user->notify_follow, + 'notify_mention' => (bool) $user->notify_mention, + 'notify_comment' => (bool) $user->notify_comment, + ]; + + return $this->json($res); + } + + /** + * POST /api/v1.1/status/create + * + * + * @return StatusTransformer + */ + public function statusCreate(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + + $this->validate($request, [ + 'status' => 'nullable|string|max:'.(int) config_cache('pixelfed.max_caption_length'), + 'file' => [ + 'required', + 'file', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + function ($attribute, $value, $fail) { + if (is_array($value) && count($value) > 1) { + $fail('Only one file can be uploaded at a time.'); + } + }, + ], + 'sensitive' => 'nullable', + 'visibility' => 'string|in:private,unlisted,public', + 'spoiler_text' => 'sometimes|max:140', + ]); + + if ($request->hasHeader('idempotency-key')) { + $key = 'pf:api:v1:status:idempotency-key:'.$request->user()->id.':'.hash('sha1', $request->header('idempotency-key')); + $exists = Cache::has($key); + abort_if($exists, 400, 'Duplicate idempotency key.'); + Cache::put($key, 1, 3600); + } + + if (config('costar.enabled') == true) { + $blockedKeywords = config('costar.keyword.block'); + if ($blockedKeywords !== null && $request->status) { + $keywords = config('costar.keyword.block'); + foreach ($keywords as $kw) { + if (Str::contains($request->status, $kw) == true) { + abort(400, 'Invalid object. Contains banned keyword.'); + } + } + } + } + $user = $request->user(); + + if ($user->has_roles) { + abort_if(! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + } + + $profile = $user->profile; + + $limitKey = 'compose:rate-limit:media-upload:'.$user->id; + $photo = $request->file('file'); + $fileSize = $photo->getSize(); + $sizeInKbs = (int) ceil($fileSize / 1000); + $accountSize = UserStorageService::get($user->id); + abort_if($accountSize === -1, 403, 'Invalid request.'); + $updatedAccountSize = (int) $accountSize + (int) $sizeInKbs; + + if ((bool) config_cache('pixelfed.enforce_account_limit') == true) { + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($updatedAccountSize >= $limit) { + abort(403, 'Account size limit reached.'); + } + } + + $mimes = explode(',', config_cache('pixelfed.media_types')); + if (in_array($photo->getMimeType(), $mimes) == false) { + abort(403, 'Invalid or unsupported mime type.'); + } + + $storagePath = MediaPathService::get($user, 2); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + $license = null; + $mime = $photo->getMimeType(); + + $settings = UserSetting::whereUserId($user->id)->first(); + + if ($settings && ! empty($settings->compose_settings)) { + $compose = $settings->compose_settings; + + if (isset($compose['default_license']) && $compose['default_license'] != 1) { + $license = $compose['default_license']; + } + } + + abort_if(MediaBlocklistService::exists($hash) == true, 451); + + $visibility = $profile->is_private ? 'private' : ( + $profile->unlisted == true && + $request->input('visibility', 'public') == 'public' ? + 'unlisted' : + $request->input('visibility', 'public')); + + if ($user->last_active_at == null) { + return []; + } + $defaultCaption = config_cache('database.default') === 'mysql' ? null : ""; + $content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : $defaultCaption; + $cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false); + $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null; + + $status = new Status; + $status->caption = $content; + $status->rendered = $defaultCaption; + $status->profile_id = $user->profile_id; + $status->is_nsfw = $cw; + $status->cw_summary = $spoilerText; + $status->scope = $visibility; + $status->visibility = $visibility; + $status->type = StatusController::mimeTypeCheck([$mime]); + $status->save(); + + if (! $status) { + abort(500, 'An error occured.'); + } + + $media = new Media; + $media->status_id = $status->id; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->mime = $mime; + $media->order = 1; + $media->caption = $request->input('description'); + if ($license) { + $media->license = $license; + } + $media->save(); + + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + ImageOptimize::dispatch($media)->onQueue('mmo'); + break; + + case 'video/mp4': + VideoThumbnail::dispatch($media)->onQueue('mmo'); + $preview_url = '/storage/no-preview.png'; + $url = '/storage/no-preview.png'; + break; + } + + $user->storage_used = (int) $updatedAccountSize; + $user->storage_used_updated_at = now(); + $user->save(); + + NewStatusPipeline::dispatch($status); + + Cache::forget('user:account:id:'.$user->id); + Cache::forget('_api:statuses:recent_9:'.$user->profile_id); + Cache::forget('profile:status_count:'.$user->profile_id); + Cache::forget($user->storageUsedKey()); + Cache::forget('profile:embed:'.$status->profile_id); + Cache::forget($limitKey); + + $res = StatusService::getMastodon($status->id, false); + $res['favourited'] = false; + $res['language'] = 'en'; + $res['bookmarked'] = false; + $res['card'] = null; + + return $this->json($res); + } + + public function nagState(Request $request) + { + abort_unless((bool) config_cache('pixelfed.oauth_enabled'), 404); + + return [ + 'active' => NotificationAppGatewayService::enabled(), + ]; + } } diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index 2ca5b96c5..ee193e8af 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -2,319 +2,337 @@ namespace App\Http\Controllers\Api; -use Illuminate\Http\Request; use App\Http\Controllers\Controller; +use App\Jobs\ImageOptimizePipeline\ImageOptimize; +use App\Jobs\MediaPipeline\MediaDeletePipeline; +use App\Jobs\VideoPipeline\VideoThumbnail; use App\Media; -use App\UserSetting; -use App\User; -use Illuminate\Support\Facades\Cache; use App\Services\AccountService; -use App\Services\BouncerService; use App\Services\InstanceService; use App\Services\MediaBlocklistService; use App\Services\MediaPathService; use App\Services\SearchApiV2Service; +use App\Services\UserRoleService; +use App\Services\UserStorageService; +use App\Transformer\Api\Mastodon\v1\MediaTransformer; +use App\User; +use App\UserSetting; use App\Util\Media\Filter; -use App\Jobs\MediaPipeline\MediaDeletePipeline; -use App\Jobs\VideoPipeline\{ - VideoOptimize, - VideoPostProcess, - VideoThumbnail -}; -use App\Jobs\ImageOptimizePipeline\ImageOptimize; +use App\Util\Site\Nodeinfo; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Storage; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; -use App\Transformer\Api\Mastodon\v1\{ - AccountTransformer, - MediaTransformer, - NotificationTransformer, - StatusTransformer, -}; -use App\Transformer\Api\{ - RelationshipTransformer, -}; -use App\Util\Site\Nodeinfo; class ApiV2Controller extends Controller { - const PF_API_ENTITY_KEY = "_pe"; + const PF_API_ENTITY_KEY = '_pe'; - public function json($res, $code = 200, $headers = []) - { - return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); - } + public function json($res, $code = 200, $headers = []) + { + return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); + } public function instance(Request $request) { - $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () { - if(config_cache('instance.admin.pid')) { - return AccountService::getMastodon(config_cache('instance.admin.pid'), true); - } - $admin = User::whereIsAdmin(true)->first(); - return $admin && isset($admin->profile_id) ? - AccountService::getMastodon($admin->profile_id, true) : - null; - }); + $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () { + if (config_cache('instance.admin.pid')) { + return AccountService::getMastodon(config_cache('instance.admin.pid'), true); + } + $admin = User::whereIsAdmin(true)->first(); - $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { - return config_cache('app.rules') ? - collect(json_decode(config_cache('app.rules'), true)) - ->map(function($rule, $key) { - $id = $key + 1; - return [ - 'id' => "{$id}", - 'text' => $rule - ]; - }) - ->toArray() : []; - }); + return $admin && isset($admin->profile_id) ? + AccountService::getMastodon($admin->profile_id, true) : + null; + }); - $res = [ - 'domain' => config('pixelfed.domain.app'), - 'title' => config_cache('app.name'), - 'version' => config('pixelfed.version'), - 'source_url' => 'https://github.com/pixelfed/pixelfed', - 'description' => config_cache('app.short_description'), - 'usage' => [ - 'users' => [ - 'active_month' => (int) Nodeinfo::activeUsersMonthly() - ] - ], - 'thumbnail' => [ - 'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), - 'blurhash' => InstanceService::headerBlurhash(), - 'versions' => [ - '@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), - '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')) - ] - ], - 'languages' => [config('app.locale')], - 'configuration' => [ - 'urls' => [ - 'streaming' => 'wss://' . config('pixelfed.domain.app'), - 'status' => null - ], - 'accounts' => [ - 'max_featured_tags' => 0, - ], - 'statuses' => [ - 'max_characters' => (int) config('pixelfed.max_caption_length'), - 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), - 'characters_reserved_per_url' => 23 - ], - 'media_attachments' => [ - 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')), - 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, - 'image_matrix_limit' => 3686400, - 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, - 'video_frame_rate_limit' => 240, - 'video_matrix_limit' => 3686400 - ], - 'polls' => [ - 'max_options' => 4, - 'max_characters_per_option' => 50, - 'min_expiration' => 300, - 'max_expiration' => 2629746, - ], - 'translation' => [ - 'enabled' => false, - ], - ], - 'registrations' => [ - 'enabled' => (bool) config_cache('pixelfed.open_registration'), - 'approval_required' => false, - 'message' => null - ], - 'contact' => [ - 'email' => config('instance.email'), - 'account' => $contact - ], - 'rules' => $rules - ]; + $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { + return config_cache('app.rules') ? + collect(json_decode(config_cache('app.rules'), true)) + ->map(function ($rule, $key) { + $id = $key + 1; - return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); + return [ + 'id' => "{$id}", + 'text' => $rule, + ]; + }) + ->toArray() : []; + }); + + $res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use ($contact, $rules) { + return [ + 'domain' => config('pixelfed.domain.app'), + 'title' => config_cache('app.name'), + 'version' => '3.5.3 (compatible; Pixelfed '.config('pixelfed.version').')', + 'source_url' => 'https://github.com/pixelfed/pixelfed', + 'description' => config_cache('app.short_description'), + 'usage' => [ + 'users' => [ + 'active_month' => (int) Nodeinfo::activeUsersMonthly(), + ], + ], + 'thumbnail' => [ + 'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), + 'blurhash' => InstanceService::headerBlurhash(), + 'versions' => [ + '@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), + '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), + ], + ], + 'languages' => [config('app.locale')], + 'configuration' => [ + 'urls' => [ + 'streaming' => null, + 'status' => null, + ], + 'vapid' => [ + 'public_key' => config('webpush.vapid.public_key'), + ], + 'accounts' => [ + 'max_featured_tags' => 0, + ], + 'statuses' => [ + 'max_characters' => (int) config_cache('pixelfed.max_caption_length'), + 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), + 'characters_reserved_per_url' => 23, + ], + 'media_attachments' => [ + 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')), + 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, + 'image_matrix_limit' => 3686400, + 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, + 'video_frame_rate_limit' => 240, + 'video_matrix_limit' => 3686400, + ], + 'polls' => [ + 'max_options' => 0, + 'max_characters_per_option' => 0, + 'min_expiration' => 0, + 'max_expiration' => 0, + ], + 'translation' => [ + 'enabled' => false, + ], + ], + 'registrations' => [ + 'enabled' => null, + 'approval_required' => false, + 'message' => null, + 'url' => null, + ], + 'contact' => [ + 'email' => config('instance.email'), + 'account' => $contact, + ], + 'rules' => $rules, + ]; + }); + + $res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration'); + $res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled'); + + return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); } - /** - * GET /api/v2/search - * - * - * @return array - */ - public function search(Request $request) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v2/search + * + * + * @return array + */ + public function search(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - $this->validate($request, [ - 'q' => 'required|string|min:1|max:100', - 'account_id' => 'nullable|string', - 'max_id' => 'nullable|string', - 'min_id' => 'nullable|string', - 'type' => 'nullable|in:accounts,hashtags,statuses', - 'exclude_unreviewed' => 'nullable', - 'resolve' => 'nullable', - 'limit' => 'nullable|integer|max:40', - 'offset' => 'nullable|integer', - 'following' => 'nullable' - ]); + $this->validate($request, [ + 'q' => 'required|string|min:1|max:100', + 'account_id' => 'nullable|string', + 'max_id' => 'nullable|string', + 'min_id' => 'nullable|string', + 'type' => 'nullable|in:accounts,hashtags,statuses', + 'exclude_unreviewed' => 'nullable', + 'resolve' => 'nullable', + 'limit' => 'nullable|integer|max:40', + 'offset' => 'nullable|integer', + 'following' => 'nullable', + ]); - $mastodonMode = !$request->has('_pe'); - return $this->json(SearchApiV2Service::query($request, $mastodonMode)); - } + if ($request->user()->has_roles && ! UserRoleService::can('can-view-discover', $request->user()->id)) { + return [ + 'accounts' => [], + 'hashtags' => [], + 'statuses' => [], + ]; + } - /** - * GET /api/v2/streaming/config - * - * - * @return object - */ - public function getWebsocketConfig() - { - return config('broadcasting.default') === 'pusher' ? [ - 'host' => config('broadcasting.connections.pusher.options.host'), - 'port' => config('broadcasting.connections.pusher.options.port'), - 'key' => config('broadcasting.connections.pusher.key'), - 'cluster' => config('broadcasting.connections.pusher.options.cluster') - ] : []; - } + $mastodonMode = ! $request->has('_pe'); - /** - * POST /api/v2/media - * - * - * @return MediaTransformer - */ - public function mediaUploadV2(Request $request) - { - abort_if(!$request->user(), 403); + return $this->json(SearchApiV2Service::query($request, $mastodonMode)); + } - $this->validate($request, [ - 'file.*' => [ - 'required_without:file', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'file' => [ - 'required_without:file.*', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'filter_name' => 'nullable|string|max:24', - 'filter_class' => 'nullable|alpha_dash|max:24', - 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'), - 'replace_id' => 'sometimes' - ]); + /** + * GET /api/v2/streaming/config + * + * + * @return object + */ + public function getWebsocketConfig() + { + return config('broadcasting.default') === 'pusher' ? [ + 'host' => config('broadcasting.connections.pusher.options.host'), + 'port' => config('broadcasting.connections.pusher.options.port'), + 'key' => config('broadcasting.connections.pusher.key'), + 'cluster' => config('broadcasting.connections.pusher.options.cluster'), + ] : []; + } - $user = $request->user(); + /** + * POST /api/v2/media + * + * + * @return MediaTransformer + */ + public function mediaUploadV2(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); - if($user->last_active_at == null) { - return []; - } + $this->validate($request, [ + 'file.*' => [ + 'required_without:file', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + ], + 'file' => [ + 'required_without:file.*', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + ], + 'filter_name' => 'nullable|string|max:24', + 'filter_class' => 'nullable|alpha_dash|max:24', + 'description' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'), + 'replace_id' => 'sometimes', + ]); - if(empty($request->file('file'))) { - return response('', 422); - } + $user = $request->user(); - $limitKey = 'compose:rate-limit:media-upload:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); + if ($user->last_active_at == null) { + return []; + } - return $dailyLimit >= 1250; - }); - abort_if($limitReached == true, 429); + if (empty($request->file('file'))) { + return response('', 422); + } - $profile = $user->profile; + $limitKey = 'compose:rate-limit:media-upload:'.$user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { + $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { - return Media::whereUserId($user->id)->sum('size') / 1000; - }); - $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { - abort(403, 'Account size limit reached.'); - } - } + return $dailyLimit >= 1250; + }); + abort_if($limitReached == true, 429); - $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; - $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; + $profile = $user->profile; - $photo = $request->file('file'); + $accountSize = UserStorageService::get($user->id); + abort_if($accountSize === -1, 403, 'Invalid request.'); + $photo = $request->file('file'); + $fileSize = $photo->getSize(); + $sizeInKbs = (int) ceil($fileSize / 1000); + $updatedAccountSize = (int) $accountSize + (int) $sizeInKbs; - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), $mimes) == false) { - abort(403, 'Invalid or unsupported mime type.'); - } + if ((bool) config_cache('pixelfed.enforce_account_limit') == true) { + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($updatedAccountSize >= $limit) { + abort(403, 'Account size limit reached.'); + } + } - $storagePath = MediaPathService::get($user, 2); - $path = $photo->storePublicly($storagePath); - $hash = \hash_file('sha256', $photo); - $license = null; - $mime = $photo->getMimeType(); + $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; + $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; - $settings = UserSetting::whereUserId($user->id)->first(); + $mimes = explode(',', config_cache('pixelfed.media_types')); + if (in_array($photo->getMimeType(), $mimes) == false) { + abort(403, 'Invalid or unsupported mime type.'); + } - if($settings && !empty($settings->compose_settings)) { - $compose = $settings->compose_settings; + $storagePath = MediaPathService::get($user, 2); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + $license = null; + $mime = $photo->getMimeType(); - if(isset($compose['default_license']) && $compose['default_license'] != 1) { - $license = $compose['default_license']; - } - } + $settings = UserSetting::whereUserId($user->id)->first(); - abort_if(MediaBlocklistService::exists($hash) == true, 451); + if ($settings && ! empty($settings->compose_settings)) { + $compose = $settings->compose_settings; - if($request->has('replace_id')) { - $rpid = $request->input('replace_id'); - $removeMedia = Media::whereNull('status_id') - ->whereUserId($user->id) - ->whereProfileId($profile->id) - ->where('created_at', '>', now()->subHours(2)) - ->find($rpid); - if($removeMedia) { - MediaDeletePipeline::dispatch($removeMedia) - ->onQueue('mmo') - ->delay(now()->addMinutes(15)); - } - } + if (isset($compose['default_license']) && $compose['default_license'] != 1) { + $license = $compose['default_license']; + } + } - $media = new Media(); - $media->status_id = null; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $photo->getSize(); - $media->mime = $mime; - $media->caption = $request->input('description'); - $media->filter_class = $filterClass; - $media->filter_name = $filterName; - if($license) { - $media->license = $license; - } - $media->save(); + abort_if(MediaBlocklistService::exists($hash) == true, 451); - switch ($media->mime) { - case 'image/jpeg': - case 'image/png': - ImageOptimize::dispatch($media)->onQueue('mmo'); - break; + if ($request->has('replace_id')) { + $rpid = $request->input('replace_id'); + $removeMedia = Media::whereNull('status_id') + ->whereUserId($user->id) + ->whereProfileId($profile->id) + ->where('created_at', '>', now()->subHours(2)) + ->find($rpid); + if ($removeMedia) { + MediaDeletePipeline::dispatch($removeMedia) + ->onQueue('mmo') + ->delay(now()->addMinutes(15)); + } + } - case 'video/mp4': - VideoThumbnail::dispatch($media)->onQueue('mmo'); - $preview_url = '/storage/no-preview.png'; - $url = '/storage/no-preview.png'; - break; - } + $media = new Media(); + $media->status_id = null; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->mime = $mime; + $media->caption = $request->input('description'); + $media->filter_class = $filterClass; + $media->filter_name = $filterName; + if ($license) { + $media->license = $license; + } + $media->save(); - Cache::forget($limitKey); - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - $res = $fractal->createData($resource)->toArray(); - $res['preview_url'] = $media->url(). '?v=' . time(); - $res['url'] = null; - return $this->json($res, 202); - } + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + ImageOptimize::dispatch($media)->onQueue('mmo'); + break; + + case 'video/mp4': + VideoThumbnail::dispatch($media)->onQueue('mmo'); + $preview_url = '/storage/no-preview.png'; + $url = '/storage/no-preview.png'; + break; + } + + $user->storage_used = (int) $updatedAccountSize; + $user->storage_used_updated_at = now(); + $user->save(); + + Cache::forget($limitKey); + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + $res = $fractal->createData($resource)->toArray(); + $res['preview_url'] = $media->url().'?v='.time(); + $res['url'] = null; + + return $this->json($res, 202); + } } diff --git a/app/Http/Controllers/Api/BaseApiController.php b/app/Http/Controllers/Api/BaseApiController.php index 2e1fb5678..7ac73b4d0 100644 --- a/app/Http/Controllers/Api/BaseApiController.php +++ b/app/Http/Controllers/Api/BaseApiController.php @@ -99,6 +99,7 @@ class BaseApiController extends Controller public function avatarUpdate(Request $request) { abort_if(!$request->user(), 403); + $this->validate($request, [ 'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'), ]); @@ -134,9 +135,10 @@ class BaseApiController extends Controller public function verifyCredentials(Request $request) { + abort_if(!$request->user(), 403); + $user = $request->user(); - abort_if(!$user, 403); - if($user->status != null) { + if ($user->status != null) { Auth::logout(); abort(403); } @@ -147,6 +149,7 @@ class BaseApiController extends Controller public function accountLikes(Request $request) { abort_if(!$request->user(), 403); + $this->validate($request, [ 'page' => 'sometimes|int|min:1|max:20', 'limit' => 'sometimes|int|min:1|max:10' diff --git a/app/Http/Controllers/Api/InstanceApiController.php b/app/Http/Controllers/Api/InstanceApiController.php index 6edd27de3..37b597a31 100644 --- a/app/Http/Controllers/Api/InstanceApiController.php +++ b/app/Http/Controllers/Api/InstanceApiController.php @@ -4,8 +4,9 @@ namespace App\Http\Controllers\Api; use Illuminate\Http\Request; use App\Http\Controllers\Controller; -use App\{Profile, Status, User}; +use App\{Profile, Instance, Status, User}; use Cache; +use App\Services\StatusService; class InstanceApiController extends Controller { @@ -40,11 +41,8 @@ class InstanceApiController extends Controller { 'urls' => [], 'stats' => [ 'user_count' => User::count(), - 'status_count' => Status::whereNull('uri')->count(), - 'domain_count' => Profile::whereNotNull('domain') - ->groupBy('domain') - ->pluck('domain') - ->count() + 'status_count' => StatusService::totalLocalStatuses(), + 'domain_count' => Instance::count() ], 'thumbnail' => '', 'languages' => [], diff --git a/app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php b/app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php new file mode 100644 index 000000000..95e399720 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php @@ -0,0 +1,147 @@ +middleware(['auth:api', 'api.admin', 'scope:admin:read,admin:read:domain_blocks'])->only(['index', 'show']); + $this->middleware(['auth:api', 'api.admin', 'scope:admin:write,admin:write:domain_blocks'])->only(['create', 'update', 'delete']); + } + + public function index(Request $request) { + $this->validate($request, [ + 'limit' => 'sometimes|integer|max:100|min:1', + ]); + + $limit = $request->input('limit', 100); + + $res = Instance::moderated() + ->orderBy('id') + ->cursorPaginate($limit) + ->withQueryString(); + + return $this->json(DomainBlockResource::collection($res), [ + 'Link' => $this->linksForCollection($res) + ]); + } + + public function show(Request $request, $id) { + $domain_block = Instance::moderated()->find($id); + + if (!$domain_block) { + return $this->json([ 'error' => 'Record not found'], [], 404); + } + + return $this->json(new DomainBlockResource($domain_block)); + } + + public function create(Request $request) { + $this->validate($request, [ + 'domain' => 'required|string|min:1|max:120', + 'severity' => [ + 'sometimes', + Rule::in(['noop', 'silence', 'suspend']) + ], + 'reject_media' => 'sometimes|required|boolean', + 'reject_reports' => 'sometimes|required|boolean', + 'private_comment' => 'sometimes|string|min:1|max:1000', + 'public_comment' => 'sometimes|string|min:1|max:1000', + 'obfuscate' => 'sometimes|required|boolean' + ]); + + $domain = $request->input('domain'); + $severity = $request->input('severity', 'silence'); + $private_comment = $request->input('private_comment'); + + abort_if(!strpos($domain, '.'), 400, 'Invalid domain'); + abort_if(!filter_var($domain, FILTER_VALIDATE_DOMAIN), 400, 'Invalid domain'); + + // This is because Pixelfed can't currently support wildcard domain blocks + // We have to find something that could plausibly be an instance + $parts = explode('.', $domain); + if ($parts[0] == '*') { + // If we only have two parts, e.g., "*", "example", then we want to fail: + abort_if(count($parts) <= 2, 400, 'Invalid domain: This API does not support wildcard domain blocks yet'); + + // Otherwise we convert the *.foo.example to foo.example + $domain = implode('.', array_slice($parts, 1)); + } + + // Double check we definitely haven't let anything through: + abort_if(str_contains($domain, '*'), 400, 'Invalid domain'); + + $existing_domain_block = Instance::moderated()->whereDomain($domain)->first(); + + if ($existing_domain_block) { + return $this->json([ + 'error' => 'A domain block already exists for this domain', + 'existing_domain_block' => new DomainBlockResource($existing_domain_block) + ], [], 422); + } + + $domain_block = Instance::updateOrCreate( + [ 'domain' => $domain ], + [ 'banned' => $severity === 'suspend', 'unlisted' => $severity === 'silence', 'notes' => [$private_comment]] + ); + + InstanceService::refresh(); + + return $this->json(new DomainBlockResource($domain_block)); + } + + public function update(Request $request, $id) { + $this->validate($request, [ + 'severity' => [ + 'sometimes', + Rule::in(['noop', 'silence', 'suspend']) + ], + 'reject_media' => 'sometimes|required|boolean', + 'reject_reports' => 'sometimes|required|boolean', + 'private_comment' => 'sometimes|string|min:1|max:1000', + 'public_comment' => 'sometimes|string|min:1|max:1000', + 'obfuscate' => 'sometimes|required|boolean' + ]); + + $severity = $request->input('severity', 'silence'); + $private_comment = $request->input('private_comment'); + + $domain_block = Instance::moderated()->find($id); + + if (!$domain_block) { + return $this->json([ 'error' => 'Record not found'], [], 404); + } + + $domain_block->banned = $severity === 'suspend'; + $domain_block->unlisted = $severity === 'silence'; + $domain_block->notes = [$private_comment]; + $domain_block->save(); + + InstanceService::refresh(); + + return $this->json(new DomainBlockResource($domain_block)); + } + + public function delete(Request $request, $id) { + $domain_block = Instance::moderated()->find($id); + + if (!$domain_block) { + return $this->json([ 'error' => 'Record not found'], [], 404); + } + + $domain_block->banned = false; + $domain_block->unlisted = false; + $domain_block->save(); + + InstanceService::refresh(); + + return $this->json(null, [], 200); + } +} diff --git a/app/Http/Controllers/Api/V1/DomainBlockController.php b/app/Http/Controllers/Api/V1/DomainBlockController.php new file mode 100644 index 000000000..3b6730789 --- /dev/null +++ b/app/Http/Controllers/Api/V1/DomainBlockController.php @@ -0,0 +1,119 @@ +json($res, $code, $headers, JSON_UNESCAPED_SLASHES); + } + + public function index(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1|max:200' + ]); + $limit = $request->input('limit', 100); + $id = $request->user()->profile_id; + $filters = UserDomainBlock::whereProfileId($id)->orderByDesc('id')->cursorPaginate($limit); + $links = null; + $headers = []; + + if($filters->nextCursor()) { + $links .= '<'.$filters->nextPageUrl().'&limit='.$limit.'>; rel="next"'; + } + + if($filters->previousCursor()) { + if($links != null) { + $links .= ', '; + } + $links .= '<'.$filters->previousPageUrl().'&limit='.$limit.'>; rel="prev"'; + } + + if($links) { + $headers = ['Link' => $links]; + } + return $this->json($filters->pluck('domain'), 200, $headers); + } + + public function store(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'domain' => 'required|active_url|min:1|max:120' + ]); + + $pid = $request->user()->profile_id; + + $domain = trim($request->input('domain')); + + if(Helpers::validateUrl($domain) == false) { + return abort(500, 'Invalid domain or already blocked by server admins'); + } + + $domain = strtolower(parse_url($domain, PHP_URL_HOST)); + + abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server'); + + $existingCount = UserDomainBlock::whereProfileId($pid)->count(); + $maxLimit = (int) config_cache('instance.user_filters.max_domain_blocks'); + $errorMsg = __('profile.block.domain.max', ['max' => $maxLimit]); + + abort_if($existingCount >= $maxLimit, 400, $errorMsg); + + $block = UserDomainBlock::updateOrCreate([ + 'profile_id' => $pid, + 'domain' => $domain + ]); + + if($block->wasRecentlyCreated) { + Bus::batch([ + [ + new FeedRemoveDomainPipeline($pid, $domain), + new ProfilePurgeNotificationsByDomain($pid, $domain), + new ProfilePurgeFollowersByDomain($pid, $domain) + ] + ])->allowFailures()->onQueue('feed')->dispatch(); + + Cache::forget('profile:following:' . $pid); + UserFilterService::domainBlocks($pid, true); + } + + return $this->json([]); + } + + public function delete(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'domain' => 'required|min:1|max:120' + ]); + + $pid = $request->user()->profile_id; + + $domain = strtolower(trim($request->input('domain'))); + + $filters = UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->delete(); + + UserFilterService::domainBlocks($pid, true); + + return $this->json([]); + } +} diff --git a/app/Http/Controllers/Api/V1/TagsController.php b/app/Http/Controllers/Api/V1/TagsController.php new file mode 100644 index 000000000..2f7acf4a0 --- /dev/null +++ b/app/Http/Controllers/Api/V1/TagsController.php @@ -0,0 +1,209 @@ +json($res, $code, $headers, JSON_UNESCAPED_SLASHES); + } + + /** + * GET /api/v1/tags/:id/related + * + * + * @return array + */ + public function relatedTags(Request $request, $tag) + { + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $tag = Hashtag::whereSlug($tag)->firstOrFail(); + return HashtagRelatedService::get($tag->id); + } + + /** + * POST /api/v1/tags/:id/follow + * + * + * @return object + */ + public function followHashtag(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); + + $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; + $tag = Hashtag::where('name', $operator, $id) + ->orWhere('slug', $operator, $id) + ->first(); + + abort_if(!$tag, 422, 'Unknown hashtag'); + + abort_if( + HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT, + 422, + 'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.' + ); + + $follows = HashtagFollow::updateOrCreate( + [ + 'profile_id' => $account['id'], + 'hashtag_id' => $tag->id + ], + [ + 'user_id' => $request->user()->id + ] + ); + + HashtagService::follow($pid, $tag->id); + HashtagFollowService::add($tag->id, $pid); + + return response()->json(FollowedTagResource::make($follows)->toArray($request)); + } + + /** + * POST /api/v1/tags/:id/unfollow + * + * + * @return object + */ + public function unfollowHashtag(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); + + $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; + $tag = Hashtag::where('name', $operator, $id) + ->orWhere('slug', $operator, $id) + ->first(); + + abort_if(!$tag, 422, 'Unknown hashtag'); + + $follows = HashtagFollow::whereProfileId($pid) + ->whereHashtagId($tag->id) + ->first(); + + if(!$follows) { + return [ + 'name' => $tag->name, + 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, + 'history' => [], + 'following' => false + ]; + } + + if($follows) { + HashtagService::unfollow($pid, $tag->id); + HashtagFollowService::unfollow($tag->id, $pid); + HashtagUnfollowPipeline::dispatch($tag->id, $pid, $tag->slug)->onQueue('feed'); + $follows->delete(); + } + + $res = FollowedTagResource::make($follows)->toArray($request); + $res['following'] = false; + return response()->json($res); + } + + /** + * GET /api/v1/tags/:id + * + * + * @return object + */ + public function getHashtag(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); + $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; + $tag = Hashtag::where('name', $operator, $id) + ->orWhere('slug', $operator, $id) + ->first(); + + if(!$tag) { + return [ + 'name' => $id, + 'url' => config('app.url') . '/i/web/hashtag/' . $id, + 'history' => [], + 'following' => false + ]; + } + + $res = [ + 'name' => $tag->name, + 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, + 'history' => [], + 'following' => HashtagService::isFollowing($pid, $tag->id) + ]; + + if($request->has(self::PF_API_ENTITY_KEY)) { + $res['count'] = HashtagService::count($tag->id); + } + + return $this->json($res); + } + + /** + * GET /api/v1/followed_tags + * + * + * @return array + */ + public function getFollowedTags(Request $request) + { + abort_if(!$request->user(), 403); + + $account = AccountService::get($request->user()->profile_id); + + $this->validate($request, [ + 'cursor' => 'sometimes', + 'limit' => 'sometimes|integer|min:1|max:200' + ]); + $limit = $request->input('limit', 100); + + $res = HashtagFollow::whereProfileId($account['id']) + ->orderByDesc('id') + ->cursorPaginate($limit) + ->withQueryString(); + + $pagination = false; + $prevPage = $res->nextPageUrl(); + $nextPage = $res->previousPageUrl(); + if($nextPage && $prevPage) { + $pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"'; + } else if($nextPage && !$prevPage) { + $pagination = '<' . $nextPage . '>; rel="next"'; + } else if(!$nextPage && $prevPage) { + $pagination = '<' . $prevPage . '>; rel="prev"'; + } + + if($pagination) { + return response()->json(FollowedTagResource::collection($res)->collection) + ->header('Link', $pagination); + } + return response()->json(FollowedTagResource::collection($res)->collection); + } +} diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 618c495e2..22562e985 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -62,7 +62,7 @@ class ForgotPasswordController extends Controller usleep(random_int(100000, 3000000)); - if(config('captcha.enabled')) { + if((bool) config_cache('captcha.enabled')) { $rules = [ 'email' => 'required|email', 'h-captcha-response' => 'required|captcha' diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 3861d3272..86ee52c84 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -71,20 +71,21 @@ class LoginController extends Controller $this->username() => 'required|email', 'password' => 'required|string|min:6', ]; + $messages = []; if( - config('captcha.enabled') || - config('captcha.active.login') || + (bool) config_cache('captcha.enabled') && + (bool) config_cache('captcha.active.login') || ( - config('captcha.triggers.login.enabled') && + (bool) config_cache('captcha.triggers.login.enabled') && request()->session()->has('login_attempts') && request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts') ) ) { $rules['h-captcha-response'] = 'required|filled|captcha|min:5'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; } - - $this->validate($request, $rules); + $request->validate($rules, $messages); } /** diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 5eb1159fe..230daea85 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -3,230 +3,239 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; +use App\Services\BouncerService; +use App\Services\EmailService; use App\User; -use Purify; use App\Util\Lexer\RestrictedNames; +use Illuminate\Auth\Events\Registered; use Illuminate\Foundation\Auth\RegistersUsers; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; -use Illuminate\Auth\Events\Registered; -use Illuminate\Http\Request; -use App\Services\EmailService; -use App\Services\BouncerService; +use Purify; class RegisterController extends Controller { - /* - |-------------------------------------------------------------------------- - | Register Controller - |-------------------------------------------------------------------------- - | - | This controller handles the registration of new users as well as their - | validation and creation. By default this controller uses a trait to - | provide this functionality without requiring any additional code. - | - */ + /* + |-------------------------------------------------------------------------- + | Register Controller + |-------------------------------------------------------------------------- + | + | This controller handles the registration of new users as well as their + | validation and creation. By default this controller uses a trait to + | provide this functionality without requiring any additional code. + | + */ - use RegistersUsers; + use RegistersUsers; - /** - * Where to redirect users after registration. - * - * @var string - */ - protected $redirectTo = '/i/web'; + /** + * Where to redirect users after registration. + * + * @var string + */ + protected $redirectTo = '/i/web'; - /** - * Create a new controller instance. - * - * @return void - */ - public function __construct() - { - $this->middleware('guest'); - } + /** + * Create a new controller instance. + * + * @return void + */ + public function __construct() + { + $this->middleware('guest'); + } - public function getRegisterToken() - { - return \Cache::remember('pf:register:rt', 900, function() { - return str_random(40); - }); - } + public function getRegisterToken() + { + return \Cache::remember('pf:register:rt', 900, function () { + return str_random(40); + }); + } - /** - * Get a validator for an incoming registration request. - * - * @param array $data - * - * @return \Illuminate\Contracts\Validation\Validator - */ - protected function validator(array $data) - { - if(config('database.default') == 'pgsql') { - $data['username'] = strtolower($data['username']); - $data['email'] = strtolower($data['email']); - } + /** + * Get a validator for an incoming registration request. + * + * + * @return \Illuminate\Contracts\Validation\Validator + */ + public function validator(array $data) + { + if (config('database.default') == 'pgsql') { + $data['username'] = strtolower($data['username']); + $data['email'] = strtolower($data['email']); + } - $usernameRules = [ - 'required', - 'min:2', - 'max:15', - 'unique:users', - function ($attribute, $value, $fail) { - $dash = substr_count($value, '-'); - $underscore = substr_count($value, '_'); - $period = substr_count($value, '.'); + $usernameRules = [ + 'required', + 'min:2', + 'max:15', + 'unique:users', + function ($attribute, $value, $fail) { + $dash = substr_count($value, '-'); + $underscore = substr_count($value, '_'); + $period = substr_count($value, '.'); - if(ends_with($value, ['.php', '.js', '.css'])) { - return $fail('Username is invalid.'); - } + if (ends_with($value, ['.php', '.js', '.css'])) { + return $fail('Username is invalid.'); + } - if(($dash + $underscore + $period) > 1) { - return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); - } + if (($dash + $underscore + $period) > 1) { + return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); + } - if (!ctype_alnum($value[0])) { - return $fail('Username is invalid. Must start with a letter or number.'); - } + if (! ctype_alnum($value[0])) { + return $fail('Username is invalid. Must start with a letter or number.'); + } - if (!ctype_alnum($value[strlen($value) - 1])) { - return $fail('Username is invalid. Must end with a letter or number.'); - } + if (! ctype_alnum($value[strlen($value) - 1])) { + return $fail('Username is invalid. Must end with a letter or number.'); + } - $val = str_replace(['_', '.', '-'], '', $value); - if(!ctype_alnum($val)) { - return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); - } + $val = str_replace(['_', '.', '-'], '', $value); + if (! ctype_alnum($val)) { + return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); + } - $restricted = RestrictedNames::get(); - if (in_array(strtolower($value), array_map('strtolower', $restricted))) { - return $fail('Username cannot be used.'); - } - }, - ]; + if (! preg_match('/[a-zA-Z]/', $value)) { + return $fail('Username is invalid. Must contain at least one alphabetical character.'); + } - $emailRules = [ - 'required', - 'string', - 'email', - 'max:255', - 'unique:users', - function ($attribute, $value, $fail) { - $banned = EmailService::isBanned($value); - if($banned) { - return $fail('Email is invalid.'); - } - }, - ]; + $restricted = RestrictedNames::get(); + if (in_array(strtolower($value), array_map('strtolower', $restricted))) { + return $fail('Username cannot be used.'); + } + }, + ]; - $rt = [ - 'required', - function ($attribute, $value, $fail) { - if($value !== $this->getRegisterToken()) { - return $fail('Something went wrong'); - } - } - ]; + $emailRules = [ + 'required', + 'string', + 'email', + 'max:255', + 'unique:users', + function ($attribute, $value, $fail) { + $banned = EmailService::isBanned($value); + if ($banned) { + return $fail('Email is invalid.'); + } + }, + ]; - $rules = [ - 'agecheck' => 'required|accepted', - 'rt' => $rt, - 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'), - 'username' => $usernameRules, - 'email' => $emailRules, - 'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed', - ]; + $rt = [ + 'required', + function ($attribute, $value, $fail) { + if ($value !== $this->getRegisterToken()) { + return $fail('Something went wrong'); + } + }, + ]; - if(config('captcha.enabled') || config('captcha.active.register')) { - $rules['h-captcha-response'] = 'required|captcha'; - } + $rules = [ + 'agecheck' => 'required|accepted', + 'rt' => $rt, + 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'), + 'username' => $usernameRules, + 'email' => $emailRules, + 'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed', + ]; - return Validator::make($data, $rules); - } + if ((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register')) { + $rules['h-captcha-response'] = 'required|captcha'; + } - /** - * Create a new user instance after a valid registration. - * - * @param array $data - * - * @return \App\User - */ - protected function create(array $data) - { - if(config('database.default') == 'pgsql') { - $data['username'] = strtolower($data['username']); - $data['email'] = strtolower($data['email']); - } + return Validator::make($data, $rules); + } - return User::create([ - 'name' => Purify::clean($data['name']), - 'username' => $data['username'], - 'email' => $data['email'], - 'password' => Hash::make($data['password']), - 'app_register_ip' => request()->ip() - ]); - } + /** + * Create a new user instance after a valid registration. + * + * + * @return \App\User + */ + public function create(array $data) + { + if (config('database.default') == 'pgsql') { + $data['username'] = strtolower($data['username']); + $data['email'] = strtolower($data['email']); + } - /** - * Show the application registration form. - * - * @return \Illuminate\Http\Response - */ - public function showRegistrationForm() - { - if(config_cache('pixelfed.open_registration')) { - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp(request()->ip()), 404); - } - $hasLimit = config('pixelfed.enforce_max_users'); - if($hasLimit) { - $limit = config('pixelfed.max_users'); - $count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count(); - if($limit <= $count) { - return redirect(route('help.instance-max-users-limit')); - } - abort_if($limit <= $count, 404); - return view('auth.register'); - } else { - return view('auth.register'); - } - } else { - abort(404); - } - } + return User::create([ + 'name' => Purify::clean($data['name']), + 'username' => $data['username'], + 'email' => $data['email'], + 'password' => Hash::make($data['password']), + 'app_register_ip' => request()->ip(), + ]); + } - /** - * Handle a registration request for the application. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - */ - public function register(Request $request) - { - abort_if(config_cache('pixelfed.open_registration') == false, 400); + /** + * Show the application registration form. + * + * @return \Illuminate\Http\Response + */ + public function showRegistrationForm() + { + if ((bool) config_cache('pixelfed.open_registration')) { + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp(request()->ip()), 404); + } + $hasLimit = config('pixelfed.enforce_max_users'); + if ($hasLimit) { + $limit = config('pixelfed.max_users'); + $count = User::where(function ($q) { + return $q->whereNull('status')->orWhereNotIn('status', ['deleted', 'delete']); + })->count(); + if ($limit <= $count) { + return redirect(route('help.instance-max-users-limit')); + } + abort_if($limit <= $count, 404); - if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } + return view('auth.register'); + } else { + return view('auth.register'); + } + } else { + if ((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) { + return redirect('/auth/sign_up'); + } else { + abort(404); + } + } + } - $hasLimit = config('pixelfed.enforce_max_users'); - if($hasLimit) { - $count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count(); - $limit = config('pixelfed.max_users'); + /** + * Handle a registration request for the application. + * + * @return \Illuminate\Http\Response + */ + public function register(Request $request) + { + abort_if(config_cache('pixelfed.open_registration') == false, 400); - if($limit && $limit <= $count) { - return redirect(route('help.instance-max-users-limit')); - } - } + if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } + $hasLimit = config('pixelfed.enforce_max_users'); + if ($hasLimit) { + $count = User::where(function ($q) { + return $q->whereNull('status')->orWhereNotIn('status', ['deleted', 'delete']); + })->count(); + $limit = config('pixelfed.max_users'); - $this->validator($request->all())->validate(); + if ($limit && $limit <= $count) { + return redirect(route('help.instance-max-users-limit')); + } + } - event(new Registered($user = $this->create($request->all()))); + $this->validator($request->all())->validate(); - $this->guard()->login($user); + event(new Registered($user = $this->create($request->all()))); - return $this->registered($request, $user) - ?: redirect($this->redirectPath()); - } + $this->guard()->login($user); + + return $this->registered($request, $user) + ?: redirect($this->redirectPath()); + } } diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index a92c4e38d..166ec01e3 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -50,7 +50,7 @@ class ResetPasswordController extends Controller { usleep(random_int(100000, 3000000)); - if(config('captcha.enabled')) { + if((bool) config_cache('captcha.enabled')) { return [ 'token' => 'required', 'email' => 'required|email', diff --git a/app/Http/Controllers/AuthorizeInteractionController.php b/app/Http/Controllers/AuthorizeInteractionController.php new file mode 100644 index 000000000..701ee06f1 --- /dev/null +++ b/app/Http/Controllers/AuthorizeInteractionController.php @@ -0,0 +1,37 @@ +validate([ + 'uri' => 'required|url', + ]); + + abort_unless((bool) config_cache('federation.activitypub.enabled'), 404); + + $uri = Helpers::validateUrl($request->input('uri'), true); + abort_unless($uri, 404); + + if (! $request->user()) { + return redirect('/login?next='.urlencode($uri)); + } + + $status = Helpers::statusFetch($uri); + if ($status && isset($status['id'])) { + return redirect('/i/web/post/'.$status['id']); + } + + $profile = Helpers::profileFetch($uri); + if ($profile && isset($profile['id'])) { + return redirect('/i/web/profile/'.$profile['id']); + } + + return redirect('/i/web'); + } +} diff --git a/app/Http/Controllers/BookmarkController.php b/app/Http/Controllers/BookmarkController.php index a24520d64..14b1d9e3f 100644 --- a/app/Http/Controllers/BookmarkController.php +++ b/app/Http/Controllers/BookmarkController.php @@ -3,65 +3,62 @@ namespace App\Http\Controllers; use App\Bookmark; -use App\Status; -use Auth; -use Illuminate\Http\Request; +use App\Services\AccountService; use App\Services\BookmarkService; use App\Services\FollowerService; +use App\Services\UserRoleService; +use App\Status; +use Illuminate\Http\Request; class BookmarkController extends Controller { - public function __construct() - { - $this->middleware('auth'); - } + public function __construct() + { + $this->middleware('auth'); + } - public function store(Request $request) - { - $this->validate($request, [ - 'item' => 'required|integer|min:1', - ]); + public function store(Request $request) + { + $this->validate($request, [ + 'item' => 'required|integer|min:1', + ]); - $profile = Auth::user()->profile; - $status = Status::findOrFail($request->input('item')); + $user = $request->user(); + $status = Status::findOrFail($request->input('item')); + $account = AccountService::get($status->profile_id); + abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot bookmark or unbookmark a post from an account that has migrated'); + abort_if($user->has_roles && ! UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action'); + abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); + abort_if(! in_array($status->scope, ['public', 'unlisted', 'private']), 404); + abort_if(! in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']), 404); - abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); - abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); - abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); + if ($status->scope == 'private') { + if ($user->profile_id !== $status->profile_id && ! FollowerService::follows($user->profile_id, $status->profile_id)) { + if ($exists = Bookmark::whereStatusId($status->id)->whereProfileId($user->profile_id)->first()) { + BookmarkService::del($user->profile_id, $status->id); + $exists->delete(); - if($status->scope == 'private') { - if($profile->id !== $status->profile_id && !FollowerService::follows($profile->id, $status->profile_id)) { - if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($profile->id)->first()) { - BookmarkService::del($profile->id, $status->id); - $exists->delete(); + if ($request->ajax()) { + return ['code' => 200, 'msg' => 'Bookmark removed!']; + } else { + return redirect()->back(); + } + } + abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.'); + } + } - if ($request->ajax()) { - return ['code' => 200, 'msg' => 'Bookmark removed!']; - } else { - return redirect()->back(); - } - } - abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.'); - } - } + $bookmark = Bookmark::firstOrCreate( + ['status_id' => $status->id], ['profile_id' => $user->profile_id] + ); - $bookmark = Bookmark::firstOrCreate( - ['status_id' => $status->id], ['profile_id' => $profile->id] - ); + if (! $bookmark->wasRecentlyCreated) { + BookmarkService::del($user->profile_id, $status->id); + $bookmark->delete(); + } else { + BookmarkService::add($user->profile_id, $status->id); + } - if (!$bookmark->wasRecentlyCreated) { - BookmarkService::del($profile->id, $status->id); - $bookmark->delete(); - } else { - BookmarkService::add($profile->id, $status->id); - } - - if ($request->ajax()) { - $response = ['code' => 200, 'msg' => 'Bookmark saved!']; - } else { - $response = redirect()->back(); - } - - return $response; - } + return $request->expectsJson() ? ['code' => 200, 'msg' => 'Bookmark saved!'] : redirect()->back(); + } } diff --git a/app/Http/Controllers/CollectionController.php b/app/Http/Controllers/CollectionController.php index 6cd4bda57..447490433 100644 --- a/app/Http/Controllers/CollectionController.php +++ b/app/Http/Controllers/CollectionController.php @@ -2,72 +2,65 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use Auth; -use App\{ - Collection, - CollectionItem, - Profile, - Status -}; -use League\Fractal; -use App\Transformer\Api\{ - AccountTransformer, - StatusTransformer, -}; -use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use App\Collection; +use App\CollectionItem; use App\Services\AccountService; use App\Services\CollectionService; use App\Services\FollowerService; use App\Services\StatusService; +use App\Status; +use Auth; +use Illuminate\Http\Request; class CollectionController extends Controller { public function create(Request $request) { - abort_if(!Auth::check(), 403); + abort_if(! Auth::check(), 403); $profile = Auth::user()->profile; $collection = Collection::firstOrCreate([ 'profile_id' => $profile->id, - 'published_at' => null + 'published_at' => null, ]); $collection->visibility = 'draft'; $collection->save(); + return view('collection.create', compact('collection')); } public function show(Request $request, int $id) { $user = $request->user(); - $collection = CollectionService::getCollection($id); - abort_if(!$collection, 404); - if($collection['published_at'] == null || $collection['visibility'] != 'public') { - abort_if(!$user, 404); - if($user->profile_id != $collection['pid']) { - if(!$user->is_admin) { - abort_if($collection['visibility'] != 'private', 404); - abort_if(!FollowerService::follows($user->profile_id, $collection['pid']), 404); - } - } - } - return view('collection.show', compact('collection')); + $collection = CollectionService::getCollection($id); + abort_if(! $collection, 404); + if ($collection['published_at'] == null || $collection['visibility'] != 'public') { + abort_if(! $user, 404); + if ($user->profile_id != $collection['pid']) { + if (! $user->is_admin) { + abort_if($collection['visibility'] != 'private', 404); + abort_if(! FollowerService::follows($user->profile_id, $collection['pid']), 404); + } + } + } + + return view('collection.show', compact('collection')); } public function index(Request $request) { - abort_if(!Auth::check(), 403); - return $request->all(); + abort_if(! Auth::check(), 403); + + return $request->all(); } public function store(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'title' => 'nullable|max:50', - 'description' => 'nullable|max:500', - 'visibility' => 'nullable|string|in:public,private,draft' + 'title' => 'nullable|max:50', + 'description' => 'nullable|max:500', + 'visibility' => 'nullable|string|in:public,private,draft', ]); $pid = $request->user()->profile_id; @@ -78,20 +71,21 @@ class CollectionController extends Controller $collection->save(); CollectionService::deleteCollection($id); + return CollectionService::setCollection($collection->id, $collection); } public function publish(Request $request, int $id) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'title' => 'nullable|max:50', - 'description' => 'nullable|max:500', - 'visibility' => 'required|alpha|in:public,private,draft' + 'title' => 'nullable|max:50', + 'description' => 'nullable|max:500', + 'visibility' => 'required|alpha|in:public,private,draft', ]); - $profile = Auth::user()->profile; + $profile = Auth::user()->profile; $collection = Collection::whereProfileId($profile->id)->findOrFail($id); - if($collection->items()->count() == 0) { + if ($collection->items()->count() == 0) { abort(404); } $collection->title = strip_tags($request->input('title')); @@ -99,12 +93,13 @@ class CollectionController extends Controller $collection->visibility = $request->input('visibility'); $collection->published_at = now(); $collection->save(); + return CollectionService::setCollection($collection->id, $collection); } public function delete(Request $request, int $id) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $user = $request->user(); $collection = Collection::whereProfileId($user->profile_id)->findOrFail($id); @@ -113,7 +108,7 @@ class CollectionController extends Controller CollectionService::deleteCollection($id); - if($request->wantsJson()) { + if ($request->wantsJson()) { return 200; } @@ -122,13 +117,13 @@ class CollectionController extends Controller public function storeId(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ 'collection_id' => 'required|int|min:1|exists:collections,id', - 'post_id' => 'required|int|min:1' + 'post_id' => 'required|int|min:1', ]); - + $profileId = $request->user()->profile_id; $collectionId = $request->input('collection_id'); $postId = $request->input('post_id'); @@ -136,157 +131,153 @@ class CollectionController extends Controller $collection = Collection::whereProfileId($profileId)->findOrFail($collectionId); $count = $collection->items()->count(); - if($count) { + if ($count) { CollectionItem::whereCollectionId($collection->id) ->get() - ->filter(function($col) { + ->filter(function ($col) { return StatusService::get($col->object_id, false) == null; }) - ->each(function($col) use($collectionId) { + ->each(function ($col) use ($collectionId) { CollectionService::removeItem($collectionId, $col->object_id); $col->delete(); }); } $max = config('pixelfed.max_collection_length'); - if($count >= $max) { + if ($count >= $max) { abort(400, 'You can only add '.$max.' posts per collection'); } - $status = Status::whereScope('public') + $status = Status::whereIn('scope', ['public', 'unlisted']) ->whereProfileId($profileId) ->whereIn('type', ['photo', 'photo:album', 'video']) ->findOrFail($postId); $item = CollectionItem::firstOrCreate([ 'collection_id' => $collection->id, - 'object_type' => 'App\Status', - 'object_id' => $status->id - ],[ - 'order' => $count, + 'object_type' => 'App\Status', + 'object_id' => $status->id, + ], [ + 'order' => $count, ]); - CollectionService::addItem( - $collection->id, - $status->id, - $count - ); + CollectionService::deleteCollection($collection->id); $collection->updated_at = now(); $collection->save(); CollectionService::setCollection($collection->id, $collection); - return StatusService::get($status->id); + return StatusService::get($status->id, false); } public function getCollection(Request $request, $id) { - $user = $request->user(); - $collection = CollectionService::getCollection($id); + $user = $request->user(); + $collection = CollectionService::getCollection($id); - if(!$collection) { + if (! $collection) { return response()->json([], 404); } - if($collection['published_at'] == null || $collection['visibility'] != 'public') { - abort_unless($user, 404); - if($user->profile_id != $collection['pid']) { - if(!$user->is_admin) { - abort_if($collection['visibility'] != 'private', 404); - abort_if(!FollowerService::follows($user->profile_id, $collection['pid']), 404); - } - } - } + if ($collection['published_at'] == null || $collection['visibility'] != 'public') { + abort_unless($user, 404); + if ($user->profile_id != $collection['pid']) { + if (! $user->is_admin) { + abort_if($collection['visibility'] != 'private', 404); + abort_if(! FollowerService::follows($user->profile_id, $collection['pid']), 404); + } + } + } return $collection; } public function getItems(Request $request, int $id) { - $user = $request->user(); - $collection = CollectionService::getCollection($id); + $user = $request->user(); + $collection = CollectionService::getCollection($id); - if(!$collection) { + if (! $collection) { return response()->json([], 404); } - if($collection['published_at'] == null || $collection['visibility'] != 'public') { - abort_unless($user, 404); - if($user->profile_id != $collection['pid']) { - if(!$user->is_admin) { - abort_if($collection['visibility'] != 'private', 404); - abort_if(!FollowerService::follows($user->profile_id, $collection['pid']), 404); - } - } - } + if ($collection['published_at'] == null || $collection['visibility'] != 'public') { + abort_unless($user, 404); + if ($user->profile_id != $collection['pid']) { + if (! $user->is_admin) { + abort_if($collection['visibility'] != 'private', 404); + abort_if(! FollowerService::follows($user->profile_id, $collection['pid']), 404); + } + } + } $page = $request->input('page') ?? 1; $start = $page == 1 ? 0 : ($page * 10 - 10); $end = $start + 10; $items = CollectionService::getItems($id, $start, $end); return collect($items) - ->map(function($id) { - return StatusService::get($id); - }) - ->filter(function($item) { - return $item && isset($item['account'], $item['media_attachments']); - }) - ->values(); + ->map(function ($id) { + return StatusService::get($id, false); + }) + ->filter(function ($item) { + return $item && ($item['visibility'] == 'public' || $item['visibility'] == 'unlisted') && isset($item['account'], $item['media_attachments']); + }) + ->values(); } public function getUserCollections(Request $request, int $id) { - $user = $request->user(); - $pid = $user ? $user->profile_id : null; - $follows = false; - $visibility = ['public']; + $user = $request->user(); + $pid = $user ? $user->profile_id : null; + $follows = false; + $visibility = ['public']; $profile = AccountService::get($id, true); - if(!$profile || !isset($profile['id'])) { + if (! $profile || ! isset($profile['id'])) { return response()->json([], 404); } - if($pid) { - $follows = FollowerService::follows($pid, $profile['id']); + if ($pid) { + $follows = FollowerService::follows($pid, $profile['id']); } - if($profile['locked']) { - abort_if(!$pid, 404); - if(!$user->is_admin) { - abort_if($profile['id'] != $pid && $follows == false, 404); + if ($profile['locked']) { + abort_if(! $pid, 404); + if (! $user->is_admin) { + abort_if($profile['id'] != $pid && $follows == false, 404); } } $owner = $pid ? $pid == $profile['id'] : false; - if($follows) { - $visibility = ['public', 'private']; + if ($follows) { + $visibility = ['public', 'private']; } - if($pid && $pid == $profile['id']) { - $visibility = ['public', 'private', 'draft']; + if ($pid && $pid == $profile['id']) { + $visibility = ['public', 'private', 'draft']; } return Collection::whereProfileId($profile['id']) - ->whereIn('visibility', $visibility) - ->when(!$owner, function($q, $owner) { - return $q->whereNotNull('published_at'); - }) + ->whereIn('visibility', $visibility) + ->when(! $owner, function ($q, $owner) { + return $q->whereNotNull('published_at'); + }) ->orderByDesc('id') ->paginate(9) - ->map(function($collection) { - return CollectionService::getCollection($collection->id); - }); + ->map(function ($collection) { + return CollectionService::getCollection($collection->id); + }); } public function deleteId(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ 'collection_id' => 'required|int|min:1|exists:collections,id', - 'post_id' => 'required|int|min:1' + 'post_id' => 'required|int|min:1', ]); - + $profileId = $request->user()->profile_id; $collectionId = $request->input('collection_id'); $postId = $request->input('post_id'); @@ -294,11 +285,11 @@ class CollectionController extends Controller $collection = Collection::whereProfileId($profileId)->findOrFail($collectionId); $count = $collection->items()->count(); - if($count == 1) { + if ($count == 1) { abort(400, 'You cannot delete the only post of a collection!'); } - $status = Status::whereScope('public') + $status = Status::whereIn('scope', ['public', 'unlisted']) ->whereIn('type', ['photo', 'photo:album', 'video']) ->findOrFail($postId); @@ -312,7 +303,7 @@ class CollectionController extends Controller CollectionItem::whereCollectionId($collection->id) ->orderBy('created_at') ->get() - ->each(function($item, $index) { + ->each(function ($item, $index) { $item->order = $index; $item->save(); }); @@ -323,4 +314,31 @@ class CollectionController extends Controller return 200; } + + public function getSelfCollections(Request $request) + { + abort_if(! $request->user(), 404); + $user = $request->user(); + $pid = $user->profile_id; + + $profile = AccountService::get($pid, true); + if (! $profile || ! isset($profile['id'])) { + return response()->json([], 404); + } + + return Collection::whereProfileId($pid) + ->orderByDesc('id') + ->paginate(9) + ->map(function ($collection) { + $c = CollectionService::getCollection($collection->id); + $c['items'] = collect(CollectionService::getItems($collection->id)) + ->map(function ($id) { + return StatusService::get($id, false); + })->filter()->values(); + + return $c; + }) + ->filter() + ->values(); + } } diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index 42a5490d0..f529b0ebd 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -2,23 +2,18 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use Auth; -use DB; -use Cache; - -use App\Comment; use App\Jobs\CommentPipeline\CommentPipeline; use App\Jobs\StatusPipeline\NewStatusPipeline; -use App\Util\Lexer\Autolink; -use App\Profile; -use App\Status; -use App\UserFilter; -use League\Fractal; -use App\Transformer\Api\StatusTransformer; -use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\Services\StatusService; +use App\Status; +use App\Transformer\Api\StatusTransformer; +use App\UserFilter; +use Auth; +use DB; +use Illuminate\Http\Request; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; +use Purify; class CommentController extends Controller { @@ -33,9 +28,9 @@ class CommentController extends Controller abort(403); } $this->validate($request, [ - 'item' => 'required|integer|min:1', - 'comment' => 'required|string|max:'.(int) config('pixelfed.max_caption_length'), - 'sensitive' => 'nullable|boolean' + 'item' => 'required|integer|min:1', + 'comment' => 'required|string|max:'.config_cache('pixelfed.max_caption_length'), + 'sensitive' => 'nullable|boolean', ]); $comment = $request->input('comment'); $statusId = $request->input('item'); @@ -45,7 +40,7 @@ class CommentController extends Controller $profile = $user->profile; $status = Status::findOrFail($statusId); - if($status->comments_disabled == true) { + if ($status->comments_disabled == true) { return; } @@ -55,18 +50,19 @@ class CommentController extends Controller ->whereFilterableId($profile->id) ->exists(); - if($filtered == true) { + if ($filtered == true) { return; } - $reply = DB::transaction(function() use($comment, $status, $profile, $nsfw) { + $reply = DB::transaction(function () use ($comment, $status, $profile, $nsfw) { + $defaultCaption = config_cache('database.default') === 'mysql' ? null : ""; + $scope = $profile->is_private == true ? 'private' : 'public'; - $autolink = Autolink::create()->autolink($comment); - $reply = new Status(); + $reply = new Status; $reply->profile_id = $profile->id; $reply->is_nsfw = $nsfw; - $reply->caption = e($comment); - $reply->rendered = $autolink; + $reply->caption = Purify::clean($comment); + $reply->rendered = $defaultCaption; $reply->in_reply_to_id = $status->id; $reply->in_reply_to_profile_id = $status->profile_id; $reply->scope = $scope; @@ -81,9 +77,9 @@ class CommentController extends Controller CommentPipeline::dispatch($status, $reply); if ($request->ajax()) { - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $entity = new Fractal\Resource\Item($reply, new StatusTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $entity = new Fractal\Resource\Item($reply, new StatusTransformer); $entity = $fractal->createData($entity)->toArray(); $response = [ 'code' => 200, diff --git a/app/Http/Controllers/ComposeController.php b/app/Http/Controllers/ComposeController.php index 9be50f346..2069e2693 100644 --- a/app/Http/Controllers/ComposeController.php +++ b/app/Http/Controllers/ComposeController.php @@ -2,293 +2,295 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use Auth, Cache, DB, Storage, URL; -use Carbon\Carbon; -use App\{ - Avatar, - Collection, - CollectionItem, - Hashtag, - Like, - Media, - MediaTag, - Notification, - Profile, - Place, - Status, - UserFilter, - UserSetting -}; -use App\Models\Poll; -use App\Transformer\Api\{ - MediaTransformer, - MediaDraftTransformer, - StatusTransformer, - StatusStatelessTransformer -}; -use League\Fractal; -use App\Util\Media\Filter; -use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; -use App\Jobs\AvatarPipeline\AvatarOptimize; +use App\Collection; +use App\CollectionItem; +use App\Hashtag; use App\Jobs\ImageOptimizePipeline\ImageOptimize; -use App\Jobs\ImageOptimizePipeline\ImageThumbnail; use App\Jobs\StatusPipeline\NewStatusPipeline; -use App\Jobs\VideoPipeline\{ - VideoOptimize, - VideoPostProcess, - VideoThumbnail -}; +use App\Jobs\VideoPipeline\VideoThumbnail; +use App\Media; +use App\MediaTag; +use App\Models\Poll; +use App\Notification; +use App\Profile; use App\Services\AccountService; use App\Services\CollectionService; -use App\Services\NotificationService; -use App\Services\MediaPathService; use App\Services\MediaBlocklistService; +use App\Services\MediaPathService; use App\Services\MediaStorageService; use App\Services\MediaTagService; -use App\Services\StatusService; use App\Services\SnowflakeService; -use Illuminate\Support\Str; -use App\Util\Lexer\Autolink; -use App\Util\Lexer\Extractor; +use App\Services\UserRoleService; +use App\Services\UserStorageService; +use App\Status; +use App\Transformer\Api\MediaTransformer; +use App\UserFilter; +use App\Util\Media\Filter; use App\Util\Media\License; -use Image; +use Auth; +use Cache; +use DB; +use Purify; +use Illuminate\Http\Request; +use Illuminate\Support\Str; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; class ComposeController extends Controller { - protected $fractal; + protected $fractal; - public function __construct() - { - $this->middleware('auth'); - $this->fractal = new Fractal\Manager(); - $this->fractal->setSerializer(new ArraySerializer()); - } + public function __construct() + { + $this->middleware('auth'); + $this->fractal = new Fractal\Manager; + $this->fractal->setSerializer(new ArraySerializer); + } - public function show(Request $request) - { - return view('status.compose'); - } + public function show(Request $request) + { + return view('status.compose'); + } - public function mediaUpload(Request $request) - { - abort_if(!$request->user(), 403); + public function mediaUpload(Request $request) + { + abort_if(! $request->user(), 403); - $this->validate($request, [ - 'file.*' => [ - 'required_without:file', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'file' => [ - 'required_without:file.*', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'filter_name' => 'nullable|string|max:24', - 'filter_class' => 'nullable|alpha_dash|max:24' - ]); + $this->validate($request, [ + 'file.*' => [ + 'required_without:file', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + ], + 'file' => [ + 'required_without:file.*', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + ], + 'filter_name' => 'nullable|string|max:24', + 'filter_class' => 'nullable|alpha_dash|max:24', + ]); - $user = Auth::user(); - $profile = $user->profile; + $user = $request->user(); + $profile = $user->profile; + abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); - $limitKey = 'compose:rate-limit:media-upload:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); + $limitKey = 'compose:rate-limit:media-upload:'.$user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { + $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); - return $dailyLimit >= 1250; - }); + return $dailyLimit >= 1250; + }); - abort_if($limitReached == true, 429); + abort_if($limitReached == true, 429); - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { - return Media::whereUserId($user->id)->sum('size') / 1000; - }); - $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { - abort(403, 'Account size limit reached.'); - } - } + $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; + $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; + $accountSize = UserStorageService::get($user->id); + abort_if($accountSize === -1, 403, 'Invalid request.'); + $photo = $request->file('file'); + $fileSize = $photo->getSize(); + $sizeInKbs = (int) ceil($fileSize / 1000); + $updatedAccountSize = (int) $accountSize + (int) $sizeInKbs; - $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; - $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; + if ((bool) config_cache('pixelfed.enforce_account_limit') == true) { + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($updatedAccountSize >= $limit) { + abort(403, 'Account size limit reached.'); + } + } - $photo = $request->file('file'); + $mimes = explode(',', config_cache('pixelfed.media_types')); - $mimes = explode(',', config_cache('pixelfed.media_types')); + abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format'); - abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format'); + $storagePath = MediaPathService::get($user, 2); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + $mime = $photo->getMimeType(); - $storagePath = MediaPathService::get($user, 2); - $path = $photo->storePublicly($storagePath); - $hash = \hash_file('sha256', $photo); - $mime = $photo->getMimeType(); + abort_if(MediaBlocklistService::exists($hash) == true, 451); - abort_if(MediaBlocklistService::exists($hash) == true, 451); + $media = new Media; + $media->status_id = null; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->caption = ''; + $media->mime = $mime; + $media->filter_class = $filterClass; + $media->filter_name = $filterName; + $media->version = '3'; + $media->save(); - $media = new Media(); - $media->status_id = null; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $photo->getSize(); - $media->mime = $mime; - $media->filter_class = $filterClass; - $media->filter_name = $filterName; - $media->version = 3; - $media->save(); + $preview_url = $media->url().'?v='.time(); + $url = $media->url().'?v='.time(); - $preview_url = $media->url() . '?v=' . time(); - $url = $media->url() . '?v=' . time(); + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + case 'image/webp': + ImageOptimize::dispatch($media)->onQueue('mmo'); + break; - switch ($media->mime) { - case 'image/jpeg': - case 'image/png': - case 'image/webp': - ImageOptimize::dispatch($media)->onQueue('mmo'); - break; + case 'video/mp4': + VideoThumbnail::dispatch($media)->onQueue('mmo'); + $preview_url = '/storage/no-preview.png'; + $url = '/storage/no-preview.png'; + break; - case 'video/mp4': - VideoThumbnail::dispatch($media)->onQueue('mmo'); - $preview_url = '/storage/no-preview.png'; - $url = '/storage/no-preview.png'; - break; + default: + break; + } - default: - break; - } + $user->storage_used = (int) $updatedAccountSize; + $user->storage_used_updated_at = now(); + $user->save(); - Cache::forget($limitKey); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - $res['preview_url'] = $preview_url; - $res['url'] = $url; - return response()->json($res); - } + Cache::forget($limitKey); + $resource = new Fractal\Resource\Item($media, new MediaTransformer); + $res = $this->fractal->createData($resource)->toArray(); + $res['preview_url'] = $preview_url; + $res['url'] = $url; - public function mediaUpdate(Request $request) - { - $this->validate($request, [ - 'id' => 'required', - 'file' => function() { - return [ - 'required', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ]; - }, - ]); + return response()->json($res); + } - $user = Auth::user(); + public function mediaUpdate(Request $request) + { + $this->validate($request, [ + 'id' => 'required', + 'file' => function () { + return [ + 'required', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + ]; + }, + ]); - $limitKey = 'compose:rate-limit:media-updates:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); + $user = Auth::user(); + abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); - return $dailyLimit >= 1500; - }); + $limitKey = 'compose:rate-limit:media-updates:'.$user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { + $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); - abort_if($limitReached == true, 429); + return $dailyLimit >= 1500; + }); - $photo = $request->file('file'); - $id = $request->input('id'); + abort_if($limitReached == true, 429); - $media = Media::whereUserId($user->id) - ->whereProfileId($user->profile_id) - ->whereNull('status_id') - ->findOrFail($id); + $photo = $request->file('file'); + $id = $request->input('id'); - $media->save(); + $media = Media::whereUserId($user->id) + ->whereProfileId($user->profile_id) + ->whereNull('status_id') + ->findOrFail($id); - $fragments = explode('/', $media->media_path); - $name = last($fragments); - array_pop($fragments); - $dir = implode('/', $fragments); - $path = $photo->storePubliclyAs($dir, $name); - $res = [ - 'url' => $media->url() . '?v=' . time() - ]; - ImageOptimize::dispatch($media)->onQueue('mmo'); - Cache::forget($limitKey); - return $res; - } + $media->save(); - public function mediaDelete(Request $request) - { - abort_if(!$request->user(), 403); + $fragments = explode('/', $media->media_path); + $name = last($fragments); + array_pop($fragments); + $dir = implode('/', $fragments); + $path = $photo->storePubliclyAs($dir, $name); + $res = [ + 'url' => $media->url().'?v='.time(), + ]; + ImageOptimize::dispatch($media)->onQueue('mmo'); + Cache::forget($limitKey); + UserStorageService::recalculateUpdateStorageUsed($request->user()->id); - $this->validate($request, [ - 'id' => 'required|integer|min:1|exists:media,id' - ]); + return $res; + } - $media = Media::whereNull('status_id') - ->whereUserId(Auth::id()) - ->findOrFail($request->input('id')); + public function mediaDelete(Request $request) + { + abort_if(! $request->user(), 403); - MediaStorageService::delete($media, true); + $this->validate($request, [ + 'id' => 'required|integer|min:1|exists:media,id', + ]); - return response()->json([ - 'msg' => 'Successfully deleted', - 'code' => 200 - ]); - } + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - public function searchTag(Request $request) - { - abort_if(!$request->user(), 403); + $media = Media::whereNull('status_id') + ->whereUserId(Auth::id()) + ->findOrFail($request->input('id')); - $this->validate($request, [ - 'q' => 'required|string|min:1|max:50' - ]); + MediaStorageService::delete($media, true); - $q = $request->input('q'); + UserStorageService::recalculateUpdateStorageUsed($request->user()->id); - if(Str::of($q)->startsWith('@')) { - if(strlen($q) < 3) { - return []; - } - $q = mb_substr($q, 1); - } + return response()->json([ + 'msg' => 'Successfully deleted', + 'code' => 200, + ]); + } - $blocked = UserFilter::whereFilterableType('App\Profile') - ->whereFilterType('block') - ->whereFilterableId($request->user()->profile_id) - ->pluck('user_id'); + public function searchTag(Request $request) + { + abort_if(! $request->user(), 403); - $blocked->push($request->user()->profile_id); + $this->validate($request, [ + 'q' => 'required|string|min:1|max:50', + ]); - $results = Profile::select('id','domain','username') - ->whereNotIn('id', $blocked) - ->whereNull('domain') - ->where('username','like','%'.$q.'%') - ->limit(15) - ->get() - ->map(function($r) { - return [ - 'id' => (string) $r->id, - 'name' => $r->username, - 'privacy' => true, - 'avatar' => $r->avatarUrl() - ]; - }); + $q = $request->input('q'); - return $results; - } + if (Str::of($q)->startsWith('@')) { + if (strlen($q) < 3) { + return []; + } + $q = mb_substr($q, 1); + } + + $user = $request->user(); + + abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + + $blocked = UserFilter::whereFilterableType('App\Profile') + ->whereFilterType('block') + ->whereFilterableId($request->user()->profile_id) + ->pluck('user_id'); + + $blocked->push($request->user()->profile_id); + + $results = Profile::select('id', 'domain', 'username') + ->whereNotIn('id', $blocked) + ->whereNull('domain') + ->where('username', 'like', '%'.$q.'%') + ->limit(15) + ->get() + ->map(function ($r) { + return [ + 'id' => (string) $r->id, + 'name' => $r->username, + 'privacy' => true, + 'avatar' => $r->avatarUrl(), + ]; + }); + + return $results; + } public function searchUntag(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ 'status_id' => 'required', - 'profile_id' => 'required' + 'profile_id' => 'required', ]); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + $user = $request->user(); $status_id = $request->input('status_id'); $profile_id = (int) $request->input('profile_id'); @@ -299,7 +301,7 @@ class ComposeController extends Controller ->whereProfileId($profile_id) ->first(); - if(!$tag) { + if (! $tag) { return []; } Notification::whereItemType('App\MediaTag') @@ -313,506 +315,521 @@ class ComposeController extends Controller return [200]; } - public function searchLocation(Request $request) - { - abort_if(!$request->user(), 403); - $this->validate($request, [ - 'q' => 'required|string|max:100' - ]); - $pid = $request->user()->profile_id; - abort_if(!$pid, 400); - $q = e($request->input('q')); + public function searchLocation(Request $request) + { + abort_if(! $request->user(), 403); + $this->validate($request, [ + 'q' => 'required|string|max:100', + ]); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + $pid = $request->user()->profile_id; + abort_if(! $pid, 400); + $q = e($request->input('q')); - $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function() { - $minId = SnowflakeService::byDate(now()->subDays(290)); - if(config('database.default') == 'pgsql') { - return Status::selectRaw('id, place_id, count(place_id) as pc') - ->whereNotNull('place_id') - ->where('id', '>', $minId) - ->orderByDesc('pc') - ->groupBy(['place_id', 'id']) - ->limit(400) - ->get() - ->filter(function($post) { - return $post; - }) - ->map(function($place) { - return [ - 'id' => $place->place_id, - 'count' => $place->pc - ]; - }) - ->unique('id') - ->values(); - } - return Status::selectRaw('id, place_id, count(place_id) as pc') - ->whereNotNull('place_id') - ->where('id', '>', $minId) - ->groupBy('place_id') - ->orderByDesc('pc') - ->limit(400) - ->get() - ->filter(function($post) { - return $post; - }) - ->map(function($place) { - return [ - 'id' => $place->place_id, - 'count' => $place->pc - ]; - }); - }); - $q = '%' . $q . '%'; - $wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like'; + $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function () { + $minId = SnowflakeService::byDate(now()->subDays(290)); + if (config('database.default') == 'pgsql') { + return Status::selectRaw('id, place_id, count(place_id) as pc') + ->whereNotNull('place_id') + ->where('id', '>', $minId) + ->orderByDesc('pc') + ->groupBy(['place_id', 'id']) + ->limit(400) + ->get() + ->filter(function ($post) { + return $post; + }) + ->map(function ($place) { + return [ + 'id' => $place->place_id, + 'count' => $place->pc, + ]; + }) + ->unique('id') + ->values(); + } - $places = DB::table('places') - ->where('name', $wildcard, $q) - ->limit((strlen($q) > 5 ? 360 : 30)) - ->get() - ->sortByDesc(function($place, $key) use($popular) { - return $popular->filter(function($p) use($place) { - return $p['id'] == $place->id; - })->map(function($p) use($place) { - return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1; - })->values(); - }) - ->map(function($r) { - return [ - 'id' => $r->id, - 'name' => $r->name, - 'country' => $r->country, - 'url' => url('/discover/places/' . $r->id . '/' . $r->slug) - ]; - }) - ->values() - ->all(); - return $places; - } + return Status::selectRaw('id, place_id, count(place_id) as pc') + ->whereNotNull('place_id') + ->where('id', '>', $minId) + ->groupBy('place_id') + ->orderByDesc('pc') + ->limit(400) + ->get() + ->filter(function ($post) { + return $post; + }) + ->map(function ($place) { + return [ + 'id' => $place->place_id, + 'count' => $place->pc, + ]; + }); + }); + $q = '%'.$q.'%'; + $wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like'; - public function searchMentionAutocomplete(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'q' => 'required|string|min:2|max:50' - ]); - - $q = $request->input('q'); - - if(Str::of($q)->startsWith('@')) { - if(strlen($q) < 3) { - return []; - } - } - - $blocked = UserFilter::whereFilterableType('App\Profile') - ->whereFilterType('block') - ->whereFilterableId($request->user()->profile_id) - ->pluck('user_id'); - - $blocked->push($request->user()->profile_id); - - $results = Profile::select('id','domain','username') - ->whereNotIn('id', $blocked) - ->where('username','like','%'.$q.'%') - ->groupBy('id', 'domain') - ->limit(15) - ->get() - ->map(function($profile) { - $username = $profile->domain ? substr($profile->username, 1) : $profile->username; + $places = DB::table('places') + ->where('name', $wildcard, $q) + ->limit((strlen($q) > 5 ? 360 : 30)) + ->get() + ->sortByDesc(function ($place, $key) use ($popular) { + return $popular->filter(function ($p) use ($place) { + return $p['id'] == $place->id; + })->map(function ($p) use ($place) { + return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1; + })->values(); + }) + ->map(function ($r) { return [ - 'key' => '@' . str_limit($username, 30), + 'id' => $r->id, + 'name' => $r->name, + 'country' => $r->country, + 'url' => url('/discover/places/'.$r->id.'/'.$r->slug), + ]; + }) + ->values() + ->all(); + + return $places; + } + + public function searchMentionAutocomplete(Request $request) + { + abort_if(! $request->user(), 403); + + $this->validate($request, [ + 'q' => 'required|string|min:2|max:50', + ]); + + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + + $q = $request->input('q'); + + if (Str::of($q)->startsWith('@')) { + if (strlen($q) < 3) { + return []; + } + } + + $blocked = UserFilter::whereFilterableType('App\Profile') + ->whereFilterType('block') + ->whereFilterableId($request->user()->profile_id) + ->pluck('user_id'); + + $blocked->push($request->user()->profile_id); + + $results = Profile::select('id', 'domain', 'username') + ->whereNotIn('id', $blocked) + ->where('username', 'like', '%'.$q.'%') + ->groupBy('id', 'domain') + ->limit(15) + ->get() + ->map(function ($profile) { + $username = $profile->domain ? substr($profile->username, 1) : $profile->username; + + return [ + 'key' => '@'.str_limit($username, 30), 'value' => $username, ]; - }); + }); - return $results; - } + return $results; + } - public function searchHashtagAutocomplete(Request $request) - { - abort_if(!$request->user(), 403); + public function searchHashtagAutocomplete(Request $request) + { + abort_if(! $request->user(), 403); - $this->validate($request, [ - 'q' => 'required|string|min:2|max:50' - ]); + $this->validate($request, [ + 'q' => 'required|string|min:2|max:50', + ]); - $q = $request->input('q'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - $results = Hashtag::select('slug') - ->where('slug', 'like', '%'.$q.'%') - ->whereIsNsfw(false) - ->whereIsBanned(false) - ->limit(5) - ->get() - ->map(function($tag) { - return [ - 'key' => '#' . $tag->slug, - 'value' => $tag->slug - ]; - }); + $q = $request->input('q'); - return $results; - } + $results = Hashtag::select('slug') + ->where('slug', 'like', '%'.$q.'%') + ->whereIsNsfw(false) + ->whereIsBanned(false) + ->limit(5) + ->get() + ->map(function ($tag) { + return [ + 'key' => '#'.$tag->slug, + 'value' => $tag->slug, + ]; + }); - public function store(Request $request) - { - $this->validate($request, [ - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), - 'media.*' => 'required', - 'media.*.id' => 'required|integer|min:1', - 'media.*.filter_class' => 'nullable|alpha_dash|max:30', - 'media.*.license' => 'nullable|string|max:140', - 'media.*.alt' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'), - 'cw' => 'nullable|boolean', - 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10', - 'place' => 'nullable', - 'comments_disabled' => 'nullable', - 'tagged' => 'nullable', - 'license' => 'nullable|integer|min:1|max:16', - 'collections' => 'sometimes|array|min:1|max:5', - 'spoiler_text' => 'nullable|string|max:140', - // 'optimize_media' => 'nullable' - ]); + return $results; + } - if(config('costar.enabled') == true) { - $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null && $request->caption) { - $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($request->caption, $kw) == true) { - abort(400, 'Invalid object'); - } - } - } - } + public function store(Request $request) + { + $this->validate($request, [ + 'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500), + 'media.*' => 'required', + 'media.*.id' => 'required|integer|min:1', + 'media.*.filter_class' => 'nullable|alpha_dash|max:30', + 'media.*.license' => 'nullable|string|max:140', + 'media.*.alt' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'), + 'cw' => 'nullable|boolean', + 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10', + 'place' => 'nullable', + 'comments_disabled' => 'nullable', + 'tagged' => 'nullable', + 'license' => 'nullable|integer|min:1|max:16', + 'collections' => 'sometimes|array|min:1|max:5', + 'spoiler_text' => 'nullable|string|max:140', + // 'optimize_media' => 'nullable' + ]); - $user = Auth::user(); - $profile = $user->profile; + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - $limitKey = 'compose:rate-limit:store:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Status::whereProfileId($user->profile_id) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->where('created_at', '>', now()->subDays(1)) - ->count(); + if (config('costar.enabled') == true) { + $blockedKeywords = config('costar.keyword.block'); + if ($blockedKeywords !== null && $request->caption) { + $keywords = config('costar.keyword.block'); + foreach ($keywords as $kw) { + if (Str::contains($request->caption, $kw) == true) { + abort(400, 'Invalid object'); + } + } + } + } - return $dailyLimit >= 1000; - }); + $user = $request->user(); + $profile = $user->profile; - abort_if($limitReached == true, 429); + $limitKey = 'compose:rate-limit:store:'.$user->id; + $limitTtl = now()->addMinutes(15); + // $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { + // $dailyLimit = Status::whereProfileId($user->profile_id) + // ->whereNull('in_reply_to_id') + // ->whereNull('reblog_of_id') + // ->where('created_at', '>', now()->subDays(1)) + // ->count(); - $license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null; + // return $dailyLimit >= 1000; + // }); - $visibility = $request->input('visibility'); - $medias = $request->input('media'); - $attachments = []; - $status = new Status; - $mimes = []; - $place = $request->input('place'); - $cw = $request->input('cw'); - $tagged = $request->input('tagged'); - $optimize_media = (bool) $request->input('optimize_media'); + // abort_if($limitReached == true, 429); - foreach($medias as $k => $media) { - if($k + 1 > config_cache('pixelfed.max_album_length')) { - continue; - } - $m = Media::findOrFail($media['id']); - if($m->profile_id !== $profile->id || $m->status_id) { - abort(403, 'Invalid media id'); - } - $m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null; - $m->license = $license; - $m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null; - $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k; + $license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null; - if($cw == true || $profile->cw == true) { - $m->is_nsfw = $cw; - $status->is_nsfw = $cw; - } - $m->save(); - $attachments[] = $m; - array_push($mimes, $m->mime); - } + $visibility = $request->input('visibility'); + $medias = $request->input('media'); + $attachments = []; + $status = new Status; + $mimes = []; + $place = $request->input('place'); + $cw = $request->input('cw'); + $tagged = $request->input('tagged'); + $optimize_media = (bool) $request->input('optimize_media'); - abort_if(empty($attachments), 422); + foreach ($medias as $k => $media) { + if ($k + 1 > config_cache('pixelfed.max_album_length')) { + continue; + } + $m = Media::findOrFail($media['id']); + if ($m->profile_id !== $profile->id || $m->status_id) { + abort(403, 'Invalid media id'); + } + $m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null; + $m->license = $license; + $m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null; + $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k; - $mediaType = StatusController::mimeTypeCheck($mimes); + if ($cw == true || $profile->cw == true) { + $m->is_nsfw = $cw; + $status->is_nsfw = $cw; + } + $m->save(); + $attachments[] = $m; + array_push($mimes, $m->mime); + } - if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) { - abort(400, __('exception.compose.invalid.album')); - } + abort_if(empty($attachments), 422); - if($place && is_array($place)) { - $status->place_id = $place['id']; - } + $mediaType = StatusController::mimeTypeCheck($mimes); - if($request->filled('comments_disabled')) { - $status->comments_disabled = (bool) $request->input('comments_disabled'); - } + if (in_array($mediaType, ['photo', 'video', 'photo:album']) == false) { + abort(400, __('exception.compose.invalid.album')); + } - if($request->filled('spoiler_text') && $cw) { - $status->cw_summary = $request->input('spoiler_text'); - } + if ($place && is_array($place)) { + $status->place_id = $place['id']; + } - $status->caption = strip_tags($request->caption); - $status->rendered = Autolink::create()->autolink($status->caption); - $status->scope = 'draft'; - $status->visibility = 'draft'; - $status->profile_id = $profile->id; - $status->save(); + if ($request->filled('comments_disabled')) { + $status->comments_disabled = (bool) $request->input('comments_disabled'); + } - foreach($attachments as $media) { - $media->status_id = $status->id; - $media->save(); - } + if ($request->filled('spoiler_text') && $cw) { + $status->cw_summary = $request->input('spoiler_text'); + } - $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; - $visibility = $profile->is_private ? 'private' : $visibility; - $cw = $profile->cw == true ? true : $cw; - $status->is_nsfw = $cw; - $status->visibility = $visibility; - $status->scope = $visibility; - $status->type = $mediaType; - $status->save(); + $defaultCaption = config_cache('database.default') === 'mysql' ? null : ""; + $status->caption = strip_tags($request->input('caption')) ?? $defaultCaption; + $status->rendered = $defaultCaption; + $status->scope = 'draft'; + $status->visibility = 'draft'; + $status->profile_id = $profile->id; + $status->save(); - foreach($tagged as $tg) { - $mt = new MediaTag; - $mt->status_id = $status->id; - $mt->media_id = $status->media->first()->id; - $mt->profile_id = $tg['id']; - $mt->tagged_username = $tg['name']; - $mt->is_public = true; - $mt->metadata = json_encode([ - '_v' => 1, - ]); - $mt->save(); - MediaTagService::set($mt->status_id, $mt->profile_id); - MediaTagService::sendNotification($mt); - } + foreach ($attachments as $media) { + $media->status_id = $status->id; + $media->save(); + } - if($request->filled('collections')) { - $collections = Collection::whereProfileId($profile->id) - ->find($request->input('collections')) - ->each(function($collection) use($status) { - $count = $collection->items()->count(); - CollectionItem::firstOrCreate([ - 'collection_id' => $collection->id, - 'object_type' => 'App\Status', - 'object_id' => $status->id - ], [ - 'order' => $count - ]); + $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; + $visibility = $profile->is_private ? 'private' : $visibility; + $cw = $profile->cw == true ? true : $cw; + $status->is_nsfw = $cw; + $status->visibility = $visibility; + $status->scope = $visibility; + $status->type = $mediaType; + $status->save(); - CollectionService::addItem( - $collection->id, - $status->id, - $count - ); + foreach ($tagged as $tg) { + $mt = new MediaTag; + $mt->status_id = $status->id; + $mt->media_id = $status->media->first()->id; + $mt->profile_id = $tg['id']; + $mt->tagged_username = $tg['name']; + $mt->is_public = true; + $mt->metadata = json_encode([ + '_v' => 1, + ]); + $mt->save(); + MediaTagService::set($mt->status_id, $mt->profile_id); + MediaTagService::sendNotification($mt); + } - $collection->updated_at = now(); + if ($request->filled('collections')) { + $collections = Collection::whereProfileId($profile->id) + ->find($request->input('collections')) + ->each(function ($collection) use ($status) { + $count = $collection->items()->count(); + CollectionItem::firstOrCreate([ + 'collection_id' => $collection->id, + 'object_type' => 'App\Status', + 'object_id' => $status->id, + ], [ + 'order' => $count, + ]); + + CollectionService::addItem( + $collection->id, + $status->id, + $count + ); + + $collection->updated_at = now(); $collection->save(); CollectionService::setCollection($collection->id, $collection); - }); - } + }); + } - NewStatusPipeline::dispatch($status); - Cache::forget('user:account:id:'.$profile->user_id); - Cache::forget('_api:statuses:recent_9:'.$profile->id); - Cache::forget('profile:status_count:'.$profile->id); - Cache::forget('status:transformer:media:attachments:'.$status->id); - Cache::forget($user->storageUsedKey()); - Cache::forget('profile:embed:' . $status->profile_id); - Cache::forget($limitKey); + NewStatusPipeline::dispatch($status); + Cache::forget('user:account:id:'.$profile->user_id); + Cache::forget('_api:statuses:recent_9:'.$profile->id); + Cache::forget('profile:status_count:'.$profile->id); + Cache::forget('status:transformer:media:attachments:'.$status->id); + Cache::forget('profile:embed:'.$status->profile_id); + Cache::forget($limitKey); - return $status->url(); - } + return $status->url(); + } - public function storeText(Request $request) - { - abort_unless(config('exp.top'), 404); - $this->validate($request, [ - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), - 'cw' => 'nullable|boolean', - 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10', - 'place' => 'nullable', - 'comments_disabled' => 'nullable', - 'tagged' => 'nullable', - ]); + public function storeText(Request $request) + { + abort_unless(config('exp.top'), 404); + $this->validate($request, [ + 'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500), + 'cw' => 'nullable|boolean', + 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10', + 'place' => 'nullable', + 'comments_disabled' => 'nullable', + 'tagged' => 'nullable', + ]); - if(config('costar.enabled') == true) { - $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null && $request->caption) { - $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($request->caption, $kw) == true) { - abort(400, 'Invalid object'); - } - } - } - } + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - $user = Auth::user(); - $profile = $user->profile; - $visibility = $request->input('visibility'); - $status = new Status; - $place = $request->input('place'); - $cw = $request->input('cw'); - $tagged = $request->input('tagged'); + if (config('costar.enabled') == true) { + $blockedKeywords = config('costar.keyword.block'); + if ($blockedKeywords !== null && $request->caption) { + $keywords = config('costar.keyword.block'); + foreach ($keywords as $kw) { + if (Str::contains($request->caption, $kw) == true) { + abort(400, 'Invalid object'); + } + } + } + } - if($place && is_array($place)) { - $status->place_id = $place['id']; - } + $user = $request->user(); + $profile = $user->profile; + $visibility = $request->input('visibility'); + $status = new Status; + $place = $request->input('place'); + $cw = $request->input('cw'); + $tagged = $request->input('tagged'); + $defaultCaption = config_cache('database.default') === 'mysql' ? null : ""; - if($request->filled('comments_disabled')) { - $status->comments_disabled = (bool) $request->input('comments_disabled'); - } + if ($place && is_array($place)) { + $status->place_id = $place['id']; + } - $status->caption = strip_tags($request->caption); - $status->profile_id = $profile->id; - $entities = []; - $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; - $cw = $profile->cw == true ? true : $cw; - $status->is_nsfw = $cw; - $status->visibility = $visibility; - $status->scope = $visibility; - $status->type = 'text'; - $status->rendered = Autolink::create()->autolink($status->caption); - $status->entities = json_encode(array_merge([ - 'timg' => [ - 'version' => 0, - 'bg_id' => 1, - 'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3', - 'length' => strlen($status->caption), - ] - ], $entities), JSON_UNESCAPED_SLASHES); - $status->save(); + if ($request->filled('comments_disabled')) { + $status->comments_disabled = (bool) $request->input('comments_disabled'); + } - foreach($tagged as $tg) { - $mt = new MediaTag; - $mt->status_id = $status->id; - $mt->media_id = $status->media->first()->id; - $mt->profile_id = $tg['id']; - $mt->tagged_username = $tg['name']; - $mt->is_public = true; - $mt->metadata = json_encode([ - '_v' => 1, - ]); - $mt->save(); - MediaTagService::set($mt->status_id, $mt->profile_id); - MediaTagService::sendNotification($mt); - } + $status->caption = $request->filled('caption') ? strip_tags($request->caption) : $defaultCaption; + $status->rendered = $defaultCaption; + $status->profile_id = $profile->id; + $entities = []; + $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; + $cw = $profile->cw == true ? true : $cw; + $status->is_nsfw = $cw; + $status->visibility = $visibility; + $status->scope = $visibility; + $status->type = 'text'; + $status->entities = json_encode(array_merge([ + 'timg' => [ + 'version' => 0, + 'bg_id' => 1, + 'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3', + 'length' => strlen($status->caption), + ], + ], $entities), JSON_UNESCAPED_SLASHES); + $status->save(); + foreach ($tagged as $tg) { + $mt = new MediaTag; + $mt->status_id = $status->id; + $mt->media_id = $status->media->first()->id; + $mt->profile_id = $tg['id']; + $mt->tagged_username = $tg['name']; + $mt->is_public = true; + $mt->metadata = json_encode([ + '_v' => 1, + ]); + $mt->save(); + MediaTagService::set($mt->status_id, $mt->profile_id); + MediaTagService::sendNotification($mt); + } - Cache::forget('user:account:id:'.$profile->user_id); - Cache::forget('_api:statuses:recent_9:'.$profile->id); - Cache::forget('profile:status_count:'.$profile->id); + Cache::forget('user:account:id:'.$profile->user_id); + Cache::forget('_api:statuses:recent_9:'.$profile->id); + Cache::forget('profile:status_count:'.$profile->id); - return $status->url(); - } + return $status->url(); + } - public function mediaProcessingCheck(Request $request) - { - $this->validate($request, [ - 'id' => 'required|integer|min:1' - ]); + public function mediaProcessingCheck(Request $request) + { + $this->validate($request, [ + 'id' => 'required|integer|min:1', + ]); - $media = Media::whereUserId($request->user()->id) - ->whereNull('status_id') - ->findOrFail($request->input('id')); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - if(config('pixelfed.media_fast_process')) { - return [ - 'finished' => true - ]; - } + $media = Media::whereUserId($request->user()->id) + ->whereNull('status_id') + ->findOrFail($request->input('id')); - $finished = false; + if (config('pixelfed.media_fast_process')) { + return [ + 'finished' => true, + ]; + } - switch ($media->mime) { - case 'image/jpeg': - case 'image/png': - case 'video/mp4': - $finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at; - break; + $finished = false; - default: - # code... - break; - } + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + case 'video/mp4': + $finished = (bool) config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at; + break; - return [ - 'finished' => $finished - ]; - } + default: + // code... + break; + } - public function composeSettings(Request $request) - { - $uid = $request->user()->id; - $default = [ - 'default_license' => 1, - 'media_descriptions' => false, - 'max_altext_length' => config_cache('pixelfed.max_altext_length') - ]; - $settings = AccountService::settings($uid); - if(isset($settings['other']) && isset($settings['other']['scope'])) { - $s = $settings['compose_settings']; - $s['default_scope'] = $settings['other']['scope']; - $settings['compose_settings'] = $s; - } + return [ + 'finished' => $finished, + ]; + } - return array_merge($default, $settings['compose_settings']); - } + public function composeSettings(Request $request) + { + $uid = $request->user()->id; + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - public function createPoll(Request $request) - { - $this->validate($request, [ - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), - 'cw' => 'nullable|boolean', - 'visibility' => 'required|string|in:public,private', - 'comments_disabled' => 'nullable', - 'expiry' => 'required|in:60,360,1440,10080', - 'pollOptions' => 'required|array|min:1|max:4' - ]); + $default = [ + 'default_license' => 1, + 'media_descriptions' => false, + 'max_altext_length' => config_cache('pixelfed.max_altext_length'), + ]; + $settings = AccountService::settings($uid); + if (isset($settings['other']) && isset($settings['other']['scope'])) { + $s = $settings['compose_settings']; + $s['default_scope'] = $settings['other']['scope']; + $settings['compose_settings'] = $s; + } - abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled'); + return array_merge($default, $settings['compose_settings']); + } - abort_if(Status::whereType('poll') - ->whereProfileId($request->user()->profile_id) - ->whereCaption($request->input('caption')) - ->where('created_at', '>', now()->subDays(2)) - ->exists() - , 422, 'Duplicate detected.'); + public function createPoll(Request $request) + { + $this->validate($request, [ + 'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500), + 'cw' => 'nullable|boolean', + 'visibility' => 'required|string|in:public,private', + 'comments_disabled' => 'nullable', + 'expiry' => 'required|in:60,360,1440,10080', + 'pollOptions' => 'required|array|min:1|max:4', + ]); + abort(404); + abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled'); + abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - $status = new Status; - $status->profile_id = $request->user()->profile_id; - $status->caption = $request->input('caption'); - $status->rendered = Autolink::create()->autolink($status->caption); - $status->visibility = 'draft'; - $status->scope = 'draft'; - $status->type = 'poll'; - $status->local = true; - $status->save(); + abort_if(Status::whereType('poll') + ->whereProfileId($request->user()->profile_id) + ->whereCaption($request->input('caption')) + ->where('created_at', '>', now()->subDays(2)) + ->exists(), 422, 'Duplicate detected.'); - $poll = new Poll; - $poll->status_id = $status->id; - $poll->profile_id = $status->profile_id; - $poll->poll_options = $request->input('pollOptions'); - $poll->expires_at = now()->addMinutes($request->input('expiry')); - $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { - return 0; - })->toArray(); - $poll->save(); + $status = new Status; + $status->profile_id = $request->user()->profile_id; + $status->caption = $request->input('caption'); + $status->visibility = 'draft'; + $status->scope = 'draft'; + $status->type = 'poll'; + $status->local = true; + $status->save(); - $status->visibility = $request->input('visibility'); - $status->scope = $request->input('visibility'); - $status->save(); + $poll = new Poll; + $poll->status_id = $status->id; + $poll->profile_id = $status->profile_id; + $poll->poll_options = $request->input('pollOptions'); + $poll->expires_at = now()->addMinutes($request->input('expiry')); + $poll->cached_tallies = collect($poll->poll_options)->map(function ($o) { + return 0; + })->toArray(); + $poll->save(); - NewStatusPipeline::dispatch($status); + $status->visibility = $request->input('visibility'); + $status->scope = $request->input('visibility'); + $status->save(); - return ['url' => $status->url()]; - } + NewStatusPipeline::dispatch($status); + + return ['url' => $status->url()]; + } } diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php index 3123b8d16..dbba9599a 100644 --- a/app/Http/Controllers/ContactController.php +++ b/app/Http/Controllers/ContactController.php @@ -50,4 +50,15 @@ class ContactController extends Controller return redirect()->back()->with('status', 'Success - Your message has been sent to admins.'); } + + public function showAdminResponse(Request $request, $id) + { + abort_if(!$request->user(), 404); + $uid = $request->user()->id; + $contact = Contact::whereUserId($uid) + ->whereNotNull('response') + ->whereNotNull('responded_at') + ->findOrFail($id); + return view('site.contact.admin-response', compact('contact')); + } } diff --git a/app/Http/Controllers/CuratedRegisterController.php b/app/Http/Controllers/CuratedRegisterController.php new file mode 100644 index 000000000..58bddb498 --- /dev/null +++ b/app/Http/Controllers/CuratedRegisterController.php @@ -0,0 +1,399 @@ +user(), 404); + return view('auth.curated-register.index', ['step' => 1]); + } + + public function concierge(Request $request) + { + abort_if($request->user(), 404); + $emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') && + $request->has('next') && + $request->session()->has('cur-reg-con.cr-id'); + return view('auth.curated-register.concierge', compact('emailConfirmed')); + } + + public function conciergeResponseSent(Request $request) + { + return view('auth.curated-register.user_response_sent'); + } + + public function conciergeFormShow(Request $request) + { + abort_if($request->user(), 404); + abort_unless( + $request->session()->has('cur-reg-con.email-confirmed') && + $request->session()->has('cur-reg-con.cr-id') && + $request->session()->has('cur-reg-con.ac-id'), 404); + $crid = $request->session()->get('cur-reg-con.cr-id'); + $arid = $request->session()->get('cur-reg-con.ac-id'); + $showCaptcha = config('instance.curated_registration.captcha_enabled'); + if($attempts = $request->session()->get('cur-reg-con-attempt')) { + $showCaptcha = $attempts && $attempts >= 2; + } else { + $showCaptcha = false; + } + $activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid); + return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha')); + } + + public function conciergeFormStore(Request $request) + { + abort_if($request->user(), 404); + $request->session()->increment('cur-reg-con-attempt'); + abort_unless( + $request->session()->has('cur-reg-con.email-confirmed') && + $request->session()->has('cur-reg-con.cr-id') && + $request->session()->has('cur-reg-con.ac-id'), 404); + $attempts = $request->session()->get('cur-reg-con-attempt'); + $messages = []; + $rules = [ + 'response' => 'required|string|min:5|max:1000', + 'crid' => 'required|integer|min:1', + 'acid' => 'required|integer|min:1' + ]; + if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + $this->validate($request, $rules, $messages); + $crid = $request->session()->get('cur-reg-con.cr-id'); + $acid = $request->session()->get('cur-reg-con.ac-id'); + abort_if((string) $crid !== $request->input('crid'), 404); + abort_if((string) $acid !== $request->input('acid'), 404); + + if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) { + return redirect()->back()->withErrors(['code' => 'You already replied to this request.']); + } + + $act = CuratedRegisterActivity::create([ + 'register_id' => $crid, + 'reply_to_id' => $acid, + 'type' => 'user_response', + 'message' => $request->input('response'), + 'from_user' => true, + 'action_required' => true, + ]); + + CuratedRegister::findOrFail($crid)->update(['user_has_responded' => true]); + $request->session()->pull('cur-reg-con'); + $request->session()->pull('cur-reg-con-attempt'); + + return view('auth.curated-register.user_response_sent'); + } + + public function conciergeStore(Request $request) + { + abort_if($request->user(), 404); + $rules = [ + 'sid' => 'required_if:action,email|integer|min:1|max:20000000', + 'id' => 'required_if:action,email|integer|min:1|max:20000000', + 'code' => 'required_if:action,email', + 'action' => 'required|string|in:email,message', + 'email' => 'required_if:action,email|email', + 'response' => 'required_if:action,message|string|min:20|max:1000', + ]; + $messages = []; + if(config('instance.curated_registration.captcha_enabled')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + $this->validate($request, $rules, $messages); + + $action = $request->input('action'); + $sid = $request->input('sid'); + $id = $request->input('id'); + $code = $request->input('code'); + $email = $request->input('email'); + + $cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid); + $ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id); + + if(!hash_equals($ac->secret_code, $code)) { + return redirect()->back()->withErrors(['code' => 'Invalid code']); + } + + if(!hash_equals($cr->email, $email)) { + return redirect()->back()->withErrors(['email' => 'Invalid email']); + } + + $request->session()->put('cur-reg-con.email-confirmed', true); + $request->session()->put('cur-reg-con.cr-id', $cr->id); + $request->session()->put('cur-reg-con.ac-id', $ac->id); + $emailConfirmed = true; + return redirect('/auth/sign_up/concierge/form'); + } + + public function confirmEmail(Request $request) + { + if($request->user()) { + return redirect(route('help.email-confirmation-issues')); + } + return view('auth.curated-register.confirm_email'); + } + + public function emailConfirmed(Request $request) + { + if($request->user()) { + return redirect(route('help.email-confirmation-issues')); + } + return view('auth.curated-register.email_confirmed'); + } + + public function resendConfirmation(Request $request) + { + return view('auth.curated-register.resend-confirmation'); + } + + public function resendConfirmationProcess(Request $request) + { + $rules = [ + 'email' => [ + 'required', + 'string', + app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email', + 'exists:curated_registers', + ] + ]; + + $messages = []; + + if(config('instance.curated_registration.captcha_enabled')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + + $this->validate($request, $rules, $messages); + + $cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first(); + if(!$cur) { + return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']); + } + + $totalCount = CuratedRegisterActivity::whereRegisterId($cur->id) + ->whereType('user_resend_email_confirmation') + ->count(); + + if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) { + return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please contact the admin team.']); + } + + $count = CuratedRegisterActivity::whereRegisterId($cur->id) + ->whereType('user_resend_email_confirmation') + ->where('created_at', '>', now()->subHours(12)) + ->count(); + + if($count) { + return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']); + } + + CuratedRegisterActivity::create([ + 'register_id' => $cur->id, + 'type' => 'user_resend_email_confirmation', + 'admin_only_view' => true, + 'from_admin' => false, + 'from_user' => false, + 'action_required' => false, + ]); + + Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur)); + return view('auth.curated-register.resent-confirmation'); + return $request->all(); + } + + public function confirmEmailHandle(Request $request) + { + $rules = [ + 'sid' => 'required', + 'code' => 'required' + ]; + $messages = []; + if(config('instance.curated_registration.captcha_enabled')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + $this->validate($request, $rules, $messages); + + $cr = CuratedRegister::whereNull('email_verified_at') + ->where('created_at', '>', now()->subHours(24)) + ->find($request->input('sid')); + if(!$cr) { + return redirect(route('help.email-confirmation-issues')); + } + if(!hash_equals($cr->verify_code, $request->input('code'))) { + return redirect(route('help.email-confirmation-issues')); + } + $cr->email_verified_at = now(); + $cr->save(); + + if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) { + CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr); + } + return view('auth.curated-register.email_confirmed'); + } + + public function proceed(Request $request) + { + $this->validate($request, [ + 'step' => 'required|integer|in:1,2,3,4' + ]); + $step = $request->input('step'); + + switch($step) { + case 1: + $step = 2; + $request->session()->put('cur-step', 1); + return view('auth.curated-register.index', compact('step')); + break; + + case 2: + $this->stepTwo($request); + $step = 3; + $request->session()->put('cur-step', 2); + return view('auth.curated-register.index', compact('step')); + break; + + case 3: + $this->stepThree($request); + $step = 3; + $request->session()->put('cur-step', 3); + $verifiedEmail = true; + $request->session()->pull('cur-reg'); + return view('auth.curated-register.index', compact('step', 'verifiedEmail')); + break; + } + } + + protected function stepTwo($request) + { + if($request->filled('reason')) { + $request->session()->put('cur-reg.form-reason', $request->input('reason')); + } + if($request->filled('username')) { + $request->session()->put('cur-reg.form-username', $request->input('username')); + } + if($request->filled('email')) { + $request->session()->put('cur-reg.form-email', $request->input('email')); + } + $this->validate($request, [ + 'username' => [ + 'required', + 'min:2', + 'max:15', + 'unique:curated_registers', + 'unique:users', + function ($attribute, $value, $fail) { + $dash = substr_count($value, '-'); + $underscore = substr_count($value, '_'); + $period = substr_count($value, '.'); + + if(ends_with($value, ['.php', '.js', '.css'])) { + return $fail('Username is invalid.'); + } + + if(($dash + $underscore + $period) > 1) { + return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); + } + + if (!ctype_alnum($value[0])) { + return $fail('Username is invalid. Must start with a letter or number.'); + } + + if (!ctype_alnum($value[strlen($value) - 1])) { + return $fail('Username is invalid. Must end with a letter or number.'); + } + + $val = str_replace(['_', '.', '-'], '', $value); + if(!ctype_alnum($val)) { + return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); + } + + $restricted = RestrictedNames::get(); + if (in_array(strtolower($value), array_map('strtolower', $restricted))) { + return $fail('Username cannot be used.'); + } + }, + ], + 'email' => [ + 'required', + 'string', + app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email', + 'max:255', + 'unique:users', + 'unique:curated_registers', + function ($attribute, $value, $fail) { + $banned = EmailService::isBanned($value); + if($banned) { + return $fail('Email is invalid.'); + } + }, + ], + 'password' => 'required|min:8', + 'password_confirmation' => 'required|same:password', + 'reason' => 'required|min:20|max:1000', + 'agree' => 'required|accepted' + ]); + $request->session()->put('cur-reg.form-email', $request->input('email')); + $request->session()->put('cur-reg.form-password', $request->input('password')); + } + + protected function stepThree($request) + { + $this->validate($request, [ + 'email' => [ + 'required', + 'string', + app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email', + 'max:255', + 'unique:users', + 'unique:curated_registers', + function ($attribute, $value, $fail) { + $banned = EmailService::isBanned($value); + if($banned) { + return $fail('Email is invalid.'); + } + }, + ] + ]); + $cr = new CuratedRegister; + $cr->email = $request->email; + $cr->username = $request->session()->get('cur-reg.form-username'); + $cr->password = bcrypt($request->session()->get('cur-reg.form-password')); + $cr->ip_address = $request->ip(); + $cr->reason_to_join = $request->session()->get('cur-reg.form-reason'); + $cr->verify_code = Str::random(40); + $cr->save(); + + Mail::to($cr->email)->send(new CuratedRegisterConfirmEmail($cr)); + } +} diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php index 1f7e04e59..b2e0df3a2 100644 --- a/app/Http/Controllers/DirectMessageController.php +++ b/app/Http/Controllers/DirectMessageController.php @@ -2,857 +2,909 @@ namespace App\Http\Controllers; -use Auth, Cache; -use Illuminate\Http\Request; -use App\{ - DirectMessage, - Media, - Notification, - Profile, - Status, - User, - UserFilter, - UserSetting -}; -use App\Services\MediaPathService; -use App\Services\MediaBlocklistService; -use App\Jobs\StatusPipeline\NewStatusPipeline; -use Illuminate\Support\Str; -use App\Util\ActivityPub\Helpers; -use App\Services\AccountService; -use App\Services\StatusService; -use App\Services\WebfingerService; +use App\DirectMessage; +use App\Jobs\DirectPipeline\DirectDeletePipeline; +use App\Jobs\DirectPipeline\DirectDeliverPipeline; +use App\Jobs\StatusPipeline\StatusDelete; +use App\Media; use App\Models\Conversation; +use App\Notification; +use App\Profile; +use App\Services\AccountService; +use App\Services\MediaBlocklistService; +use App\Services\MediaPathService; +use App\Services\MediaService; +use App\Services\StatusService; +use App\Services\UserFilterService; +use App\Services\UserRoleService; +use App\Services\UserStorageService; +use App\Services\WebfingerService; +use App\Status; +use App\UserFilter; +use App\Util\ActivityPub\Helpers; +use App\Util\Lexer\Autolink; +use Illuminate\Http\Request; +use Illuminate\Support\Str; class DirectMessageController extends Controller { - public function __construct() - { - $this->middleware('auth'); - } - - public function browse(Request $request) - { - $this->validate($request, [ - 'a' => 'nullable|string|in:inbox,sent,filtered', - 'page' => 'nullable|integer|min:1|max:99' - ]); - - $profile = $request->user()->profile_id; - $action = $request->input('a', 'inbox'); - $page = $request->input('page'); - - if(config('database.default') == 'pgsql') { - if($action == 'inbox') { - $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') - ->whereToId($profile) - ->with(['author','status']) - ->whereIsHidden(false) - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->latest() - ->get() - ->unique('from_id') - ->take(8) - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - })->values(); - } - - if($action == 'sent') { - $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') - ->whereFromId($profile) - ->with(['author','status']) - ->orderBy('id', 'desc') - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->get() - ->unique('to_id') - ->take(8) - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - }); - } - - if($action == 'filtered') { - $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') - ->whereToId($profile) - ->with(['author','status']) - ->whereIsHidden(true) - ->orderBy('id', 'desc') - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->get() - ->unique('from_id') - ->take(8) - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - }); - } - } elseif(config('database.default') == 'mysql') { - if($action == 'inbox') { - $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') - ->whereToId($profile) - ->with(['author','status']) - ->whereIsHidden(false) - ->groupBy('from_id') - ->latest() - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->limit(8) - ->get() - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - }); - } - - if($action == 'sent') { - $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') - ->whereFromId($profile) - ->with(['author','status']) - ->groupBy('to_id') - ->orderBy('createdAt', 'desc') - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->limit(8) - ->get() - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - }); - } - - if($action == 'filtered') { - $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') - ->whereToId($profile) - ->with(['author','status']) - ->whereIsHidden(true) - ->groupBy('from_id') - ->orderBy('createdAt', 'desc') - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->limit(8) - ->get() - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - }); - } - } - - return response()->json($dms->all()); - } - - public function create(Request $request) - { - $this->validate($request, [ - 'to_id' => 'required', - 'message' => 'required|string|min:1|max:500', - 'type' => 'required|in:text,emoji' - ]); - - $profile = $request->user()->profile; - $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id')); - - abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403); - $msg = $request->input('message'); - - if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) { - if($recipient->follows($profile) == true) { - $hidden = false; - } else { - $hidden = true; - } - } else { - $hidden = false; - } - - $status = new Status; - $status->profile_id = $profile->id; - $status->caption = $msg; - $status->rendered = $msg; - $status->visibility = 'direct'; - $status->scope = 'direct'; - $status->in_reply_to_profile_id = $recipient->id; - $status->save(); - - $dm = new DirectMessage; - $dm->to_id = $recipient->id; - $dm->from_id = $profile->id; - $dm->status_id = $status->id; - $dm->is_hidden = $hidden; - $dm->type = $request->input('type'); - $dm->save(); - - Conversation::updateOrInsert( - [ - 'to_id' => $recipient->id, - 'from_id' => $profile->id - ], - [ - 'type' => $dm->type, - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => $hidden - ] - ); - - if(filter_var($msg, FILTER_VALIDATE_URL)) { - if(Helpers::validateUrl($msg)) { - $dm->type = 'link'; - $dm->meta = [ - 'domain' => parse_url($msg, PHP_URL_HOST), - 'local' => parse_url($msg, PHP_URL_HOST) == - parse_url(config('app.url'), PHP_URL_HOST) - ]; - $dm->save(); - } - } - - $nf = UserFilter::whereUserId($recipient->id) - ->whereFilterableId($profile->id) - ->whereFilterableType('App\Profile') - ->whereFilterType('dm.mute') - ->exists(); - - if($recipient->domain == null && $hidden == false && !$nf) { - $notification = new Notification(); - $notification->profile_id = $recipient->id; - $notification->actor_id = $profile->id; - $notification->action = 'dm'; - $notification->item_id = $dm->id; - $notification->item_type = "App\DirectMessage"; - $notification->save(); - } - - if($recipient->domain) { - $this->remoteDeliver($dm); - } - - $res = [ - 'id' => (string) $dm->id, - 'isAuthor' => $profile->id == $dm->from_id, - 'reportId' => (string) $dm->status_id, - 'hidden' => (bool) $dm->is_hidden, - 'type' => $dm->type, - 'text' => $dm->status->caption, - 'media' => null, - 'timeAgo' => $dm->created_at->diffForHumans(null,null,true), - 'seen' => $dm->read_at != null, - 'meta' => $dm->meta - ]; - - return response()->json($res); - } - - public function thread(Request $request) - { - $this->validate($request, [ - 'pid' => 'required' - ]); - $uid = $request->user()->profile_id; - $pid = $request->input('pid'); - $max_id = $request->input('max_id'); - $min_id = $request->input('min_id'); - - $r = Profile::findOrFail($pid); - - if($min_id) { - $res = DirectMessage::select('*') - ->where('id', '>', $min_id) - ->where(function($q) use($pid,$uid) { - return $q->where([['from_id',$pid],['to_id',$uid] - ])->orWhere([['from_id',$uid],['to_id',$pid]]); - }) - ->latest() - ->take(8) - ->get(); - } else if ($max_id) { - $res = DirectMessage::select('*') - ->where('id', '<', $max_id) - ->where(function($q) use($pid,$uid) { - return $q->where([['from_id',$pid],['to_id',$uid] - ])->orWhere([['from_id',$uid],['to_id',$pid]]); - }) - ->latest() - ->take(8) - ->get(); - } else { - $res = DirectMessage::where(function($q) use($pid,$uid) { - return $q->where([['from_id',$pid],['to_id',$uid] - ])->orWhere([['from_id',$uid],['to_id',$pid]]); - }) - ->latest() - ->take(8) - ->get(); - } - - $res = $res->filter(function($s) { - return $s && $s->status; - }) - ->map(function($s) use ($uid) { - return [ - 'id' => (string) $s->id, - 'hidden' => (bool) $s->is_hidden, - 'isAuthor' => $uid == $s->from_id, - 'type' => $s->type, - 'text' => $s->status->caption, - 'media' => $s->status->firstMedia() ? $s->status->firstMedia()->url() : null, - 'timeAgo' => $s->created_at->diffForHumans(null,null,true), - 'seen' => $s->read_at != null, - 'reportId' => (string) $s->status_id, - 'meta' => json_decode($s->meta,true) - ]; - }) - ->values(); - - $w = [ - 'id' => (string) $r->id, - 'name' => $r->name, - 'username' => $r->username, - 'avatar' => $r->avatarUrl(), - 'url' => $r->url(), - 'muted' => UserFilter::whereUserId($uid) - ->whereFilterableId($r->id) - ->whereFilterableType('App\Profile') - ->whereFilterType('dm.mute') - ->first() ? true : false, - 'isLocal' => (bool) !$r->domain, - 'domain' => $r->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => '', - 'messages' => $res - ]; - - return response()->json($w, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - - public function delete(Request $request) - { - $this->validate($request, [ - 'id' => 'required' - ]); - - $sid = $request->input('id'); - $pid = $request->user()->profile_id; - - $dm = DirectMessage::whereFromId($pid) - ->whereStatusId($sid) - ->firstOrFail(); - - $status = Status::whereProfileId($pid) - ->findOrFail($dm->status_id); - - $recipient = AccountService::get($dm->to_id); - - if(!$recipient) { - return response('', 422); - } - - if($recipient['local'] == false) { - $dmc = $dm; - $this->remoteDelete($dmc); - } - - if(Conversation::whereStatusId($sid)->count()) { - $latest = DirectMessage::where(['from_id' => $dm->from_id, 'to_id' => $dm->to_id]) - ->orWhere(['to_id' => $dm->from_id, 'from_id' => $dm->to_id]) - ->latest() - ->first(); - - if($latest->status_id == $sid) { - Conversation::where(['to_id' => $dm->from_id, 'from_id' => $dm->to_id]) - ->update([ - 'updated_at' => $latest->updated_at, - 'status_id' => $latest->status_id, - 'type' => $latest->type, - 'is_hidden' => false - ]); - - Conversation::where(['to_id' => $dm->to_id, 'from_id' => $dm->from_id]) - ->update([ - 'updated_at' => $latest->updated_at, - 'status_id' => $latest->status_id, - 'type' => $latest->type, - 'is_hidden' => false - ]); - } else { - Conversation::where([ - 'status_id' => $sid, - 'to_id' => $dm->from_id, - 'from_id' => $dm->to_id - ])->delete(); - - Conversation::where([ - 'status_id' => $sid, - 'from_id' => $dm->from_id, - 'to_id' => $dm->to_id - ])->delete(); - } - } - - StatusService::del($status->id, true); - - $status->delete(); - $dm->delete(); - - return [200]; - } - - public function get(Request $request, $id) - { - $pid = $request->user()->profile_id; - $dm = DirectMessage::whereStatusId($id)->firstOrFail(); - abort_if($pid !== $dm->to_id && $pid !== $dm->from_id, 404); - return response()->json($dm, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - - public function mediaUpload(Request $request) - { - $this->validate($request, [ - 'file' => function() { - return [ - 'required', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ]; - }, - 'to_id' => 'required' - ]); - - $user = $request->user(); - $profile = $user->profile; - $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id')); - abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403); - - if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) { - if($recipient->follows($profile) == true) { - $hidden = false; - } else { - $hidden = true; - } - } else { - $hidden = false; - } - - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { - return Media::whereUserId($user->id)->sum('size') / 1000; - }); - $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { - abort(403, 'Account size limit reached.'); - } - } - $photo = $request->file('file'); - - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), $mimes) == false) { - abort(403, 'Invalid or unsupported mime type.'); - } - - $storagePath = MediaPathService::get($user, 2) . Str::random(8); - $path = $photo->storePublicly($storagePath); - $hash = \hash_file('sha256', $photo); - - abort_if(MediaBlocklistService::exists($hash) == true, 451); - - $status = new Status; - $status->profile_id = $profile->id; - $status->caption = null; - $status->rendered = null; - $status->visibility = 'direct'; - $status->scope = 'direct'; - $status->in_reply_to_profile_id = $recipient->id; - $status->save(); - - $media = new Media(); - $media->status_id = $status->id; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $photo->getSize(); - $media->mime = $photo->getMimeType(); - $media->caption = null; - $media->filter_class = null; - $media->filter_name = null; - $media->save(); - - $dm = new DirectMessage; - $dm->to_id = $recipient->id; - $dm->from_id = $profile->id; - $dm->status_id = $status->id; - $dm->type = array_first(explode('/', $media->mime)) == 'video' ? 'video' : 'photo'; - $dm->is_hidden = $hidden; - $dm->save(); - - Conversation::updateOrInsert( - [ - 'to_id' => $recipient->id, - 'from_id' => $profile->id - ], - [ - 'type' => $dm->type, - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => $hidden - ] - ); - - if($recipient->domain) { - $this->remoteDeliver($dm); - } - - return [ - 'id' => $dm->id, - 'reportId' => (string) $dm->status_id, - 'type' => $dm->type, - 'url' => $media->url() - ]; - } - - public function composeLookup(Request $request) - { - $this->validate($request, [ - 'q' => 'required|string|min:2|max:50', - 'remote' => 'nullable', - ]); - - $q = $request->input('q'); - $r = $request->input('remote', false); - - if($r && !Str::of($q)->contains('.')) { - return []; - } - - if($r && Helpers::validateUrl($q)) { - Helpers::profileFetch($q); - } - - if(Str::of($q)->startsWith('@')) { - if(strlen($q) < 3) { - return []; - } - if(substr_count($q, '@') == 2) { - WebfingerService::lookup($q); - } - $q = mb_substr($q, 1); - } - - $blocked = UserFilter::whereFilterableType('App\Profile') - ->whereFilterType('block') - ->whereFilterableId($request->user()->profile_id) - ->pluck('user_id'); - - $blocked->push($request->user()->profile_id); - - $results = Profile::select('id','domain','username') - ->whereNotIn('id', $blocked) - ->where('username','like','%'.$q.'%') - ->orderBy('domain') - ->limit(8) - ->get() - ->map(function($r) { - $acct = AccountService::get($r->id); - return [ - 'local' => (bool) !$r->domain, - 'id' => (string) $r->id, - 'name' => $r->username, - 'privacy' => true, - 'avatar' => $r->avatarUrl(), - 'account' => $acct - ]; - }); - - return $results; - } - - public function read(Request $request) - { - $this->validate($request, [ - 'pid' => 'required', - 'sid' => 'required' - ]); - - $pid = $request->input('pid'); - $sid = $request->input('sid'); - - $dms = DirectMessage::whereToId($request->user()->profile_id) - ->whereFromId($pid) - ->where('status_id', '>=', $sid) - ->get(); - - $now = now(); - foreach($dms as $dm) { - $dm->read_at = $now; - $dm->save(); - } - - return response()->json($dms->pluck('id')); - } - - public function mute(Request $request) - { - $this->validate($request, [ - 'id' => 'required' - ]); - - $fid = $request->input('id'); - $pid = $request->user()->profile_id; - - UserFilter::firstOrCreate( - [ - 'user_id' => $pid, - 'filterable_id' => $fid, - 'filterable_type' => 'App\Profile', - 'filter_type' => 'dm.mute' - ] - ); - - return [200]; - } - - public function unmute(Request $request) - { - $this->validate($request, [ - 'id' => 'required' - ]); - - $fid = $request->input('id'); - $pid = $request->user()->profile_id; - - $f = UserFilter::whereUserId($pid) - ->whereFilterableId($fid) - ->whereFilterableType('App\Profile') - ->whereFilterType('dm.mute') - ->firstOrFail(); - - $f->delete(); - - return [200]; - } - - public function remoteDeliver($dm) - { - $profile = $dm->author; - $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; - - $tags = [ - [ - 'type' => 'Mention', - 'href' => $dm->recipient->permalink(), - 'name' => $dm->recipient->emailUrl(), - ] - ]; - - $body = [ - '@context' => [ - 'https://w3id.org/security/v1', - 'https://www.w3.org/ns/activitystreams', - ], - 'id' => $dm->status->permalink(), - 'type' => 'Create', - 'actor' => $dm->status->profile->permalink(), - 'published' => $dm->status->created_at->toAtomString(), - 'to' => [$dm->recipient->permalink()], - 'cc' => [], - 'object' => [ - 'id' => $dm->status->url(), - 'type' => 'Note', - 'summary' => null, - 'content' => $dm->status->rendered ?? $dm->status->caption, - 'inReplyTo' => null, - 'published' => $dm->status->created_at->toAtomString(), - 'url' => $dm->status->url(), - 'attributedTo' => $dm->status->profile->permalink(), - 'to' => [$dm->recipient->permalink()], - 'cc' => [], - 'sensitive' => (bool) $dm->status->is_nsfw, - 'attachment' => $dm->status->media()->orderBy('order')->get()->map(function ($media) { - return [ - 'type' => $media->activityVerb(), - 'mediaType' => $media->mime, - 'url' => $media->url(), - 'name' => $media->caption, - ]; - })->toArray(), - 'tag' => $tags, - ] - ]; - - Helpers::sendSignedObject($profile, $url, $body); - } - - public function remoteDelete($dm) - { - $profile = $dm->author; - $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; - - $body = [ - '@context' => [ - 'https://www.w3.org/ns/activitystreams', - ], - 'id' => $dm->status->permalink('#delete'), - 'to' => [ - 'https://www.w3.org/ns/activitystreams#Public' - ], - 'type' => 'Delete', - 'actor' => $dm->status->profile->permalink(), - 'object' => [ - 'id' => $dm->status->url(), - 'type' => 'Tombstone' - ] - ]; - - Helpers::sendSignedObject($profile, $url, $body); - } + public function __construct() + { + $this->middleware('auth'); + } + + public function browse(Request $request) + { + $this->validate($request, [ + 'a' => 'nullable|string|in:inbox,sent,filtered', + 'page' => 'nullable|integer|min:1|max:99', + ]); + + $user = $request->user(); + if ($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id)) { + return []; + } + $profile = $user->profile_id; + $action = $request->input('a', 'inbox'); + $page = $request->input('page'); + + if (config('database.default') == 'pgsql') { + if ($action == 'inbox') { + $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') + ->whereToId($profile) + ->with(['author', 'status']) + ->whereIsHidden(false) + ->when($page, function ($q, $page) { + if ($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->latest() + ->get() + ->unique('from_id') + ->take(8) + ->map(function ($r) use ($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) ! $r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) ! $r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ]; + })->values(); + } + + if ($action == 'sent') { + $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') + ->whereFromId($profile) + ->with(['author', 'status']) + ->orderBy('id', 'desc') + ->when($page, function ($q, $page) { + if ($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->get() + ->unique('to_id') + ->take(8) + ->map(function ($r) use ($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) ! $r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) ! $r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ]; + }); + } + + if ($action == 'filtered') { + $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') + ->whereToId($profile) + ->with(['author', 'status']) + ->whereIsHidden(true) + ->orderBy('id', 'desc') + ->when($page, function ($q, $page) { + if ($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->get() + ->unique('from_id') + ->take(8) + ->map(function ($r) use ($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) ! $r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) ! $r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ]; + }); + } + } elseif (config('database.default') == 'mysql') { + if ($action == 'inbox') { + $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') + ->whereToId($profile) + ->with(['author', 'status']) + ->whereIsHidden(false) + ->groupBy('from_id') + ->latest() + ->when($page, function ($q, $page) { + if ($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->limit(8) + ->get() + ->map(function ($r) use ($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) ! $r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) ! $r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ]; + }); + } + + if ($action == 'sent') { + $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') + ->whereFromId($profile) + ->with(['author', 'status']) + ->groupBy('to_id') + ->orderBy('createdAt', 'desc') + ->when($page, function ($q, $page) { + if ($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->limit(8) + ->get() + ->map(function ($r) use ($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) ! $r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) ! $r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ]; + }); + } + + if ($action == 'filtered') { + $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') + ->whereToId($profile) + ->with(['author', 'status']) + ->whereIsHidden(true) + ->groupBy('from_id') + ->orderBy('createdAt', 'desc') + ->when($page, function ($q, $page) { + if ($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->limit(8) + ->get() + ->map(function ($r) use ($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) ! $r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) ! $r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [], + ]; + }); + } + } + + return response()->json($dms->all()); + } + + public function create(Request $request) + { + $this->validate($request, [ + 'to_id' => 'required', + 'message' => 'required|string|min:1|max:500', + 'type' => 'required|in:text,emoji', + ]); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + if (! $user->is_admin) { + abort_if($user->created_at->gt(now()->subHours(72)), 400, 'You need to wait a bit before you can DM another account'); + } + $profile = $user->profile; + $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id')); + + abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403); + $msg = $request->input('message'); + + if ((! $recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) { + if ($recipient->follows($profile) == true) { + $hidden = false; + } else { + $hidden = true; + } + } else { + $hidden = false; + } + + $status = new Status; + $status->profile_id = $profile->id; + $status->caption = $msg; + $status->visibility = 'direct'; + $status->scope = 'direct'; + $status->in_reply_to_profile_id = $recipient->id; + $status->save(); + + $dm = new DirectMessage; + $dm->to_id = $recipient->id; + $dm->from_id = $profile->id; + $dm->status_id = $status->id; + $dm->is_hidden = $hidden; + $dm->type = $request->input('type'); + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $recipient->id, + 'from_id' => $profile->id, + ], + [ + 'type' => $dm->type, + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => $hidden, + ] + ); + + if (filter_var($msg, FILTER_VALIDATE_URL)) { + if (Helpers::validateUrl($msg)) { + $dm->type = 'link'; + $dm->meta = [ + 'domain' => parse_url($msg, PHP_URL_HOST), + 'local' => parse_url($msg, PHP_URL_HOST) == + parse_url(config('app.url'), PHP_URL_HOST), + ]; + $dm->save(); + } + } + + $nf = UserFilter::whereUserId($recipient->id) + ->whereFilterableId($profile->id) + ->whereFilterableType('App\Profile') + ->whereFilterType('dm.mute') + ->exists(); + + if ($recipient->domain == null && $hidden == false && ! $nf) { + $notification = new Notification; + $notification->profile_id = $recipient->id; + $notification->actor_id = $profile->id; + $notification->action = 'dm'; + $notification->item_id = $dm->id; + $notification->item_type = "App\DirectMessage"; + $notification->save(); + } + + if ($recipient->domain) { + $this->remoteDeliver($dm); + } + + $res = [ + 'id' => (string) $dm->id, + 'isAuthor' => $profile->id == $dm->from_id, + 'reportId' => (string) $dm->status_id, + 'hidden' => (bool) $dm->is_hidden, + 'type' => $dm->type, + 'text' => $dm->status->caption, + 'media' => null, + 'timeAgo' => $dm->created_at->diffForHumans(null, null, true), + 'seen' => $dm->read_at != null, + 'meta' => $dm->meta, + ]; + + return response()->json($res); + } + + public function thread(Request $request) + { + $this->validate($request, [ + 'pid' => 'required', + 'max_id' => 'sometimes|integer', + 'min_id' => 'sometimes|integer', + ]); + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + + $uid = $user->profile_id; + $pid = $request->input('pid'); + $max_id = $request->input('max_id'); + $min_id = $request->input('min_id'); + + $r = Profile::findOrFail($pid); + + if ($min_id) { + $res = DirectMessage::select('*') + ->where('id', '>', $min_id) + ->where(function ($query) use ($pid, $uid) { + $query->where('from_id', $pid)->where('to_id', $uid); + })->orWhere(function ($query) use ($pid, $uid) { + $query->where('from_id', $uid)->where('to_id', $pid); + }) + ->orderBy('id', 'asc') + ->take(8) + ->get() + ->reverse(); + } elseif ($max_id) { + $res = DirectMessage::select('*') + ->where('id', '<', $max_id) + ->where(function ($query) use ($pid, $uid) { + $query->where('from_id', $pid)->where('to_id', $uid); + })->orWhere(function ($query) use ($pid, $uid) { + $query->where('from_id', $uid)->where('to_id', $pid); + }) + ->orderBy('id', 'desc') + ->take(8) + ->get(); + } else { + $res = DirectMessage::where(function ($query) use ($pid, $uid) { + $query->where('from_id', $pid)->where('to_id', $uid); + })->orWhere(function ($query) use ($pid, $uid) { + $query->where('from_id', $uid)->where('to_id', $pid); + }) + ->orderBy('id', 'desc') + ->take(8) + ->get(); + } + + $res = $res->filter(function ($s) { + return $s && $s->status; + }) + ->map(function ($s) use ($uid) { + return [ + 'id' => (string) $s->id, + 'hidden' => (bool) $s->is_hidden, + 'isAuthor' => $uid == $s->from_id, + 'type' => $s->type, + 'text' => $s->status->caption, + 'media' => $s->status->firstMedia() ? $s->status->firstMedia()->url() : null, + 'carousel' => MediaService::get($s->status_id), + 'created_at' => $s->created_at->format('c'), + 'timeAgo' => $s->created_at->diffForHumans(null, null, true), + 'seen' => $s->read_at != null, + 'reportId' => (string) $s->status_id, + 'meta' => json_decode($s->meta, true), + ]; + }) + ->values(); + + $filters = UserFilterService::mutes($uid); + + $w = [ + 'id' => (string) $r->id, + 'name' => $r->name, + 'username' => $r->username, + 'avatar' => $r->avatarUrl(), + 'url' => $r->url(), + 'muted' => in_array($r->id, $filters), + 'isLocal' => (bool) ! $r->domain, + 'domain' => $r->domain, + 'created_at' => $r->created_at->format('c'), + 'updated_at' => $r->updated_at->format('c'), + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => '', + 'messages' => $res, + ]; + + return response()->json($w, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + public function delete(Request $request) + { + $this->validate($request, [ + 'id' => 'required', + ]); + + $sid = $request->input('id'); + $pid = $request->user()->profile_id; + + $dm = DirectMessage::whereFromId($pid) + ->whereStatusId($sid) + ->firstOrFail(); + + $status = Status::whereProfileId($pid) + ->findOrFail($dm->status_id); + + $recipient = AccountService::get($dm->to_id); + + if (! $recipient) { + return response('', 422); + } + + if ($recipient['local'] == false) { + $dmc = $dm; + $this->remoteDelete($dmc); + } else { + StatusDelete::dispatch($status)->onQueue('high'); + } + + if (Conversation::whereStatusId($sid)->count()) { + $latest = DirectMessage::where(['from_id' => $dm->from_id, 'to_id' => $dm->to_id]) + ->orWhere(['to_id' => $dm->from_id, 'from_id' => $dm->to_id]) + ->latest() + ->first(); + + if ($latest->status_id == $sid) { + Conversation::where(['to_id' => $dm->from_id, 'from_id' => $dm->to_id]) + ->update([ + 'updated_at' => $latest->updated_at, + 'status_id' => $latest->status_id, + 'type' => $latest->type, + 'is_hidden' => false, + ]); + + Conversation::where(['to_id' => $dm->to_id, 'from_id' => $dm->from_id]) + ->update([ + 'updated_at' => $latest->updated_at, + 'status_id' => $latest->status_id, + 'type' => $latest->type, + 'is_hidden' => false, + ]); + } else { + Conversation::where([ + 'status_id' => $sid, + 'to_id' => $dm->from_id, + 'from_id' => $dm->to_id, + ])->delete(); + + Conversation::where([ + 'status_id' => $sid, + 'from_id' => $dm->from_id, + 'to_id' => $dm->to_id, + ])->delete(); + } + } + + StatusService::del($status->id, true); + + $status->forceDeleteQuietly(); + + return [200]; + } + + public function get(Request $request, $id) + { + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + + $pid = $request->user()->profile_id; + $dm = DirectMessage::whereStatusId($id)->firstOrFail(); + abort_if($pid !== $dm->to_id && $pid !== $dm->from_id, 404); + + return response()->json($dm, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + public function mediaUpload(Request $request) + { + $this->validate($request, [ + 'file' => function () { + return [ + 'required', + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), + ]; + }, + 'to_id' => 'required', + ]); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + $profile = $user->profile; + $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id')); + abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403); + + if ((! $recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) { + if ($recipient->follows($profile) == true) { + $hidden = false; + } else { + $hidden = true; + } + } else { + $hidden = false; + } + + $accountSize = UserStorageService::get($user->id); + abort_if($accountSize === -1, 403, 'Invalid request.'); + $photo = $request->file('file'); + $fileSize = $photo->getSize(); + $sizeInKbs = (int) ceil($fileSize / 1000); + $updatedAccountSize = (int) $accountSize + (int) $sizeInKbs; + + if ((bool) config_cache('pixelfed.enforce_account_limit') == true) { + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($updatedAccountSize >= $limit) { + abort(403, 'Account size limit reached.'); + } + } + + $mimes = explode(',', config_cache('pixelfed.media_types')); + if (in_array($photo->getMimeType(), $mimes) == false) { + abort(403, 'Invalid or unsupported mime type.'); + } + + $storagePath = MediaPathService::get($user, 2).Str::random(8); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + + abort_if(MediaBlocklistService::exists($hash) == true, 451); + + $status = new Status; + $status->profile_id = $profile->id; + $status->caption = null; + $status->visibility = 'direct'; + $status->scope = 'direct'; + $status->in_reply_to_profile_id = $recipient->id; + $status->save(); + + $media = new Media; + $media->status_id = $status->id; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->mime = $photo->getMimeType(); + $media->caption = null; + $media->filter_class = null; + $media->filter_name = null; + $media->save(); + + $dm = new DirectMessage; + $dm->to_id = $recipient->id; + $dm->from_id = $profile->id; + $dm->status_id = $status->id; + $dm->type = array_first(explode('/', $media->mime)) == 'video' ? 'video' : 'photo'; + $dm->is_hidden = $hidden; + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $recipient->id, + 'from_id' => $profile->id, + ], + [ + 'type' => $dm->type, + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => $hidden, + ] + ); + + $user->storage_used = (int) $updatedAccountSize; + $user->storage_used_updated_at = now(); + $user->save(); + + if ($recipient->domain) { + $this->remoteDeliver($dm); + } + + return [ + 'id' => $dm->id, + 'reportId' => (string) $dm->status_id, + 'type' => $dm->type, + 'url' => $media->url(), + ]; + } + + public function composeLookup(Request $request) + { + $this->validate($request, [ + 'q' => 'required|string|min:2|max:50', + 'remote' => 'nullable', + ]); + + $user = $request->user(); + if ($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id)) { + return []; + } + + $q = $request->input('q'); + $r = $request->input('remote', false); + + if ($r && ! Str::of($q)->contains('.')) { + return []; + } + + if ($r && Helpers::validateUrl($q)) { + Helpers::profileFetch($q); + } + + if (Str::of($q)->startsWith('@')) { + if (strlen($q) < 3) { + return []; + } + if (substr_count($q, '@') == 2) { + WebfingerService::lookup($q); + } + $q = mb_substr($q, 1); + } + + $blocked = UserFilter::whereFilterableType('App\Profile') + ->whereFilterType('block') + ->whereFilterableId($request->user()->profile_id) + ->pluck('user_id'); + + $blocked->push($request->user()->profile_id); + + $results = Profile::select('id', 'domain', 'username') + ->whereNotIn('id', $blocked) + ->where('username', 'like', '%'.$q.'%') + ->orderBy('domain') + ->limit(8) + ->get() + ->map(function ($r) { + $acct = AccountService::get($r->id); + + return [ + 'local' => (bool) ! $r->domain, + 'id' => (string) $r->id, + 'name' => $r->username, + 'privacy' => true, + 'avatar' => $r->avatarUrl(), + 'account' => $acct, + ]; + }); + + return $results; + } + + public function read(Request $request) + { + $this->validate($request, [ + 'pid' => 'required', + 'sid' => 'required', + ]); + + $pid = $request->input('pid'); + $sid = $request->input('sid'); + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + + $dms = DirectMessage::whereToId($request->user()->profile_id) + ->whereFromId($pid) + ->where('status_id', '>=', $sid) + ->get(); + + $now = now(); + foreach ($dms as $dm) { + $dm->read_at = $now; + $dm->save(); + } + + return response()->json($dms->pluck('id')); + } + + public function mute(Request $request) + { + $this->validate($request, [ + 'id' => 'required', + ]); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + $fid = $request->input('id'); + $pid = $request->user()->profile_id; + + UserFilter::firstOrCreate( + [ + 'user_id' => $pid, + 'filterable_id' => $fid, + 'filterable_type' => 'App\Profile', + 'filter_type' => 'dm.mute', + ] + ); + + return [200]; + } + + public function unmute(Request $request) + { + $this->validate($request, [ + 'id' => 'required', + ]); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + + $fid = $request->input('id'); + $pid = $request->user()->profile_id; + + $f = UserFilter::whereUserId($pid) + ->whereFilterableId($fid) + ->whereFilterableType('App\Profile') + ->whereFilterType('dm.mute') + ->firstOrFail(); + + $f->delete(); + + return [200]; + } + + public function remoteDeliver($dm) + { + $profile = $dm->author; + $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; + $status = $dm->status; + + if (! $status) { + return; + } + + $tags = [ + [ + 'type' => 'Mention', + 'href' => $dm->recipient->permalink(), + 'name' => $dm->recipient->emailUrl(), + ], + ]; + + $content = $status->caption ? Autolink::create()->autolink($status->caption) : null; + + $body = [ + '@context' => [ + 'https://w3id.org/security/v1', + 'https://www.w3.org/ns/activitystreams', + ], + 'id' => $dm->status->permalink(), + 'type' => 'Create', + 'actor' => $dm->status->profile->permalink(), + 'published' => $dm->status->created_at->toAtomString(), + 'to' => [$dm->recipient->permalink()], + 'cc' => [], + 'object' => [ + 'id' => $dm->status->url(), + 'type' => 'Note', + 'summary' => null, + 'content' => $content, + 'inReplyTo' => null, + 'published' => $dm->status->created_at->toAtomString(), + 'url' => $dm->status->url(), + 'attributedTo' => $dm->status->profile->permalink(), + 'to' => [$dm->recipient->permalink()], + 'cc' => [], + 'sensitive' => (bool) $dm->status->is_nsfw, + 'attachment' => $dm->status->media()->orderBy('order')->get()->map(function ($media) { + return [ + 'type' => $media->activityVerb(), + 'mediaType' => $media->mime, + 'url' => $media->url(), + 'name' => $media->caption, + ]; + })->toArray(), + 'tag' => $tags, + ], + ]; + + DirectDeliverPipeline::dispatch($profile, $url, $body)->onQueue('high'); + } + + public function remoteDelete($dm) + { + $profile = $dm->author; + $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; + + $body = [ + '@context' => [ + 'https://www.w3.org/ns/activitystreams', + ], + 'id' => $dm->status->permalink('#delete'), + 'to' => [ + 'https://www.w3.org/ns/activitystreams#Public', + ], + 'type' => 'Delete', + 'actor' => $dm->status->profile->permalink(), + 'object' => [ + 'id' => $dm->status->url(), + 'type' => 'Tombstone', + ], + ]; + DirectDeletePipeline::dispatch($profile, $url, $body)->onQueue('high'); + } } diff --git a/app/Http/Controllers/DiscoverController.php b/app/Http/Controllers/DiscoverController.php index 4bb7277a4..b3047ff79 100644 --- a/app/Http/Controllers/DiscoverController.php +++ b/app/Http/Controllers/DiscoverController.php @@ -2,366 +2,430 @@ namespace App\Http\Controllers; -use App\{ - DiscoverCategory, - Follower, - Hashtag, - HashtagFollow, - Instance, - Like, - Profile, - Status, - StatusHashtag, - UserFilter -}; -use Auth, DB, Cache; -use Illuminate\Http\Request; +use App\Hashtag; +use App\Instance; +use App\Like; +use App\Services\AccountService; +use App\Services\AdminShadowFilterService; use App\Services\BookmarkService; use App\Services\ConfigCacheService; +use App\Services\FollowerService; use App\Services\HashtagService; +use App\Services\Internal\BeagleService; use App\Services\LikeService; use App\Services\ReblogService; -use App\Services\StatusHashtagService; use App\Services\SnowflakeService; +use App\Services\StatusHashtagService; use App\Services\StatusService; use App\Services\TrendingHashtagService; use App\Services\UserFilterService; +use App\Status; +use Auth; +use Cache; +use DB; +use Illuminate\Http\Request; class DiscoverController extends Controller { - public function home(Request $request) - { - abort_if(!Auth::check() && config('instance.discover.public') == false, 403); - return view('discover.home'); - } + public function home(Request $request) + { + abort_if(! Auth::check() && config('instance.discover.public') == false, 403); - public function showTags(Request $request, $hashtag) - { - abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403); + return view('discover.home'); + } - $tag = Hashtag::whereName($hashtag) - ->orWhere('slug', $hashtag) - ->where('is_banned', '!=', true) - ->firstOrFail(); - $tagCount = StatusHashtagService::count($tag->id); - return view('discover.tags.show', compact('tag', 'tagCount')); - } + public function showTags(Request $request, $hashtag) + { + if ($request->user()) { + return redirect('/i/web/hashtag/'.$hashtag.'?src=pd'); + } + abort_if(! config('instance.discover.tags.is_public') && ! Auth::check(), 403); - public function getHashtags(Request $request) - { - $user = $request->user(); - abort_if(!config('instance.discover.tags.is_public') && !$user, 403); + $tag = Hashtag::whereName($hashtag) + ->orWhere('slug', $hashtag) + ->where('is_banned', '!=', true) + ->firstOrFail(); + $tagCount = $tag->cached_count ?? 0; - $this->validate($request, [ - 'hashtag' => 'required|string|min:1|max:124', - 'page' => 'nullable|integer|min:1|max:' . ($user ? 29 : 3) - ]); + return view('discover.tags.show', compact('tag', 'tagCount')); + } - $page = $request->input('page') ?? '1'; - $end = $page > 1 ? $page * 9 : 0; - $tag = $request->input('hashtag'); + public function getHashtags(Request $request) + { + $user = $request->user(); + abort_if(! config('instance.discover.tags.is_public') && ! $user, 403); - if(config('database.default') === 'pgsql') { - $hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail(); - } else { - $hashtag = Hashtag::whereName($tag)->firstOrFail(); - } + $this->validate($request, [ + 'hashtag' => 'required|string|min:1|max:124', + 'page' => 'nullable|integer|min:1|max:'.($user ? 29 : 3), + ]); - if($hashtag->is_banned == true) { - return []; - } - if($user) { - $res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id); - } - $res['hashtag'] = [ - 'name' => $hashtag->name, - 'url' => $hashtag->url() - ]; - if($user) { - $tags = StatusHashtagService::get($hashtag->id, $page, $end); - $res['tags'] = collect($tags) - ->map(function($tag) use($user) { - $tag['status']['favourited'] = (bool) LikeService::liked($user->profile_id, $tag['status']['id']); - $tag['status']['reblogged'] = (bool) ReblogService::get($user->profile_id, $tag['status']['id']); - $tag['status']['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $tag['status']['id']); - return $tag; - }) - ->filter(function($tag) { - if(!StatusService::get($tag['status']['id'])) { - return false; - } - return true; - }) - ->values(); - } else { - if($page != 1) { - $res['tags'] = []; - return $res; - } - $key = 'discover:tags:public_feed:' . $hashtag->id . ':page:' . $page; - $tags = Cache::remember($key, 43200, function() use($hashtag, $page, $end) { - return collect(StatusHashtagService::get($hashtag->id, $page, $end)) - ->filter(function($tag) { - if(!$tag['status']['local']) { - return false; - } - return true; - }) - ->values(); - }); - $res['tags'] = collect($tags) - ->filter(function($tag) { - if(!StatusService::get($tag['status']['id'])) { - return false; - } - return true; - }) - ->values(); - } - return $res; - } + $page = $request->input('page') ?? '1'; + $end = $page > 1 ? $page * 9 : 0; + $tag = $request->input('hashtag'); - public function profilesDirectory(Request $request) - { - return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.'); - } + if (config('database.default') === 'pgsql') { + $hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail(); + } else { + $hashtag = Hashtag::whereName($tag)->firstOrFail(); + } - public function profilesDirectoryApi(Request $request) - { - return ['error' => 'Temporarily unavailable.']; - } + if ($hashtag->is_banned == true) { + return []; + } + if ($user) { + $res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id); + } + $res['hashtag'] = [ + 'name' => $hashtag->name, + 'url' => $hashtag->url(), + ]; + if ($user) { + $tags = StatusHashtagService::get($hashtag->id, $page, $end); + $res['tags'] = collect($tags) + ->map(function ($tag) use ($user) { + $tag['status']['favourited'] = (bool) LikeService::liked($user->profile_id, $tag['status']['id']); + $tag['status']['reblogged'] = (bool) ReblogService::get($user->profile_id, $tag['status']['id']); + $tag['status']['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $tag['status']['id']); - public function trendingApi(Request $request) - { - abort_if(config('instance.discover.public') == false && !$request->user(), 403); + return $tag; + }) + ->filter(function ($tag) { + if (! StatusService::get($tag['status']['id'])) { + return false; + } - $this->validate($request, [ - 'range' => 'nullable|string|in:daily,monthly,yearly', - ]); + return true; + }) + ->values(); + } else { + if ($page != 1) { + $res['tags'] = []; - $range = $request->input('range'); - $days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365); - $ttls = [ - 1 => 1500, - 31 => 14400, - 365 => 86400 - ]; - $key = ':api:discover:trending:v2.12:range:' . $days; + return $res; + } + $key = 'discover:tags:public_feed:'.$hashtag->id.':page:'.$page; + $tags = Cache::remember($key, 43200, function () use ($hashtag, $page, $end) { + return collect(StatusHashtagService::get($hashtag->id, $page, $end)) + ->filter(function ($tag) { + if (! $tag['status']['local']) { + return false; + } - $ids = Cache::remember($key, $ttls[$days], function() use($days) { - $min_id = SnowflakeService::byDate(now()->subDays($days)); - return DB::table('statuses') - ->select( - 'id', - 'scope', - 'type', - 'is_nsfw', - 'likes_count', - 'created_at' - ) - ->where('id', '>', $min_id) - ->whereNull('uri') - ->whereScope('public') - ->whereIn('type', [ - 'photo', - 'photo:album', - 'video' - ]) - ->whereIsNsfw(false) - ->orderBy('likes_count','desc') - ->take(30) - ->pluck('id'); - }); + return true; + }) + ->values(); + }); + $res['tags'] = collect($tags) + ->filter(function ($tag) { + if (! StatusService::get($tag['status']['id'])) { + return false; + } - $filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : []; + return true; + }) + ->values(); + } - $res = $ids->map(function($s) { - return StatusService::get($s); - })->filter(function($s) use($filtered) { - return - $s && - !in_array($s['account']['id'], $filtered) && - isset($s['account']); - })->values(); + return $res; + } - return response()->json($res); - } + public function profilesDirectory(Request $request) + { + return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.'); + } - public function trendingHashtags(Request $request) - { - abort_if(!$request->user(), 403); + public function profilesDirectoryApi(Request $request) + { + return ['error' => 'Temporarily unavailable.']; + } - $res = TrendingHashtagService::getTrending(); - return $res; - } + public function trendingApi(Request $request) + { + abort_if(config('instance.discover.public') == false && ! $request->user(), 403); - public function trendingPlaces(Request $request) - { - return []; - } + $this->validate($request, [ + 'range' => 'nullable|string|in:daily,monthly,yearly', + ]); - public function myMemories(Request $request) - { - abort_if(!$request->user(), 404); - $pid = $request->user()->profile_id; - abort_if(!$this->config()['memories']['enabled'], 404); - $type = $request->input('type') ?? 'posts'; + $range = $request->input('range'); + $days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365); + $ttls = [ + 1 => 1500, + 31 => 14400, + 365 => 86400, + ]; + $key = ':api:discover:trending:v2.12:range:'.$days; - switch($type) { - case 'posts': - $res = Status::whereProfileId($pid) - ->whereDay('created_at', date('d')) - ->whereMonth('created_at', date('m')) - ->whereYear('created_at', '!=', date('Y')) - ->whereNull(['reblog_of_id', 'in_reply_to_id']) - ->limit(20) - ->pluck('id') - ->map(function($id) { - return StatusService::get($id, false); - }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); - break; + $ids = Cache::remember($key, $ttls[$days], function () use ($days) { + $min_id = SnowflakeService::byDate(now()->subDays($days)); - case 'liked': - $res = Like::whereProfileId($pid) - ->whereDay('created_at', date('d')) - ->whereMonth('created_at', date('m')) - ->whereYear('created_at', '!=', date('Y')) - ->orderByDesc('status_id') - ->limit(20) - ->pluck('status_id') - ->map(function($id) { - $status = StatusService::get($id, false); - $status['favourited'] = true; - return $status; - }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); - break; - } + return DB::table('statuses') + ->select( + 'id', + 'scope', + 'type', + 'is_nsfw', + 'likes_count', + 'created_at' + ) + ->where('id', '>', $min_id) + ->whereNull('uri') + ->whereScope('public') + ->whereIn('type', [ + 'photo', + 'photo:album', + 'video', + ]) + ->whereIsNsfw(false) + ->orderBy('likes_count', 'desc') + ->take(30) + ->pluck('id'); + }); - return $res; - } + $filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : []; - public function accountInsightsPopularPosts(Request $request) - { - abort_if(!$request->user(), 404); - $pid = $request->user()->profile_id; - abort_if(!$this->config()['insights']['enabled'], 404); - $posts = Cache::remember('pf:discover:metro2:accinsights:popular:' . $pid, 43200, function() use ($pid) { - return Status::whereProfileId($pid) - ->whereNotNull('likes_count') - ->orderByDesc('likes_count') - ->limit(12) - ->pluck('id') - ->map(function($id) { - return StatusService::get($id, false); - }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); - }); + $res = $ids->map(function ($s) { + return StatusService::get($s); + })->filter(function ($s) use ($filtered) { + return + $s && + ! in_array($s['account']['id'], $filtered) && + isset($s['account']); + })->values(); - return $posts; - } + return response()->json($res); + } - public function config() - { - $cc = ConfigCacheService::get('config.discover.features'); - if($cc) { - return is_string($cc) ? json_decode($cc, true) : $cc; - } - return [ - 'hashtags' => [ - 'enabled' => false, - ], - 'memories' => [ - 'enabled' => false, - ], - 'insights' => [ - 'enabled' => false, - ], - 'friends' => [ - 'enabled' => false, - ], - 'server' => [ - 'enabled' => false, - 'mode' => 'allowlist', - 'domains' => [] - ] - ]; - } + public function trendingHashtags(Request $request) + { + abort_if(! $request->user(), 403); - public function serverTimeline(Request $request) - { - abort_if(!$request->user(), 404); - abort_if(!$this->config()['server']['enabled'], 404); - $pid = $request->user()->profile_id; - $domain = $request->input('domain'); - $config = $this->config(); - $domains = explode(',', $config['server']['domains']); - abort_unless(in_array($domain, $domains), 400); + $res = TrendingHashtagService::getTrending(); - $res = Status::whereNotNull('uri') - ->where('uri', 'like', 'https://' . $domain . '%') - ->whereNull(['in_reply_to_id', 'reblog_of_id']) - ->orderByDesc('id') - ->limit(12) - ->pluck('id') - ->map(function($id) { - return StatusService::get($id); - }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); - return $res; - } + return $res; + } - public function enabledFeatures(Request $request) - { - abort_if(!$request->user(), 404); - return $this->config(); - } + public function trendingPlaces(Request $request) + { + return []; + } - public function updateFeatures(Request $request) - { - abort_if(!$request->user(), 404); - abort_if(!$request->user()->is_admin, 404); - $pid = $request->user()->profile_id; - $this->validate($request, [ - 'features.friends.enabled' => 'boolean', - 'features.hashtags.enabled' => 'boolean', - 'features.insights.enabled' => 'boolean', - 'features.memories.enabled' => 'boolean', - 'features.server.enabled' => 'boolean', - ]); - $res = $request->input('features'); - if($res['server'] && isset($res['server']['domains']) && !empty($res['server']['domains'])) { - $parts = explode(',', $res['server']['domains']); - $parts = array_filter($parts, function($v) { - $len = strlen($v); - $pos = strpos($v, '.'); - $domain = trim($v); - if($pos == false || $pos == ($len + 1)) { - return false; - } - if(!Instance::whereDomain($domain)->exists()) { - return false; - } - return true; - }); - $parts = array_slice($parts, 0, 10); - $d = implode(',', array_map('trim', $parts)); - $res['server']['domains'] = $d; - } - ConfigCacheService::put('config.discover.features', json_encode($res)); - return $res; - } + public function myMemories(Request $request) + { + abort_if(! $request->user(), 404); + $pid = $request->user()->profile_id; + abort_if(! $this->config()['memories']['enabled'], 404); + $type = $request->input('type') ?? 'posts'; + + switch ($type) { + case 'posts': + $res = Status::whereProfileId($pid) + ->whereDay('created_at', date('d')) + ->whereMonth('created_at', date('m')) + ->whereYear('created_at', '!=', date('Y')) + ->whereNull(['reblog_of_id', 'in_reply_to_id']) + ->limit(20) + ->pluck('id') + ->map(function ($id) { + return StatusService::get($id, false); + }) + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); + break; + + case 'liked': + $res = Like::whereProfileId($pid) + ->whereDay('created_at', date('d')) + ->whereMonth('created_at', date('m')) + ->whereYear('created_at', '!=', date('Y')) + ->orderByDesc('status_id') + ->limit(20) + ->pluck('status_id') + ->map(function ($id) { + $status = StatusService::get($id, false); + $status['favourited'] = true; + + return $status; + }) + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); + break; + } + + return $res; + } + + public function accountInsightsPopularPosts(Request $request) + { + abort_if(! $request->user(), 404); + $pid = $request->user()->profile_id; + abort_if(! $this->config()['insights']['enabled'], 404); + $posts = Cache::remember('pf:discover:metro2:accinsights:popular:'.$pid, 43200, function () use ($pid) { + return Status::whereProfileId($pid) + ->whereNotNull('likes_count') + ->orderByDesc('likes_count') + ->limit(12) + ->pluck('id') + ->map(function ($id) { + return StatusService::get($id, false); + }) + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); + }); + + return $posts; + } + + public function config() + { + $cc = ConfigCacheService::get('config.discover.features'); + if ($cc) { + return is_string($cc) ? json_decode($cc, true) : $cc; + } + + return [ + 'hashtags' => [ + 'enabled' => false, + ], + 'memories' => [ + 'enabled' => false, + ], + 'insights' => [ + 'enabled' => false, + ], + 'friends' => [ + 'enabled' => false, + ], + 'server' => [ + 'enabled' => false, + 'mode' => 'allowlist', + 'domains' => [], + ], + ]; + } + + public function serverTimeline(Request $request) + { + abort_if(! $request->user(), 404); + abort_if(! $this->config()['server']['enabled'], 404); + $pid = $request->user()->profile_id; + $domain = $request->input('domain'); + $config = $this->config(); + $domains = explode(',', $config['server']['domains']); + abort_unless(in_array($domain, $domains), 400); + + $res = Status::whereNotNull('uri') + ->where('uri', 'like', 'https://'.$domain.'%') + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->orderByDesc('id') + ->limit(12) + ->pluck('id') + ->map(function ($id) { + return StatusService::get($id); + }) + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); + + return $res; + } + + public function enabledFeatures(Request $request) + { + abort_if(! $request->user(), 404); + + return $this->config(); + } + + public function updateFeatures(Request $request) + { + abort_if(! $request->user(), 404); + abort_if(! $request->user()->is_admin, 404); + $pid = $request->user()->profile_id; + $this->validate($request, [ + 'features.friends.enabled' => 'boolean', + 'features.hashtags.enabled' => 'boolean', + 'features.insights.enabled' => 'boolean', + 'features.memories.enabled' => 'boolean', + 'features.server.enabled' => 'boolean', + ]); + $res = $request->input('features'); + if ($res['server'] && isset($res['server']['domains']) && ! empty($res['server']['domains'])) { + $parts = explode(',', $res['server']['domains']); + $parts = array_filter($parts, function ($v) { + $len = strlen($v); + $pos = strpos($v, '.'); + $domain = trim($v); + if ($pos == false || $pos == ($len + 1)) { + return false; + } + if (! Instance::whereDomain($domain)->exists()) { + return false; + } + + return true; + }); + $parts = array_slice($parts, 0, 10); + $d = implode(',', array_map('trim', $parts)); + $res['server']['domains'] = $d; + } + ConfigCacheService::put('config.discover.features', json_encode($res)); + + return $res; + } + + public function discoverAccountsPopular(Request $request) + { + abort_if(! $request->user(), 403); + + $pid = $request->user()->profile_id; + + $ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () { + return DB::table('profiles') + ->where('is_private', false) + ->whereNull('status') + ->orderByDesc('profiles.followers_count') + ->limit(30) + ->get(); + }); + $filters = UserFilterService::filters($pid); + $asf = AdminShadowFilterService::getHideFromPublicFeedsList(); + $ids = $ids->map(function ($profile) { + return AccountService::get($profile->id, true); + }) + ->filter(function ($profile) { + return $profile && isset($profile['id'], $profile['locked']) && ! $profile['locked']; + }) + ->filter(function ($profile) use ($pid) { + return $profile['id'] != $pid; + }) + ->filter(function ($profile) use ($pid) { + return ! FollowerService::follows($pid, $profile['id'], true); + }) + ->filter(function ($profile) use ($asf) { + return ! in_array($profile['id'], $asf); + }) + ->filter(function ($profile) use ($filters) { + return ! in_array($profile['id'], $filters); + }) + ->take(16) + ->values(); + + return response()->json($ids, 200, [], JSON_UNESCAPED_SLASHES); + } + + public function discoverNetworkTrending(Request $request) + { + abort_if(! $request->user(), 404); + + return BeagleService::getDiscoverPosts(); + } } diff --git a/app/Http/Controllers/FederationController.php b/app/Http/Controllers/FederationController.php index c4b5e86bf..15570eb6b 100644 --- a/app/Http/Controllers/FederationController.php +++ b/app/Http/Controllers/FederationController.php @@ -2,265 +2,304 @@ namespace App\Http\Controllers; -use App\Jobs\InboxPipeline\{ - DeleteWorker, - InboxWorker, - InboxValidator -}; -use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline; -use App\{ - AccountLog, - Like, - Profile, - Status, - User -}; -use App\Util\Lexer\Nickname; -use App\Util\Webfinger\Webfinger; -use Auth; -use Cache; -use Carbon\Carbon; -use Illuminate\Http\Request; -use League\Fractal; -use App\Util\Site\Nodeinfo; -use App\Util\ActivityPub\{ - Helpers, - HttpSignature, - Outbox -}; -use Zttp\Zttp; +use App\Jobs\InboxPipeline\DeleteWorker; +use App\Jobs\InboxPipeline\InboxValidator; +use App\Jobs\InboxPipeline\InboxWorker; +use App\Profile; +use App\Services\AccountService; use App\Services\InstanceService; +use App\Status; +use App\Util\Lexer\Nickname; +use App\Util\Site\Nodeinfo; +use App\Util\Webfinger\Webfinger; +use Cache; +use Illuminate\Http\Request; class FederationController extends Controller { - public function nodeinfoWellKnown() - { - abort_if(!config('federation.nodeinfo.enabled'), 404); - return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES) - ->header('Access-Control-Allow-Origin','*'); - } + public function nodeinfoWellKnown() + { + abort_if(! config('federation.nodeinfo.enabled'), 404); - public function nodeinfo() - { - abort_if(!config('federation.nodeinfo.enabled'), 404); - return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES) - ->header('Access-Control-Allow-Origin','*'); - } + return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES) + ->header('Access-Control-Allow-Origin', '*'); + } - public function webfinger(Request $request) - { - if (!config('federation.webfinger.enabled') || - !$request->has('resource') || - !$request->filled('resource') - ) { - return response('', 400); - } + public function nodeinfo() + { + abort_if(! config('federation.nodeinfo.enabled'), 404); - $resource = $request->input('resource'); - $domain = config('pixelfed.domain.app'); + return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES) + ->header('Access-Control-Allow-Origin', '*'); + } - if(config('federation.activitypub.sharedInbox') && - $resource == 'acct:' . $domain . '@' . $domain) { - $res = [ - 'subject' => 'acct:' . $domain . '@' . $domain, - 'aliases' => [ - 'https://' . $domain . '/i/actor' - ], - 'links' => [ - [ - 'rel' => 'http://webfinger.net/rel/profile-page', - 'type' => 'text/html', - 'href' => 'https://' . $domain . '/site/kb/instance-actor' - ], - [ - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => 'https://' . $domain . '/i/actor' - ] - ] - ]; - return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); - } - $hash = hash('sha256', $resource); - $key = 'federation:webfinger:sha256:' . $hash; - if($cached = Cache::get($key)) { - return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES); - } - if(strpos($resource, $domain) == false) { - return response('', 400); - } - $parsed = Nickname::normalizeProfileUrl($resource); - if(empty($parsed) || $parsed['domain'] !== $domain) { - return response('', 400); - } - $username = $parsed['username']; - $profile = Profile::whereNull('domain')->whereUsername($username)->first(); - if(!$profile || $profile->status !== null) { - return response('', 400); - } - $webfinger = (new Webfinger($profile))->generate(); - Cache::put($key, $webfinger, 1209600); + public function webfinger(Request $request) + { + if (! config('federation.webfinger.enabled') || + ! $request->has('resource') || + ! $request->filled('resource') + ) { + return response('', 400); + } - return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES) - ->header('Access-Control-Allow-Origin','*'); - } + $resource = $request->input('resource'); + $domain = config('pixelfed.domain.app'); - public function hostMeta(Request $request) - { - abort_if(!config('federation.webfinger.enabled'), 404); + // Instance Actor + if ( + config('federation.activitypub.sharedInbox') && + $resource == 'acct:'.$domain.'@'.$domain + ) { + $res = [ + 'subject' => 'acct:'.$domain.'@'.$domain, + 'aliases' => [ + 'https://'.$domain.'/i/actor', + ], + 'links' => [ + [ + 'rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => 'https://'.$domain.'/site/kb/instance-actor', + ], + [ + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => 'https://'.$domain.'/i/actor', + ], + [ + 'rel' => 'http://ostatus.org/schema/1.0/subscribe', + 'template' => 'https://'.$domain.'/authorize_interaction?uri={uri}', + ], + ], + ]; - $path = route('well-known.webfinger'); - $xml = ''; + return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); + } - return response($xml)->header('Content-Type', 'application/xrd+xml'); - } + if (str_starts_with($resource, 'https://')) { + if (str_starts_with($resource, 'https://'.$domain.'/users/')) { + $username = str_replace('https://'.$domain.'/users/', '', $resource); + if (strlen($username) > 15) { + return response('', 400); + } + $stripped = str_replace(['_', '.', '-'], '', $username); + if (! ctype_alnum($stripped)) { + return response('', 400); + } + $key = 'federation:webfinger:sha256:url-username:'.$username; + if ($cached = Cache::get($key)) { + return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES); + } + $profile = Profile::whereUsername($username)->first(); + if (! $profile || $profile->status !== null || $profile->domain) { + return response('', 400); + } + $webfinger = (new Webfinger($profile))->generate(); + Cache::put($key, $webfinger, 1209600); - public function userOutbox(Request $request, $username) - { - abort_if(!config_cache('federation.activitypub.enabled'), 404); + return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES) + ->header('Access-Control-Allow-Origin', '*'); + } else { + return response('', 400); + } + } + $hash = hash('sha256', $resource); + $key = 'federation:webfinger:sha256:'.$hash; + if ($cached = Cache::get($key)) { + return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES); + } + if (strpos($resource, $domain) == false) { + return response('', 400); + } + $parsed = Nickname::normalizeProfileUrl($resource); + if (empty($parsed) || $parsed['domain'] !== $domain) { + return response('', 400); + } + $username = $parsed['username']; + $profile = Profile::whereUsername($username)->first(); + if (! $profile || $profile->status !== null || $profile->domain) { + return response('', 400); + } + $webfinger = (new Webfinger($profile))->generate(); + Cache::put($key, $webfinger, 1209600); - if(!$request->wantsJson()) { - return redirect('/' . $username); - } + return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES) + ->header('Access-Control-Allow-Origin', '*'); + } - $res = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox', - 'type' => 'OrderedCollection', - 'totalItems' => 0, - 'orderedItems' => [] - ]; + public function hostMeta(Request $request) + { + abort_if(! config('federation.webfinger.enabled'), 404); - return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json'); - } + $path = route('well-known.webfinger'); + $xml = ''; - public function userInbox(Request $request, $username) - { - abort_if(!config_cache('federation.activitypub.enabled'), 404); - abort_if(!config('federation.activitypub.inbox'), 404); + return response($xml)->header('Content-Type', 'application/xrd+xml'); + } - $headers = $request->headers->all(); - $payload = $request->getContent(); - if(!$payload || empty($payload)) { - return; - } - $obj = json_decode($payload, true, 8); - if(!isset($obj['id'])) { - return; - } - $domain = parse_url($obj['id'], PHP_URL_HOST); - if(in_array($domain, InstanceService::getBannedDomains())) { - return; - } + public function userOutbox(Request $request, $username) + { + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); - if(isset($obj['type']) && $obj['type'] === 'Delete') { - if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) { - if($obj['object']['type'] === 'Person') { - if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) { - dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox'); - return; - } - } + if (! $request->wantsJson()) { + return redirect('/'.$username); + } - if($obj['object']['type'] === 'Tombstone') { - if(Status::whereObjectUrl($obj['object']['id'])->exists()) { - dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); - return; - } - } + $id = AccountService::usernameToId($username); + abort_if(! $id, 404); + $account = AccountService::get($id); + abort_if(! $account || ! isset($account['statuses_count']), 404); + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://'.config('pixelfed.domain.app').'/users/'.$username.'/outbox', + 'type' => 'OrderedCollection', + 'totalItems' => $account['statuses_count'] ?? 0, + ]; - if($obj['object']['type'] === 'Story') { - dispatch(new DeleteWorker($headers, $payload))->onQueue('story'); - return; - } - } - return; - } else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) { - dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow'); - } else { - dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high'); - } - return; - } + return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json'); + } - public function sharedInbox(Request $request) - { - abort_if(!config_cache('federation.activitypub.enabled'), 404); - abort_if(!config('federation.activitypub.sharedInbox'), 404); + public function userInbox(Request $request, $username) + { + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); + abort_if(! config('federation.activitypub.inbox'), 404); - $headers = $request->headers->all(); - $payload = $request->getContent(); + $headers = $request->headers->all(); + $payload = $request->getContent(); + if (! $payload || empty($payload)) { + return; + } + $obj = json_decode($payload, true, 8); + if (! isset($obj['id'])) { + return; + } + $domain = parse_url($obj['id'], PHP_URL_HOST); + if (in_array($domain, InstanceService::getBannedDomains())) { + return; + } - if(!$payload || empty($payload)) { - return; - } + if (isset($obj['type']) && $obj['type'] === 'Delete') { + if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) { + if ($obj['object']['type'] === 'Person') { + if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) { + dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox'); - $obj = json_decode($payload, true, 8); - if(!isset($obj['id'])) { - return; - } + return; + } + } - $domain = parse_url($obj['id'], PHP_URL_HOST); - if(in_array($domain, InstanceService::getBannedDomains())) { - return; - } + if ($obj['object']['type'] === 'Tombstone') { + if (Status::whereObjectUrl($obj['object']['id'])->exists()) { + dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); - if(isset($obj['type']) && $obj['type'] === 'Delete') { - if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) { - if($obj['object']['type'] === 'Person') { - if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) { - dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox'); - return; - } - } + return; + } + } - if($obj['object']['type'] === 'Tombstone') { - if(Status::whereObjectUrl($obj['object']['id'])->exists()) { - dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); - return; - } - } + if ($obj['object']['type'] === 'Story') { + dispatch(new DeleteWorker($headers, $payload))->onQueue('story'); - if($obj['object']['type'] === 'Story') { - dispatch(new DeleteWorker($headers, $payload))->onQueue('story'); - return; - } - } - return; - } else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) { - dispatch(new InboxWorker($headers, $payload))->onQueue('follow'); - } else { - dispatch(new InboxWorker($headers, $payload))->onQueue('shared'); - } - return; - } + return; + } + } - public function userFollowing(Request $request, $username) - { - abort_if(!config_cache('federation.activitypub.enabled'), 404); + return; + } elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) { + dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow'); + } else { + dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high'); + } - $obj = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $request->getUri(), - 'type' => 'OrderedCollectionPage', - 'totalItems' => 0, - 'orderedItems' => [] - ]; - return response()->json($obj); - } + } - public function userFollowers(Request $request, $username) - { - abort_if(!config_cache('federation.activitypub.enabled'), 404); + public function sharedInbox(Request $request) + { + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); + abort_if(! config('federation.activitypub.sharedInbox'), 404); - $obj = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $request->getUri(), - 'type' => 'OrderedCollectionPage', - 'totalItems' => 0, - 'orderedItems' => [] - ]; + $headers = $request->headers->all(); + $payload = $request->getContent(); - return response()->json($obj); - } + if (! $payload || empty($payload)) { + return; + } + + $obj = json_decode($payload, true, 8); + if (! isset($obj['id'])) { + return; + } + + $domain = parse_url($obj['id'], PHP_URL_HOST); + if (in_array($domain, InstanceService::getBannedDomains())) { + return; + } + + if (isset($obj['type']) && $obj['type'] === 'Delete') { + if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) { + if ($obj['object']['type'] === 'Person') { + if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) { + dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox'); + + return; + } + } + + if ($obj['object']['type'] === 'Tombstone') { + if (Status::whereObjectUrl($obj['object']['id'])->exists()) { + dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); + + return; + } + } + + if ($obj['object']['type'] === 'Story') { + dispatch(new DeleteWorker($headers, $payload))->onQueue('story'); + + return; + } + } + + return; + } elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) { + dispatch(new InboxWorker($headers, $payload))->onQueue('follow'); + } else { + dispatch(new InboxWorker($headers, $payload))->onQueue('shared'); + } + + } + + public function userFollowing(Request $request, $username) + { + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); + + $id = AccountService::usernameToId($username); + abort_if(! $id, 404); + $account = AccountService::get($id); + abort_if(! $account || ! isset($account['following_count']), 404); + $obj = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $request->getUri(), + 'type' => 'OrderedCollection', + 'totalItems' => $account['following_count'] ?? 0, + ]; + + return response()->json($obj)->header('Content-Type', 'application/activity+json'); + } + + public function userFollowers(Request $request, $username) + { + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); + $id = AccountService::usernameToId($username); + abort_if(! $id, 404); + $account = AccountService::get($id); + abort_if(! $account || ! isset($account['followers_count']), 404); + $obj = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $request->getUri(), + 'type' => 'OrderedCollection', + 'totalItems' => $account['followers_count'] ?? 0, + ]; + + return response()->json($obj)->header('Content-Type', 'application/activity+json'); + } } diff --git a/app/Http/Controllers/GroupController.php b/app/Http/Controllers/GroupController.php new file mode 100644 index 000000000..881d31f01 --- /dev/null +++ b/app/Http/Controllers/GroupController.php @@ -0,0 +1,671 @@ +middleware('auth'); + abort_unless(config('groups.enabled'), 404); + } + + public function index(Request $request) + { + abort_if(! $request->user(), 404); + + return view('layouts.spa'); + } + + public function home(Request $request) + { + abort_if(! $request->user(), 404); + + return view('layouts.spa'); + } + + public function show(Request $request, $id, $path = false) + { + $group = Group::find($id); + + if (! $group || $group->status) { + return response()->view('groups.unavailable')->setStatusCode(404); + } + + if ($request->wantsJson()) { + return $this->showGroupObject($group); + } + + return view('layouts.spa', compact('id', 'path')); + } + + public function showStatus(Request $request, $gid, $sid) + { + $group = Group::find($gid); + $pid = optional($request->user())->profile_id ?? false; + + if (! $group || $group->status) { + return response()->view('groups.unavailable')->setStatusCode(404); + } + + if ($group->is_private) { + abort_if(! $request->user(), 404); + abort_if(! $group->isMember($pid), 404); + } + + $gp = GroupPost::whereGroupId($gid) + ->findOrFail($sid); + + return view('layouts.spa', compact('group', 'gp')); + } + + public function getGroup(Request $request, $id) + { + $group = Group::whereNull('status')->findOrFail($id); + $pid = optional($request->user())->profile_id ?? false; + + $group = $this->toJson($group, $pid); + + return response()->json($group, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + public function showStatusLikes(Request $request, $id, $sid) + { + $group = Group::findOrFail($id); + $user = $request->user(); + $pid = $user->profile_id; + abort_if(! $group->isMember($pid), 404); + $status = GroupPost::whereGroupId($id)->findOrFail($sid); + $likes = GroupLike::whereStatusId($sid) + ->cursorPaginate(10) + ->map(function ($l) use ($group) { + $account = AccountService::get($l->profile_id); + $account['url'] = "/groups/{$group->id}/user/{$account['id']}"; + + return $account; + }) + ->filter(function ($l) { + return $l && isset($l['id']); + }) + ->values(); + + return $likes; + } + + public function groupSettings(Request $request, $id) + { + abort_if(! $request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(! $group->isMember($pid), 404); + abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + return view('groups.settings', compact('group')); + } + + public function joinGroup(Request $request, $id) + { + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if($group->isMember($pid), 404); + + if (! $request->user()->is_admin) { + abort_if(GroupService::getRejoinTimeout($group->id, $pid), 422, 'Cannot re-join this group for 24 hours after leaving or cancelling a request to join'); + } + + $member = new GroupMember; + $member->group_id = $group->id; + $member->profile_id = $pid; + $member->role = 'member'; + $member->local_group = true; + $member->local_profile = true; + $member->join_request = $group->is_private; + $member->save(); + + GroupService::delSelf($group->id, $pid); + GroupService::log( + $group->id, + $pid, + 'group:joined', + null, + GroupMember::class, + $member->id + ); + + $group = $this->toJson($group, $pid); + + return $group; + } + + public function updateGroup(Request $request, $id) + { + $this->validate($request, [ + 'description' => 'nullable|max:500', + 'membership' => 'required|in:all,local,private', + 'avatar' => 'nullable', + 'header' => 'nullable', + 'discoverable' => 'required', + 'activitypub' => 'required', + 'is_nsfw' => 'required', + 'category' => 'required|string|in:'.implode(',', GroupService::categories()), + ]); + + $pid = $request->user()->profile_id; + $group = Group::whereProfileId($pid)->findOrFail($id); + $member = GroupMember::whereGroupId($group->id)->whereProfileId($pid)->firstOrFail(); + + abort_if($member->role != 'founder', 403, 'Invalid group permission'); + + $metadata = $group->metadata; + $len = $group->is_private ? 12 : 4; + + if ($request->hasFile('avatar')) { + $avatar = $request->file('avatar'); + + if ($avatar) { + if (isset($metadata['avatar']) && + isset($metadata['avatar']['path']) && + Storage::exists($metadata['avatar']['path']) + ) { + Storage::delete($metadata['avatar']['path']); + } + + $fileName = 'avatar_'.strtolower(str_random($len)).'.'.$avatar->extension(); + $path = $avatar->storePubliclyAs('public/g/'.$group->id.'/meta', $fileName); + $url = url(Storage::url($path)); + $metadata['avatar'] = [ + 'path' => $path, + 'url' => $url, + 'updated_at' => now(), + ]; + } + } + + if ($request->hasFile('header')) { + $header = $request->file('header'); + + if ($header) { + if (isset($metadata['header']) && + isset($metadata['header']['path']) && + Storage::exists($metadata['header']['path']) + ) { + Storage::delete($metadata['header']['path']); + } + + $fileName = 'header_'.strtolower(str_random($len)).'.'.$header->extension(); + $path = $header->storePubliclyAs('public/g/'.$group->id.'/meta', $fileName); + $url = url(Storage::url($path)); + $metadata['header'] = [ + 'path' => $path, + 'url' => $url, + 'updated_at' => now(), + ]; + } + } + + $cat = GroupService::categoryById($group->category_id); + if ($request->category !== $cat['name']) { + $group->category_id = GroupCategory::whereName($request->category)->first()->id; + } + + $changes = null; + $group->description = e($request->input('description', null)); + $group->is_private = $request->input('membership') == 'private'; + $group->local_only = $request->input('membership') == 'local'; + $group->activitypub = $request->input('activitypub') == 'true'; + $group->discoverable = $request->input('discoverable') == 'true'; + $group->is_nsfw = $request->input('is_nsfw') == 'true'; + $group->metadata = $metadata; + if ($group->isDirty()) { + $changes = $group->getDirty(); + } + $group->save(); + + GroupService::log( + $group->id, + $pid, + 'group:settings:updated', + $changes + ); + + GroupService::del($group->id); + + $res = $this->toJson($group, $pid); + + return $res; + } + + protected function toJson($group, $pid = false) + { + return GroupService::get($group->id, $pid); + } + + public function groupLeave(Request $request, $id) + { + abort_if(! $request->user(), 404); + + $pid = $request->user()->profile_id; + $group = Group::findOrFail($id); + + abort_if($pid == $group->profile_id, 422, 'Cannot leave a group you created'); + + abort_if(! $group->isMember($pid), 403, 'Not a member of group.'); + + GroupMember::whereGroupId($group->id)->whereProfileId($pid)->delete(); + GroupService::del($group->id); + GroupService::delSelf($group->id, $pid); + GroupService::setRejoinTimeout($group->id, $pid); + + return [200]; + } + + public function cancelJoinRequest(Request $request, $id) + { + abort_if(! $request->user(), 404); + + $pid = $request->user()->profile_id; + $group = Group::findOrFail($id); + + abort_if($pid == $group->profile_id, 422, 'Cannot leave a group you created'); + abort_if($group->isMember($pid), 422, 'Cannot cancel approved join request, please leave group instead.'); + + GroupMember::whereGroupId($group->id)->whereProfileId($pid)->delete(); + GroupService::del($group->id); + GroupService::delSelf($group->id, $pid); + GroupService::setRejoinTimeout($group->id, $pid); + + return [200]; + } + + public function metaBlockSearch(Request $request, $id) + { + abort_if(! $request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(! $group->isMember($pid), 404); + abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $type = $request->input('type'); + $item = $request->input('item'); + + switch ($type) { + case 'instance': + $res = Instance::whereDomain($item)->first(); + if ($res) { + abort_if(GroupBlock::whereGroupId($group->id)->whereInstanceId($res->id)->exists(), 400); + } + break; + + case 'user': + $res = Profile::whereUsername($item)->first(); + if ($res) { + abort_if(GroupBlock::whereGroupId($group->id)->whereProfileId($res->id)->exists(), 400); + } + if ($res->user_id != null) { + abort_if(User::whereIsAdmin(true)->whereId($res->user_id)->exists(), 400); + } + break; + } + + return response()->json((bool) $res, ($res ? 200 : 404)); + } + + public function reportCreate(Request $request, $id) + { + abort_if(! $request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(! $group->isMember($pid), 404); + + $id = $request->input('id'); + $type = $request->input('type'); + $types = [ + // original 3 + 'spam', + 'sensitive', + 'abusive', + + // new + 'underage', + 'violence', + 'copyright', + 'impersonation', + 'scam', + 'terrorism', + ]; + + $gp = GroupPost::whereGroupId($group->id)->find($id); + abort_if(! $gp, 422, 'Cannot report an invalid or deleted post'); + abort_if(! in_array($type, $types), 422, 'Invalid report type'); + abort_if($gp->profile_id === $pid, 422, 'Cannot report your own post'); + abort_if( + GroupReport::whereGroupId($group->id) + ->whereProfileId($pid) + ->whereItemType(GroupPost::class) + ->whereItemId($id) + ->exists(), + 422, + 'You already reported this' + ); + + $report = new GroupReport(); + $report->group_id = $group->id; + $report->profile_id = $pid; + $report->type = $type; + $report->item_type = GroupPost::class; + $report->item_id = $id; + $report->open = true; + $report->save(); + + GroupService::log( + $group->id, + $pid, + 'group:report:create', + [ + 'type' => $type, + 'report_id' => $report->id, + 'status_id' => $gp->status_id, + 'profile_id' => $gp->profile_id, + 'username' => optional(AccountService::get($gp->profile_id))['acct'], + 'gpid' => $gp->id, + 'url' => $gp->url(), + ], + GroupReport::class, + $report->id + ); + + return response([200]); + } + + public function reportAction(Request $request, $id) + { + abort_if(! $request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(! $group->isMember($pid), 404); + abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $this->validate($request, [ + 'action' => 'required|in:cw,delete,ignore', + 'id' => 'required|string', + ]); + + $action = $request->input('action'); + $id = $request->input('id'); + + $report = GroupReport::whereGroupId($group->id) + ->findOrFail($id); + $status = Status::findOrFail($report->item_id); + $gp = GroupPost::whereGroupId($group->id) + ->whereStatusId($status->id) + ->firstOrFail(); + + switch ($action) { + case 'cw': + $status->is_nsfw = true; + $status->save(); + StatusService::del($status->id); + + GroupReport::whereGroupId($group->id) + ->whereItemType($report->item_type) + ->whereItemId($report->item_id) + ->update(['open' => false]); + + GroupService::log( + $group->id, + $pid, + 'group:moderation:action', + [ + 'type' => 'cw', + 'report_id' => $report->id, + 'status_id' => $status->id, + 'profile_id' => $status->profile_id, + 'status_url' => $gp->url(), + ], + GroupReport::class, + $report->id + ); + + return response()->json([200]); + break; + + case 'ignore': + GroupReport::whereGroupId($group->id) + ->whereItemType($report->item_type) + ->whereItemId($report->item_id) + ->update(['open' => false]); + + GroupService::log( + $group->id, + $pid, + 'group:moderation:action', + [ + 'type' => 'ignore', + 'report_id' => $report->id, + 'status_id' => $status->id, + 'profile_id' => $status->profile_id, + 'status_url' => $gp->url(), + ], + GroupReport::class, + $report->id + ); + + return response()->json([200]); + break; + } + } + + public function getMemberInteractionLimits(Request $request, $id) + { + abort_if(! $request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(! $group->isMember($pid), 404); + abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $profile_id = $request->input('profile_id'); + abort_if(! $group->isMember($profile_id), 404); + $limits = GroupService::getInteractionLimits($group->id, $profile_id); + + return response()->json($limits); + } + + public function updateMemberInteractionLimits(Request $request, $id) + { + abort_if(! $request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(! $group->isMember($pid), 404); + abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $this->validate($request, [ + 'profile_id' => 'required|exists:profiles,id', + 'can_post' => 'required', + 'can_comment' => 'required', + 'can_like' => 'required', + ]); + + $member = $request->input('profile_id'); + $can_post = $request->input('can_post'); + $can_comment = $request->input('can_comment'); + $can_like = $request->input('can_like'); + $account = AccountService::get($member); + + abort_if(! $account, 422, 'Invalid profile'); + abort_if(! $group->isMember($member), 422, 'Invalid profile'); + + $limit = GroupLimit::firstOrCreate([ + 'profile_id' => $member, + 'group_id' => $group->id, + ]); + + if ($limit->wasRecentlyCreated) { + abort_if(GroupLimit::whereGroupId($group->id)->count() >= 25, 422, 'limit_reached'); + } + + $previousLimits = $limit->limits; + + $limit->limits = [ + 'can_post' => $can_post, + 'can_comment' => $can_comment, + 'can_like' => $can_like, + ]; + $limit->save(); + + GroupService::clearInteractionLimits($group->id, $member); + + GroupService::log( + $group->id, + $pid, + 'group:member-limits:updated', + [ + 'profile_id' => $account['id'], + 'username' => $account['username'], + 'previousLimits' => $previousLimits, + 'newLimits' => $limit->limits, + ], + GroupLimit::class, + $limit->id + ); + + return $request->all(); + } + + public function showProfile(Request $request, $id, $pid) + { + $group = Group::find($id); + + if (! $group || $group->status) { + return response()->view('groups.unavailable')->setStatusCode(404); + } + + return view('layouts.spa'); + } + + public function showProfileByUsername(Request $request, $id, $pid) + { + abort_if(! $request->user(), 404); + if (! $request->user()) { + return redirect("/{$pid}"); + } + + $group = Group::find($id); + $cid = $request->user()->profile_id; + + if (! $group || $group->status) { + return response()->view('groups.unavailable')->setStatusCode(404); + } + + if (! $group->isMember($cid)) { + return redirect("/{$pid}"); + } + + $profile = Profile::whereUsername($pid)->first(); + + if (! $group->isMember($profile->id)) { + return redirect("/{$pid}"); + } + + if ($profile) { + $url = url("/groups/{$id}/user/{$profile->id}"); + + return redirect($url); + } + + abort(404, 'Invalid username'); + } + + public function groupInviteLanding(Request $request, $id) + { + abort(404, 'Not yet implemented'); + $group = Group::findOrFail($id); + + return view('groups.invite', compact('group')); + } + + public function groupShortLinkRedirect(Request $request, $hid) + { + $gid = HashidService::decode($hid); + $group = Group::findOrFail($gid); + + return redirect($group->url()); + } + + public function groupInviteClaim(Request $request, $id) + { + $group = GroupService::get($id); + abort_if(! $group || empty($group), 404); + + return view('groups.invite-claim', compact('group')); + } + + public function groupMemberInviteCheck(Request $request, $id) + { + abort_if(! $request->user(), 404); + $pid = $request->user()->profile_id; + $group = Group::findOrFail($id); + abort_if($group->isMember($pid), 422, 'Already a member'); + + $exists = GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->exists(); + + return response()->json([ + 'gid' => $id, + 'can_join' => (bool) $exists, + ]); + } + + public function groupMemberInviteAccept(Request $request, $id) + { + abort_if(! $request->user(), 404); + $pid = $request->user()->profile_id; + $group = Group::findOrFail($id); + abort_if($group->isMember($pid), 422, 'Already a member'); + + abort_if(! GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->exists(), 422); + + $gm = new GroupMember; + $gm->group_id = $id; + $gm->profile_id = $pid; + $gm->role = 'member'; + $gm->local_group = $group->local; + $gm->local_profile = true; + $gm->join_request = false; + $gm->save(); + + GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->delete(); + GroupService::del($id); + GroupService::delSelf($id, $pid); + + return ['next_url' => $group->url()]; + } + + public function groupMemberInviteDecline(Request $request, $id) + { + abort_if(! $request->user(), 404); + $pid = $request->user()->profile_id; + $group = Group::findOrFail($id); + abort_if($group->isMember($pid), 422, 'Already a member'); + + return ['next_url' => '/']; + } +} diff --git a/app/Http/Controllers/GroupFederationController.php b/app/Http/Controllers/GroupFederationController.php new file mode 100644 index 000000000..0e5879b01 --- /dev/null +++ b/app/Http/Controllers/GroupFederationController.php @@ -0,0 +1,107 @@ +whereActivitypub(true)->findOrFail($id); + $res = $this->showGroupObject($group); + + return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + public function showGroupObject($group) + { + return Cache::remember('ap:groups:object:'.$group->id, 3600, function () use ($group) { + return [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $group->url(), + 'inbox' => $group->permalink('/inbox'), + 'name' => $group->name, + 'outbox' => $group->permalink('/outbox'), + 'summary' => $group->description, + 'type' => 'Group', + 'attributedTo' => [ + 'type' => 'Person', + 'id' => $group->admin->permalink(), + ], + // 'endpoints' => [ + // 'sharedInbox' => config('app.url') . '/f/inbox' + // ], + 'preferredUsername' => 'gid_'.$group->id, + 'publicKey' => [ + 'id' => $group->permalink('#main-key'), + 'owner' => $group->permalink(), + 'publicKeyPem' => InstanceActor::first()->public_key, + ], + 'url' => $group->permalink(), + ]; + + if ($group->metadata && isset($group->metadata['avatar'])) { + $res['icon'] = [ + 'type' => 'Image', + 'url' => $group->metadata['avatar']['url'], + ]; + } + + if ($group->metadata && isset($group->metadata['header'])) { + $res['image'] = [ + 'type' => 'Image', + 'url' => $group->metadata['header']['url'], + ]; + } + ksort($res); + + return $res; + }); + } + + public function getStatusObject(Request $request, $gid, $sid) + { + $group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($gid); + $gp = GroupPost::whereGroupId($gid)->findOrFail($sid); + $status = Status::findOrFail($gp->status_id); + // permission check + $content = $status->caption ? Autolink::create()->autolink($status->caption) : null; + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $gp->url(), + + 'type' => 'Note', + + 'summary' => null, + 'content' => $content, + 'inReplyTo' => null, + + 'published' => $status->created_at->toAtomString(), + 'url' => $gp->url(), + 'attributedTo' => $status->profile->permalink(), + 'to' => [ + 'https://www.w3.org/ns/activitystreams#Public', + $group->permalink('/followers'), + ], + 'cc' => [], + 'sensitive' => (bool) $status->is_nsfw, + 'attachment' => MediaService::activitypub($status->id), + 'target' => [ + 'type' => 'Collection', + 'id' => $group->permalink('/wall'), + 'attributedTo' => $group->permalink(), + ], + ]; + + // ksort($res); + return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } +} diff --git a/app/Http/Controllers/GroupPostController.php b/app/Http/Controllers/GroupPostController.php new file mode 100644 index 000000000..909037a00 --- /dev/null +++ b/app/Http/Controllers/GroupPostController.php @@ -0,0 +1,10 @@ +middleware('auth'); + } + + public function checkCreatePermission(Request $request) + { + abort_if(!$request->user(), 404); + $pid = $request->user()->profile_id; + $config = GroupService::config(); + if($request->user()->is_admin) { + $allowed = true; + } else { + $max = $config['limits']['user']['create']['max']; + $allowed = Group::whereProfileId($pid)->count() <= $max; + } + + return ['permission' => (bool) $allowed]; + } + + public function storeGroup(Request $request) + { + abort_if(!$request->user(), 404); + + $this->validate($request, [ + 'name' => 'required', + 'description' => 'nullable|max:500', + 'membership' => 'required|in:public,private,local' + ]); + + $pid = $request->user()->profile_id; + + $config = GroupService::config(); + abort_if($config['limits']['user']['create']['new'] == false && $request->user()->is_admin == false, 422, 'Invalid operation'); + $max = $config['limits']['user']['create']['max']; + // abort_if(Group::whereProfileId($pid)->count() <= $max, 422, 'Group limit reached'); + + $group = new Group; + $group->profile_id = $pid; + $group->name = $request->input('name'); + $group->description = $request->input('description', null); + $group->is_private = $request->input('membership') == 'private'; + $group->local_only = $request->input('membership') == 'local'; + $group->metadata = $request->input('configuration'); + $group->save(); + + GroupService::log($group->id, $pid, 'group:created'); + + $member = new GroupMember; + $member->group_id = $group->id; + $member->profile_id = $pid; + $member->role = 'founder'; + $member->local_group = true; + $member->local_profile = true; + $member->save(); + + GroupService::log( + $group->id, + $pid, + 'group:joined', + null, + GroupMember::class, + $member->id + ); + + return [ + 'id' => $group->id, + 'url' => $group->url() + ]; + } +} diff --git a/app/Http/Controllers/Groups/GroupsAdminController.php b/app/Http/Controllers/Groups/GroupsAdminController.php new file mode 100644 index 000000000..4bdf0f504 --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsAdminController.php @@ -0,0 +1,353 @@ +middleware('auth'); + } + + public function getAdminTabs(Request $request, $id) + { + abort_if(!$request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + abort_if($pid !== $group->profile_id, 404); + + $reqs = GroupMember::whereGroupId($group->id)->whereJoinRequest(true)->count(); + $mods = GroupReport::whereGroupId($group->id)->whereOpen(true)->count(); + $tabs = [ + 'moderation_count' => $mods > 99 ? '99+' : $mods, + 'request_count' => $reqs > 99 ? '99+' : $reqs + ]; + + return response()->json($tabs); + } + + public function getInteractionLogs(Request $request, $id) + { + abort_if(!$request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $logs = GroupInteraction::whereGroupId($id) + ->latest() + ->paginate(10) + ->map(function($log) use($group) { + return [ + 'id' => $log->id, + 'profile' => GroupAccountService::get($group->id, $log->profile_id), + 'type' => $log->type, + 'metadata' => $log->metadata, + 'created_at' => $log->created_at->format('c') + ]; + }); + + return response()->json($logs, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function getBlocks(Request $request, $id) + { + abort_if(!$request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $blocks = [ + 'instances' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(false)->latest()->take(3)->pluck('name'), + 'users' => GroupBlock::whereGroupId($group->id)->whereNotNull('profile_id')->whereIsUser(true)->latest()->take(3)->pluck('name'), + 'moderated' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(true)->latest()->take(3)->pluck('name') + ]; + + return response()->json($blocks, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function exportBlocks(Request $request, $id) + { + abort_if(!$request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $blocks = [ + 'instances' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(false)->latest()->pluck('name'), + 'users' => GroupBlock::whereGroupId($group->id)->whereNotNull('profile_id')->whereIsUser(true)->latest()->pluck('name'), + 'moderated' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(true)->latest()->pluck('name') + ]; + + $blocks['_created_at'] = now()->format('c'); + $blocks['_version'] = '1.0.0'; + ksort($blocks); + + return response()->streamDownload(function() use($blocks) { + echo json_encode($blocks, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + }); + } + + public function addBlock(Request $request, $id) + { + abort_if(!$request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $this->validate($request, [ + 'item' => 'required', + 'type' => 'required|in:instance,user,moderate' + ]); + + $item = $request->input('item'); + $type = $request->input('type'); + + switch($type) { + case 'instance': + $instance = Instance::whereDomain($item)->first(); + abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid'); + $gb = new GroupBlock; + $gb->group_id = $group->id; + $gb->admin_id = $pid; + $gb->instance_id = $instance->id; + $gb->name = $instance->domain; + $gb->is_user = false; + $gb->moderated = false; + $gb->save(); + + GroupService::log( + $group->id, + $pid, + 'group:admin:block:instance', + [ + 'domain' => $instance->domain + ], + GroupBlock::class, + $gb->id + ); + + return [200]; + break; + + case 'user': + $profile = Profile::whereUsername($item)->first(); + abort_if(!$profile, 422, 'This user either isn\'nt known or is invalid'); + $gb = new GroupBlock; + $gb->group_id = $group->id; + $gb->admin_id = $pid; + $gb->profile_id = $profile->id; + $gb->name = $profile->username; + $gb->is_user = true; + $gb->moderated = false; + $gb->save(); + + GroupService::log( + $group->id, + $pid, + 'group:admin:block:user', + [ + 'username' => $profile->username, + 'domain' => $profile->domain + ], + GroupBlock::class, + $gb->id + ); + + return [200]; + break; + + case 'moderate': + $instance = Instance::whereDomain($item)->first(); + abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid'); + $gb = new GroupBlock; + $gb->group_id = $group->id; + $gb->admin_id = $pid; + $gb->instance_id = $instance->id; + $gb->name = $instance->domain; + $gb->is_user = false; + $gb->moderated = true; + $gb->save(); + + GroupService::log( + $group->id, + $pid, + 'group:admin:moderate:instance', + [ + 'domain' => $instance->domain + ], + GroupBlock::class, + $gb->id + ); + + return [200]; + break; + + default: + return response()->json([], 422, []); + break; + } + } + + public function undoBlock(Request $request, $id) + { + abort_if(!$request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $this->validate($request, [ + 'item' => 'required', + 'type' => 'required|in:instance,user,moderate' + ]); + + $item = $request->input('item'); + $type = $request->input('type'); + + switch($type) { + case 'instance': + $instance = Instance::whereDomain($item)->first(); + abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid'); + + $gb = GroupBlock::whereGroupId($group->id) + ->whereInstanceId($instance->id) + ->whereModerated(false) + ->first(); + + abort_if(!$gb, 422, 'Invalid group block'); + + GroupService::log( + $group->id, + $pid, + 'group:admin:unblock:instance', + [ + 'domain' => $instance->domain + ], + GroupBlock::class, + $gb->id + ); + + $gb->delete(); + + return [200]; + break; + + case 'user': + $profile = Profile::whereUsername($item)->first(); + abort_if(!$profile, 422, 'This user either isn\'nt known or is invalid'); + + $gb = GroupBlock::whereGroupId($group->id) + ->whereProfileId($profile->id) + ->whereIsUser(true) + ->first(); + + abort_if(!$gb, 422, 'Invalid group block'); + + GroupService::log( + $group->id, + $pid, + 'group:admin:unblock:user', + [ + 'username' => $profile->username, + 'domain' => $profile->domain + ], + GroupBlock::class, + $gb->id + ); + + $gb->delete(); + + return [200]; + break; + + case 'moderate': + $instance = Instance::whereDomain($item)->first(); + abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid'); + + $gb = GroupBlock::whereGroupId($group->id) + ->whereInstanceId($instance->id) + ->whereModerated(true) + ->first(); + + abort_if(!$gb, 422, 'Invalid group block'); + + GroupService::log( + $group->id, + $pid, + 'group:admin:moderate:instance', + [ + 'domain' => $instance->domain + ], + GroupBlock::class, + $gb->id + ); + + $gb->delete(); + + return [200]; + break; + + default: + return response()->json([], 422, []); + break; + } + } + + public function getReportList(Request $request, $id) + { + abort_if(!$request->user(), 404); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $scope = $request->input('scope', 'open'); + + $list = GroupReport::selectRaw('id, profile_id, item_type, item_id, type, created_at, count(*) as total') + ->whereGroupId($group->id) + ->groupBy('item_id') + ->when($scope == 'open', function($query, $scope) { + return $query->whereOpen(true); + }) + ->latest() + ->simplePaginate(10) + ->map(function($report) use($group) { + $res = [ + 'id' => (string) $report->id, + 'profile' => GroupAccountService::get($group->id, $report->profile_id), + 'type' => $report->type, + 'created_at' => $report->created_at->format('c'), + 'total_count' => $report->total + ]; + + if($report->item_type === GroupPost::class) { + $res['status'] = GroupPostService::get($group->id, $report->item_id); + } + + return $res; + }); + return response()->json($list, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + +} diff --git a/app/Http/Controllers/Groups/GroupsApiController.php b/app/Http/Controllers/Groups/GroupsApiController.php new file mode 100644 index 000000000..13bbca640 --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsApiController.php @@ -0,0 +1,84 @@ +middleware('auth'); + } + + protected function toJson($group, $pid = false) + { + return GroupService::get($group->id, $pid); + } + + public function getConfig(Request $request) + { + return GroupService::config(); + } + + public function getGroupAccount(Request $request, $gid, $pid) + { + $res = GroupAccountService::get($gid, $pid); + + return response()->json($res); + } + + public function getGroupCategories(Request $request) + { + $res = GroupService::categories(); + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function getGroupsByCategory(Request $request) + { + $name = $request->input('name'); + $category = GroupCategory::whereName($name)->firstOrFail(); + $groups = Group::whereCategoryId($category->id) + ->simplePaginate(6) + ->map(function($group) { + return GroupService::get($group->id); + }) + ->filter(function($group) { + return $group; + }) + ->values(); + return $groups; + } + + public function getRecommendedGroups(Request $request) + { + return []; + } + + public function getSelfGroups(Request $request) + { + $selfOnly = $request->input('self') == true; + $memberOnly = $request->input('member') == true; + $pid = $request->user()->profile_id; + $res = GroupMember::whereProfileId($request->user()->profile_id) + ->when($selfOnly, function($q, $selfOnly) { + return $q->whereRole('founder'); + }) + ->when($memberOnly, function($q, $memberOnly) { + return $q->whereRole('member'); + }) + ->simplePaginate(4) + ->map(function($member) use($pid) { + $group = $member->group; + return $this->toJson($group, $pid); + }); + + return response()->json($res); + } +} diff --git a/app/Http/Controllers/Groups/GroupsCommentController.php b/app/Http/Controllers/Groups/GroupsCommentController.php new file mode 100644 index 000000000..435ed0d78 --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsCommentController.php @@ -0,0 +1,361 @@ +validate($request, [ + 'gid' => 'required', + 'sid' => 'required', + 'cid' => 'sometimes', + 'limit' => 'nullable|integer|min:3|max:10' + ]); + + $pid = optional($request->user())->profile_id; + $gid = $request->input('gid'); + $sid = $request->input('sid'); + $cid = $request->has('cid') && $request->input('cid') == 1; + $limit = $request->input('limit', 3); + $maxId = $request->input('max_id', 0); + + $group = Group::findOrFail($gid); + + abort_if($group->is_private && !$group->isMember($pid), 403, 'Not a member of group.'); + + $status = $cid ? GroupComment::findOrFail($sid) : GroupPost::findOrFail($sid); + + abort_if($status->group_id != $group->id, 400, 'Invalid group'); + + $replies = GroupComment::whereGroupId($group->id) + ->whereStatusId($status->id) + ->orderByDesc('id') + ->when($maxId, function($query, $maxId) { + return $query->where('id', '<', $maxId); + }) + ->take($limit) + ->get() + ->map(function($gp) use($pid) { + $status = GroupCommentService::get($gp['group_id'], $gp['id']); + $status['reply_count'] = $gp['reply_count']; + $status['url'] = $gp->url(); + $status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']); + $status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$gp['profile_id']}"); + return $status; + }); + + return $replies->toArray(); + } + + public function storeComment(Request $request) + { + $this->validate($request, [ + 'gid' => 'required|exists:groups,id', + 'sid' => 'required|exists:group_posts,id', + 'cid' => 'sometimes', + 'content' => 'required|string|min:1|max:1500' + ]); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $sid = $request->input('sid'); + $cid = $request->input('cid'); + $limit = $request->input('limit', 3); + $caption = e($request->input('content')); + + $group = Group::findOrFail($gid); + + abort_if(!$group->isMember($pid), 403, 'Not a member of group.'); + abort_if(!GroupService::canComment($gid, $pid), 422, 'You cannot interact with this content at this time'); + + + $parent = $cid == 1 ? + GroupComment::findOrFail($sid) : + GroupPost::whereGroupId($gid)->findOrFail($sid); + // $autolink = Purify::clean(Autolink::create()->autolink($caption)); + // $autolink = str_replace('/discover/tags/', '/groups/' . $gid . '/topics/', $autolink); + + $status = new GroupComment; + $status->group_id = $group->id; + $status->profile_id = $pid; + $status->status_id = $parent->id; + $status->caption = Purify::clean($caption); + $status->visibility = 'public'; + $status->is_nsfw = false; + $status->local = true; + $status->save(); + + NewCommentPipeline::dispatch($parent, $status)->onQueue('groups'); + // todo: perform in job + $parent->reply_count = $parent->reply_count ? $parent->reply_count + $parent->reply_count : 1; + $parent->save(); + GroupPostService::del($parent->group_id, $parent->id); + + GroupService::log( + $group->id, + $pid, + 'group:comment:created', + [ + 'type' => 'group:post:comment', + 'status_id' => $status->id + ], + GroupPost::class, + $status->id + ); + + //GroupCommentPipeline::dispatch($parent, $status, $gp); + //NewStatusPipeline::dispatch($status, $gp); + //GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id)); + + // todo: perform in job + $s = GroupCommentService::get($status->group_id, $status->id); + + $s['pf_type'] = 'text'; + $s['visibility'] = 'public'; + $s['url'] = $status->url(); + + return $s; + } + + public function storeCommentPhoto(Request $request) + { + $this->validate($request, [ + 'gid' => 'required|exists:groups,id', + 'sid' => 'required|exists:group_posts,id', + 'photo' => 'required|image' + ]); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $sid = $request->input('sid'); + $limit = $request->input('limit', 3); + $caption = $request->input('content'); + + $group = Group::findOrFail($gid); + + abort_if(!$group->isMember($pid), 403, 'Not a member of group.'); + abort_if(!GroupService::canComment($gid, $pid), 422, 'You cannot interact with this content at this time'); + $parent = GroupPost::whereGroupId($gid)->findOrFail($sid); + + $status = new GroupComment; + $status->status_id = $parent->id; + $status->group_id = $group->id; + $status->profile_id = $pid; + $status->caption = Purify::clean($caption); + $status->visibility = 'draft'; + $status->is_nsfw = false; + $status->save(); + + $photo = $request->file('photo'); + $storagePath = GroupMediaService::path($group->id, $pid, $status->id); + $storagePath = 'public/g/' . $group->id . '/p/' . $parent->id; + $path = $photo->storePublicly($storagePath); + + $media = new GroupMedia(); + $media->group_id = $group->id; + $media->status_id = $status->id; + $media->profile_id = $request->user()->profile_id; + $media->media_path = $path; + $media->size = $photo->getSize(); + $media->mime = $photo->getMimeType(); + $media->save(); + + ImageResizePipeline::dispatchSync($media); + ImageS3UploadPipeline::dispatchSync($media); + + // $gp = new GroupPost; + // $gp->group_id = $group->id; + // $gp->profile_id = $pid; + // $gp->type = 'reply:photo'; + // $gp->status_id = $status->id; + // $gp->in_reply_to_id = $parent->id; + // $gp->save(); + + // GroupService::log( + // $group->id, + // $pid, + // 'group:comment:created', + // [ + // 'type' => $gp->type, + // 'status_id' => $status->id + // ], + // GroupPost::class, + // $gp->id + // ); + + // todo: perform in job + // $parent->reply_count = Status::whereInReplyToId($parent->id)->count(); + // $parent->save(); + // StatusService::del($parent->id); + // GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id)); + + // delay response while background job optimizes media + // sleep(5); + + // todo: perform in job + $s = GroupCommentService::get($status->group_id, $status->id); + + // $s['pf_type'] = 'text'; + // $s['visibility'] = 'public'; + // $s['url'] = $gp->url(); + + return $s; + } + + public function deleteComment(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'id' => 'required|integer|min:1', + 'gid' => 'required|integer|min:1' + ]); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $group = Group::findOrFail($gid); + abort_if(!$group->isMember($pid), 403, 'Not a member of group.'); + + $gp = GroupComment::whereGroupId($group->id)->findOrFail($request->input('id')); + abort_if($gp->profile_id != $pid && $group->profile_id != $pid, 403); + + $parent = GroupPost::find($gp->status_id); + abort_if(!$parent, 422, 'Invalid parent'); + + DeleteCommentPipeline::dispatch($parent, $gp)->onQueue('groups'); + GroupService::log( + $group->id, + $pid, + 'group:status:deleted', + [ + 'type' => $gp->type, + 'status_id' => $gp->id, + ], + GroupComment::class, + $gp->id + ); + $gp->delete(); + + if($request->wantsJson()) { + return response()->json(['Status successfully deleted.']); + } else { + return redirect('/groups/feed'); + } + } + + public function likePost(Request $request) + { + $this->validate($request, [ + 'gid' => 'required', + 'sid' => 'required' + ]); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $sid = $request->input('sid'); + + $group = GroupService::get($gid); + abort_if(!$group || $gid != $group['id'], 422, 'Invalid group'); + abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time'); + abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group'); + $gp = GroupCommentService::get($gid, $sid); + abort_if(!$gp, 422, 'Invalid status'); + $count = $gp['favourites_count'] ?? 0; + + $like = GroupLike::firstOrCreate([ + 'group_id' => $gid, + 'profile_id' => $pid, + 'comment_id' => $sid, + ]); + + if($like->wasRecentlyCreated) { + // update parent post like count + $parent = GroupComment::find($sid); + abort_if(!$parent || $parent->group_id != $gid, 422, 'Invalid status'); + $parent->likes_count = $parent->likes_count + 1; + $parent->save(); + GroupsLikeService::add($pid, $sid); + // invalidate cache + GroupCommentService::del($gid, $sid); + $count++; + GroupService::log( + $gid, + $pid, + 'group:like', + null, + GroupLike::class, + $like->id + ); + } + + $response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count]; + + return $response; + } + + public function unlikePost(Request $request) + { + $this->validate($request, [ + 'gid' => 'required', + 'sid' => 'required' + ]); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $sid = $request->input('sid'); + + $group = GroupService::get($gid); + abort_if(!$group || $gid != $group['id'], 422, 'Invalid group'); + abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time'); + abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group'); + $gp = GroupCommentService::get($gid, $sid); + abort_if(!$gp, 422, 'Invalid status'); + $count = $gp['favourites_count'] ?? 0; + + $like = GroupLike::where([ + 'group_id' => $gid, + 'profile_id' => $pid, + 'comment_id' => $sid, + ])->first(); + + if($like) { + $like->delete(); + $parent = GroupComment::find($sid); + abort_if(!$parent || $parent->group_id != $gid, 422, 'Invalid status'); + $parent->likes_count = $parent->likes_count - 1; + $parent->save(); + GroupsLikeService::remove($pid, $sid); + // invalidate cache + GroupCommentService::del($gid, $sid); + $count--; + } + + $response = ['code' => 200, 'msg' => 'Unliked post', 'count' => $count]; + + return $response; + } +} diff --git a/app/Http/Controllers/Groups/GroupsDiscoverController.php b/app/Http/Controllers/Groups/GroupsDiscoverController.php new file mode 100644 index 000000000..2194807de --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsDiscoverController.php @@ -0,0 +1,57 @@ +middleware('auth'); + } + + public function getDiscoverPopular(Request $request) + { + abort_if(!$request->user(), 404); + $groups = Group::orderByDesc('member_count') + ->take(12) + ->pluck('id') + ->map(function($id) { + return GroupService::get($id); + }) + ->filter(function($id) { + return $id; + }) + ->take(6) + ->values(); + return $groups; + } + + public function getDiscoverNew(Request $request) + { + abort_if(!$request->user(), 404); + $groups = Group::latest() + ->take(12) + ->pluck('id') + ->map(function($id) { + return GroupService::get($id); + }) + ->filter(function($id) { + return $id; + }) + ->take(6) + ->values(); + return $groups; + } +} diff --git a/app/Http/Controllers/Groups/GroupsFeedController.php b/app/Http/Controllers/Groups/GroupsFeedController.php new file mode 100644 index 000000000..bb04e2487 --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsFeedController.php @@ -0,0 +1,188 @@ +middleware('auth'); + } + + public function getSelfFeed(Request $request) + { + abort_if(!$request->user(), 404); + $pid = $request->user()->profile_id; + $limit = $request->input('limit', 5); + $page = $request->input('page'); + $initial = $request->has('initial'); + + if($initial) { + $res = Cache::remember('groups:self:feed:' . $pid, 900, function() use($pid) { + return $this->getSelfFeedV0($pid, 5, null); + }); + } else { + abort_if($page && $page > 5, 422); + $res = $this->getSelfFeedV0($pid, $limit, $page); + } + + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + protected function getSelfFeedV0($pid, $limit, $page) + { + return GroupPost::join('group_members', 'group_posts.group_id', 'group_members.group_id') + ->select('group_posts.*', 'group_members.group_id', 'group_members.profile_id') + ->where('group_members.profile_id', $pid) + ->whereIn('group_posts.type', ['text', 'photo', 'video']) + ->orderByDesc('group_posts.id') + ->limit($limit) + // ->pluck('group_posts.status_id') + ->simplePaginate($limit) + ->map(function($gp) use($pid) { + $status = GroupPostService::get($gp['group_id'], $gp['id']); + + if(!$status) { + return false; + } + + $status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']); + $status['favourites_count'] = GroupsLikeService::count($gp['id']); + $status['pf_type'] = $gp['type']; + $status['visibility'] = 'public'; + $status['url'] = url("/groups/{$gp['group_id']}/p/{$gp['id']}"); + $status['group'] = GroupService::get($gp['group_id']); + $status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}"); + + return $status; + }); + } + + public function getGroupProfileFeed(Request $request, $id, $pid) + { + abort_if(!$request->user(), 404); + $cid = $request->user()->profile_id; + + $group = Group::findOrFail($id); + abort_if(!$group->isMember($pid), 404); + + $feed = GroupPost::whereGroupId($id) + ->whereProfileId($pid) + ->latest() + ->paginate(3) + ->map(function($gp) use($pid) { + $status = GroupPostService::get($gp['group_id'], $gp['id']); + if(!$status) { + return false; + } + $status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']); + $status['favourites_count'] = GroupsLikeService::count($gp['id']); + $status['pf_type'] = $gp['type']; + $status['visibility'] = 'public'; + $status['url'] = $gp->url(); + + // if($gp['type'] == 'poll') { + // $status['poll'] = PollService::get($status['id']); + // } + + $status['account']['url'] = "/groups/{$gp['group_id']}/user/{$status['account']['id']}"; + + return $status; + }) + ->filter(function($status) { + return $status; + }); + + return $feed; + } + + public function getGroupFeed(Request $request, $id) + { + $group = Group::findOrFail($id); + $user = $request->user(); + $pid = optional($user)->profile_id ?? false; + abort_if(!$group->isMember($pid), 404); + $max = $request->input('max_id'); + $limit = $request->limit ?? 3; + $filtered = $user ? UserFilterService::filters($user->profile_id) : []; + + // $posts = GroupPost::whereGroupId($group->id) + // ->when($maxId, function($q, $maxId) { + // return $q->where('status_id', '<', $maxId); + // }) + // ->whereNull('in_reply_to_id') + // ->orderByDesc('status_id') + // ->simplePaginate($limit) + // ->map(function($gp) use($pid) { + // $status = StatusService::get($gp['status_id'], false); + // if(!$status) { + // return false; + // } + // $status['favourited'] = (bool) LikeService::liked($pid, $gp['status_id']); + // $status['favourites_count'] = LikeService::count($gp['status_id']); + // $status['pf_type'] = $gp['type']; + // $status['visibility'] = 'public'; + // $status['url'] = $gp->url(); + + // if($gp['type'] == 'poll') { + // $status['poll'] = PollService::get($status['id']); + // } + + // $status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}"); + + // return $status; + // })->filter(function($status) { + // return $status; + // }); + // return $posts; + + Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() use($id) { + if(GroupFeedService::count($id) == 0) { + GroupFeedService::warmCache($id, true, 400); + } + }); + + if ($max) { + $feed = GroupFeedService::getRankedMaxId($id, $max, $limit); + } else { + $feed = GroupFeedService::get($id, 0, $limit); + } + + $res = collect($feed) + ->map(function($k) use($user, $id) { + $status = GroupPostService::get($id, $k); + if($status && $user) { + $pid = $user->profile_id; + $sid = $status['account']['id']; + $status['favourited'] = (bool) GroupsLikeService::liked($pid, $status['id']); + $status['favourites_count'] = GroupsLikeService::count($status['id']); + $status['relationship'] = $pid == $sid ? [] : RelationshipService::get($pid, $sid); + } + return $status; + }) + ->filter(function($s) use($filtered) { + return $s && in_array($s['account']['id'], $filtered) == false; + }) + ->values() + ->toArray(); + + return $res; + } +} diff --git a/app/Http/Controllers/Groups/GroupsMemberController.php b/app/Http/Controllers/Groups/GroupsMemberController.php new file mode 100644 index 000000000..3bfe086a2 --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsMemberController.php @@ -0,0 +1,214 @@ +validate($request, [ + 'gid' => 'required', + 'limit' => 'nullable|integer|min:3|max:10' + ]); + + abort_if(!$request->user(), 404); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $group = Group::findOrFail($gid); + + abort_if(!$group->isMember($pid), 403, 'Not a member of group.'); + + $members = GroupMember::whereGroupId($gid) + ->whereJoinRequest(false) + ->simplePaginate(10) + ->map(function($member) use($pid) { + $account = AccountService::get($member['profile_id']); + $account['role'] = $member['role']; + $account['joined'] = $member['created_at']; + $account['following'] = $pid != $member['profile_id'] ? + FollowerService::follows($pid, $member['profile_id']) : + null; + $account['url'] = url("/groups/{$member->group_id}/user/{$member['profile_id']}"); + return $account; + }); + + return response()->json($members->toArray(), 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function getGroupMemberJoinRequests(Request $request) + { + abort_if(!$request->user(), 404); + $id = $request->input('gid'); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + return GroupMember::whereGroupId($group->id) + ->whereJoinRequest(true) + ->whereNull('rejected_at') + ->paginate(10) + ->map(function($member) { + return AccountService::get($member->profile_id); + }); + } + + public function handleGroupMemberJoinRequest(Request $request) + { + abort_if(!$request->user(), 404); + $id = $request->input('gid'); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + $mid = $request->input('pid'); + abort_if($group->isMember($mid), 404); + + $this->validate($request, [ + 'gid' => 'required', + 'pid' => 'required', + 'action' => 'required|in:approve,reject' + ]); + + $action = $request->input('action'); + + $member = GroupMember::whereGroupId($group->id) + ->whereProfileId($mid) + ->firstOrFail(); + + if($action == 'approve') { + MemberJoinApprovedPipeline::dispatch($member)->onQueue('groups'); + } else if ($action == 'reject') { + MemberJoinRejectedPipeline::dispatch($member)->onQueue('groups'); + } + + return $request->all(); + } + + public function getGroupMember(Request $request) + { + $this->validate($request, [ + 'gid' => 'required', + 'pid' => 'required' + ]); + + abort_if(!$request->user(), 404); + $gid = $request->input('gid'); + $group = Group::findOrFail($gid); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $member_id = $request->input('pid'); + $member = GroupMember::whereGroupId($gid) + ->whereProfileId($member_id) + ->firstOrFail(); + + $account = GroupAccountService::get($group->id, $member['profile_id']); + $account['role'] = $member['role']; + $account['joined'] = $member['created_at']; + $account['following'] = $pid != $member['profile_id'] ? + FollowerService::follows($pid, $member['profile_id']) : + null; + $account['url'] = url("/groups/{$gid}/user/{$member_id}"); + + return response()->json($account, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function getGroupMemberCommonIntersections(Request $request) + { + abort_if(!$request->user(), 404); + $cid = $request->user()->profile_id; + + // $this->validate($request, [ + // 'gid' => 'required', + // 'pid' => 'required' + // ]); + + $gid = $request->input('gid'); + $pid = $request->input('pid'); + + if($pid === $cid) { + return []; + } + + $group = Group::findOrFail($gid); + abort_if(!$group->isMember($cid), 404); + abort_if(!$group->isMember($pid), 404); + + $self = GroupPostHashtag::selectRaw('group_post_hashtags.*, count(*) as countr') + ->whereProfileId($cid) + ->groupBy('hashtag_id') + ->orderByDesc('countr') + ->take(20) + ->pluck('hashtag_id'); + $user = GroupPostHashtag::selectRaw('group_post_hashtags.*, count(*) as countr') + ->whereProfileId($pid) + ->groupBy('hashtag_id') + ->orderByDesc('countr') + ->take(20) + ->pluck('hashtag_id'); + + $topics = $self->intersect($user) + ->values() + ->shuffle() + ->take(3) + ->map(function($id) use($group) { + $tag = GroupHashtagService::get($id); + $tag['url'] = url("/groups/{$group->id}/topics/{$tag['slug']}?src=upt"); + return $tag; + }); + + // $friends = DB::table('followers as u') + // ->join('followers as s', 'u.following_id', '=', 's.following_id') + // ->where('s.profile_id', $cid) + // ->where('u.profile_id', $pid) + // ->inRandomOrder() + // ->take(10) + // ->pluck('s.following_id') + // ->map(function($id) use($gid) { + // $res = AccountService::get($id); + // $res['url'] = url("/groups/{$gid}/user/{$id}"); + // return $res; + // }); + $mutualGroups = GroupService::mutualGroups($cid, $pid, [$gid]); + + $mutualFriends = collect(FollowerService::mutualIds($cid, $pid)) + ->map(function($id) use($gid) { + $res = AccountService::get($id); + if(GroupService::isMember($gid, $id)) { + $res['url'] = url("/groups/{$gid}/user/{$id}"); + } else if(!$res['local']) { + $res['url'] = url("/i/web/profile/_/{$id}"); + } + return $res; + }); + $mutualFriendsCount = FollowerService::mutualCount($cid, $pid); + + $res = [ + 'groups_count' => $mutualGroups['count'], + 'groups' => $mutualGroups['groups'], + 'topics' => $topics, + 'friends_count' => $mutualFriendsCount, + 'friends' => $mutualFriends, + ]; + + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } +} diff --git a/app/Http/Controllers/Groups/GroupsMetaController.php b/app/Http/Controllers/Groups/GroupsMetaController.php new file mode 100644 index 000000000..bc1e58b33 --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsMetaController.php @@ -0,0 +1,31 @@ +middleware('auth'); + } + + public function deleteGroup(Request $request) + { + abort_if(!$request->user(), 404); + $id = $request->input('gid'); + $group = Group::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!$group->isMember($pid), 404); + abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404); + + $group->status = "delete"; + $group->save(); + GroupService::del($group->id); + return [200]; + } +} diff --git a/app/Http/Controllers/Groups/GroupsNotificationsController.php b/app/Http/Controllers/Groups/GroupsNotificationsController.php new file mode 100644 index 000000000..dafc6c821 --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsNotificationsController.php @@ -0,0 +1,55 @@ +middleware('auth'); + } + + public function selfGlobalNotifications(Request $request) + { + abort_if(!$request->user(), 404); + $pid = $request->user()->profile_id; + + $res = Notification::whereProfileId($pid) + ->where('action', 'like', 'group%') + ->latest() + ->paginate(10) + ->map(function($n) { + $res = [ + 'id' => $n->id, + 'type' => $n->action, + 'account' => AccountService::get($n->actor_id), + 'object' => [ + 'id' => $n->item_id, + 'type' => last(explode('\\', $n->item_type)), + ], + 'created_at' => $n->created_at->format('c') + ]; + + if($res['object']['type'] == 'Status' || in_array($n->action, ['group:comment'])) { + $res['status'] = StatusService::get($n->item_id, false); + $res['group'] = GroupService::get($res['status']['gid']); + } + + if($res['object']['type'] == 'Group') { + $res['group'] = GroupService::get($n->item_id); + } + + return $res; + }); + + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } +} diff --git a/app/Http/Controllers/Groups/GroupsPostController.php b/app/Http/Controllers/Groups/GroupsPostController.php new file mode 100644 index 000000000..11b4799fe --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsPostController.php @@ -0,0 +1,420 @@ +middleware('auth'); + } + + public function storePost(Request $request) + { + $this->validate($request, [ + 'group_id' => 'required|exists:groups,id', + 'caption' => 'sometimes|string|max:'.config_cache('pixelfed.max_caption_length', 500), + 'pollOptions' => 'sometimes|array|min:1|max:4' + ]); + + $group = Group::findOrFail($request->input('group_id')); + $pid = $request->user()->profile_id; + $caption = $request->input('caption'); + $type = $request->input('type', 'text'); + + abort_if(!GroupService::canPost($group->id, $pid), 422, 'You cannot create new posts at this time'); + + if($type == 'text') { + abort_if(strlen(e($caption)) == 0, 403); + } + + $gp = new GroupPost; + $gp->group_id = $group->id; + $gp->profile_id = $pid; + $gp->caption = e($caption); + $gp->type = $type; + $gp->visibility = 'draft'; + $gp->save(); + + $status = $gp; + + NewPostPipeline::dispatchSync($gp); + + // NewStatusPipeline::dispatch($status, $gp); + + if($type == 'poll') { + // Polls not supported yet + // $poll = new Poll; + // $poll->status_id = $status->id; + // $poll->profile_id = $status->profile_id; + // $poll->poll_options = $request->input('pollOptions'); + // $poll->expires_at = now()->addMinutes($request->input('expiry')); + // $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { + // return 0; + // })->toArray(); + // $poll->save(); + // sleep(5); + } + if($type == 'photo') { + $photo = $request->file('photo'); + $storagePath = GroupMediaService::path($group->id, $pid, $status->id); + // $storagePath = 'public/g/' . $group->id . '/p/' . $status->id; + $path = $photo->storePublicly($storagePath); + // $hash = \hash_file('sha256', $photo); + + $media = new GroupMedia(); + $media->group_id = $group->id; + $media->status_id = $status->id; + $media->profile_id = $request->user()->profile_id; + $media->media_path = $path; + $media->size = $photo->getSize(); + $media->mime = $photo->getMimeType(); + $media->save(); + + // Bus::chain([ + // new ImageResizePipeline($media), + // new ImageS3UploadPipeline($media), + // ])->dispatch($media); + + ImageResizePipeline::dispatchSync($media); + ImageS3UploadPipeline::dispatchSync($media); + // ImageOptimize::dispatch($media); + // delay response while background job optimizes media + // sleep(5); + } + if($type == 'video') { + $video = $request->file('video'); + $storagePath = 'public/g/' . $group->id . '/p/' . $status->id; + $path = $video->storePublicly($storagePath); + $hash = \hash_file('sha256', $video); + + $media = new Media(); + $media->status_id = $status->id; + $media->profile_id = $request->user()->profile_id; + $media->user_id = $request->user()->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $video->getSize(); + $media->mime = $video->getMimeType(); + $media->save(); + + VideoThumbnail::dispatch($media); + sleep(15); + } + + GroupService::log( + $group->id, + $pid, + 'group:status:created', + [ + 'type' => $gp->type, + 'status_id' => $status->id + ], + GroupPost::class, + $gp->id + ); + + $s = GroupPostService::get($status->group_id, $status->id); + GroupFeedService::add($group->id, $gp->id); + Cache::forget('groups:self:feed:' . $pid); + + $s['pf_type'] = $type; + $s['visibility'] = 'public'; + $s['url'] = $gp->url(); + + if($type == 'poll') { + $s['poll'] = PollService::get($status->id); + } + + $group->last_active_at = now(); + $group->save(); + + return $s; + } + + public function deletePost(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'id' => 'required|integer|min:1', + 'gid' => 'required|integer|min:1' + ]); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $group = Group::findOrFail($gid); + abort_if(!$group->isMember($pid), 403, 'Not a member of group.'); + + $gp = GroupPost::whereGroupId($status->group_id)->findOrFail($request->input('id')); + abort_if($gp->profile_id != $pid && $group->profile_id != $pid, 403); + $cached = GroupPostService::get($status->group_id, $status->id); + + if($cached) { + $cached = collect($cached)->filter(function($r, $k) { + return in_array($k, [ + 'id', + 'sensitive', + 'pf_type', + 'media_attachments', + 'content_text', + 'created_at' + ]); + }); + } + + GroupService::log( + $status->group_id, + $request->user()->profile_id, + 'group:status:deleted', + [ + 'type' => $gp->type, + 'status_id' => $status->id, + 'original' => $cached + ], + GroupPost::class, + $gp->id + ); + + $user = $request->user(); + + // if($status->profile_id != $user->profile->id && + // $user->is_admin == true && + // $status->uri == null + // ) { + // $media = $status->media; + + // $ai = new AccountInterstitial; + // $ai->user_id = $status->profile->user_id; + // $ai->type = 'post.removed'; + // $ai->view = 'account.moderation.post.removed'; + // $ai->item_type = 'App\Status'; + // $ai->item_id = $status->id; + // $ai->has_media = (bool) $media->count(); + // $ai->blurhash = $media->count() ? $media->first()->blurhash : null; + // $ai->meta = json_encode([ + // 'caption' => $status->caption, + // 'created_at' => $status->created_at, + // 'type' => $status->type, + // 'url' => $status->url(), + // 'is_nsfw' => $status->is_nsfw, + // 'scope' => $status->scope, + // 'reblog' => $status->reblog_of_id, + // 'likes_count' => $status->likes_count, + // 'reblogs_count' => $status->reblogs_count, + // ]); + // $ai->save(); + + // $u = $status->profile->user; + // $u->has_interstitial = true; + // $u->save(); + // } + + if($status->in_reply_to_id) { + $parent = GroupPost::find($status->in_reply_to_id); + if($parent) { + $parent->reply_count = GroupPost::whereInReplyToId($parent->id)->count(); + $parent->save(); + GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id)); + } + } + + GroupPostService::del($group->id, $gp->id); + GroupFeedService::del($group->id, $gp->id); + if ($status->profile_id == $user->profile->id || $user->is_admin == true) { + // Cache::forget('profile:status_count:'.$status->profile_id); + StatusDelete::dispatch($status); + } + + if($request->wantsJson()) { + return response()->json(['Status successfully deleted.']); + } else { + return redirect($user->url()); + } + } + + public function likePost(Request $request) + { + $this->validate($request, [ + 'gid' => 'required', + 'sid' => 'required' + ]); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $sid = $request->input('sid'); + + $group = GroupService::get($gid); + abort_if(!$group, 422, 'Invalid group'); + abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time'); + abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group'); + $gp = GroupPostService::get($gid, $sid); + abort_if(!$gp, 422, 'Invalid status'); + $count = $gp['favourites_count'] ?? 0; + + $like = GroupLike::firstOrCreate([ + 'group_id' => $gid, + 'profile_id' => $pid, + 'status_id' => $sid, + ]); + + if($like->wasRecentlyCreated) { + // update parent post like count + $parent = GroupPost::whereGroupId($gid)->find($sid); + abort_if(!$parent, 422, 'Invalid status'); + $parent->likes_count = $parent->likes_count + 1; + $parent->save(); + GroupsLikeService::add($pid, $sid); + // invalidate cache + GroupPostService::del($gid, $sid); + $count++; + GroupService::log( + $gid, + $pid, + 'group:like', + null, + GroupLike::class, + $like->id + ); + } + // if (GroupLike::whereGroupId($gid)->whereStatusId($sid)->whereProfileId($pid)->exists()) { + // $like = GroupLike::whereProfileId($pid)->whereStatusId($sid)->firstOrFail(); + // // UnlikePipeline::dispatch($like); + // $count = $gp->likes_count - 1; + // $action = 'group:unlike'; + // } else { + // $count = $gp->likes_count; + // $like = GroupLike::firstOrCreate([ + // 'group_id' => $gid, + // 'profile_id' => $pid, + // 'status_id' => $sid + // ]); + // if($like->wasRecentlyCreated == true) { + // $count++; + // $gp->likes_count = $count; + // $like->save(); + // $gp->save(); + // // LikePipeline::dispatch($like); + // $action = 'group:like'; + // } + // } + + + // Cache::forget('status:'.$status->id.':likedby:userid:'.$request->user()->id); + // StatusService::del($status->id); + + $response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count]; + + return $response; + } + + public function unlikePost(Request $request) + { + $this->validate($request, [ + 'gid' => 'required', + 'sid' => 'required' + ]); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $sid = $request->input('sid'); + + $group = GroupService::get($gid); + abort_if(!$group, 422, 'Invalid group'); + abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time'); + abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group'); + $gp = GroupPostService::get($gid, $sid); + abort_if(!$gp, 422, 'Invalid status'); + $count = $gp['favourites_count'] ?? 0; + + $like = GroupLike::where([ + 'group_id' => $gid, + 'profile_id' => $pid, + 'status_id' => $sid, + ])->first(); + + if($like) { + $like->delete(); + $parent = GroupPost::whereGroupId($gid)->find($sid); + abort_if(!$parent, 422, 'Invalid status'); + $parent->likes_count = $parent->likes_count - 1; + $parent->save(); + GroupsLikeService::remove($pid, $sid); + // invalidate cache + GroupPostService::del($gid, $sid); + $count--; + } + + $response = ['code' => 200, 'msg' => 'Unliked post', 'count' => $count]; + + return $response; + } + + public function getGroupMedia(Request $request) + { + $this->validate($request, [ + 'gid' => 'required', + 'type' => 'required|in:photo,video' + ]); + + abort_if(!$request->user(), 404); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $type = $request->input('type'); + $group = Group::findOrFail($gid); + + abort_if(!$group->isMember($pid), 403, 'Not a member of group.'); + + $media = GroupPost::whereGroupId($gid) + ->whereType($type) + ->latest() + ->simplePaginate(20) + ->map(function($gp) use($pid) { + $status = GroupPostService::get($gp['group_id'], $gp['id']); + if(!$status) { + return false; + } + $status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']); + $status['favourites_count'] = GroupsLikeService::count($gp['id']); + $status['pf_type'] = $gp['type']; + $status['visibility'] = 'public'; + $status['url'] = $gp->url(); + + // if($gp['type'] == 'poll') { + // $status['poll'] = PollService::get($status['id']); + // } + + return $status; + })->filter(function($status) { + return $status; + }); + + return response()->json($media->toArray(), 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } +} diff --git a/app/Http/Controllers/Groups/GroupsSearchController.php b/app/Http/Controllers/Groups/GroupsSearchController.php new file mode 100644 index 000000000..560436f46 --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsSearchController.php @@ -0,0 +1,221 @@ +middleware('auth'); + } + + public function inviteFriendsToGroup(Request $request) + { + abort_if(!$request->user(), 404); + $this->validate($request, [ + 'uids' => 'required', + 'g' => 'required', + ]); + $uid = $request->input('uids'); + $gid = $request->input('g'); + $pid = $request->user()->profile_id; + $group = Group::findOrFail($gid); + abort_if(!$group->isMember($pid), 404); + abort_if( + GroupInvitation::whereGroupId($group->id) + ->whereFromProfileId($pid) + ->count() >= 20, + 422, + 'Invite limit reached' + ); + + $profiles = collect($uid) + ->map(function($u) { + return Profile::find($u); + }) + ->filter(function($u) use($pid) { + return $u && + $u->id != $pid && + isset($u->id) && + Follower::whereFollowingId($pid) + ->whereProfileId($u->id) + ->exists(); + }) + ->filter(function($u) use($group, $pid) { + return GroupInvitation::whereGroupId($group->id) + ->whereFromProfileId($pid) + ->whereToProfileId($u->id) + ->exists() == false; + }) + ->each(function($u) use($gid, $pid) { + $gi = new GroupInvitation; + $gi->group_id = $gid; + $gi->from_profile_id = $pid; + $gi->to_profile_id = $u->id; + $gi->to_local = true; + $gi->from_local = $u->domain == null; + $gi->save(); + // GroupMemberInvite::dispatch($gi); + }); + return [200]; + } + + public function searchFriendsToInvite(Request $request) + { + abort_if(!$request->user(), 404); + $this->validate($request, [ + 'q' => 'required|min:2|max:40', + 'g' => 'required', + 'v' => 'required|in:0.2' + ]); + $q = $request->input('q'); + $gid = $request->input('g'); + $pid = $request->user()->profile_id; + $group = Group::findOrFail($gid); + abort_if(!$group->isMember($pid), 404); + + $res = Profile::where('username', 'like', "%{$q}%") + ->whereNull('profiles.domain') + ->join('followers', 'profiles.id', '=', 'followers.profile_id') + ->where('followers.following_id', $pid) + ->take(10) + ->get() + ->filter(function($p) use($group) { + return $group->isMember($p->profile_id) == false; + }) + ->filter(function($p) use($group, $pid) { + return GroupInvitation::whereGroupId($group->id) + ->whereFromProfileId($pid) + ->whereToProfileId($p->profile_id) + ->exists() == false; + }) + ->map(function($gm) use ($gid) { + $a = AccountService::get($gm->profile_id); + return [ + 'id' => (string) $gm->profile_id, + 'username' => $a['acct'], + 'url' => url("/groups/{$gid}/user/{$a['id']}?rf=group_search") + ]; + }) + ->values(); + + return $res; + } + + public function searchGlobalResults(Request $request) + { + abort_if(!$request->user(), 404); + $this->validate($request, [ + 'q' => 'required|min:2|max:140', + 'v' => 'required|in:0.2' + ]); + $q = $request->input('q'); + + if(str_starts_with($q, 'https://')) { + $res = Helpers::getSignedFetch($q); + if($res && $res = json_decode($res, true)) { + + } + if($res && isset($res['type']) && in_array($res['type'], ['Group', 'Note', 'Page'])) { + if($res['type'] === 'Group') { + return GroupActivityPubService::fetchGroup($q, true); + } + $resp = GroupActivityPubService::fetchGroupPost($q, true); + $resp['name'] = 'Group Post'; + $resp['url'] = '/groups/' . $resp['group_id'] . '/p/' . $resp['id']; + return [$resp]; + } + } + return Group::whereNull('status') + ->where('name', 'like', '%' . $q . '%') + ->orderBy('id') + ->take(10) + ->pluck('id') + ->map(function($group) { + return GroupService::get($group); + }); + } + + public function searchLocalAutocomplete(Request $request) + { + abort_if(!$request->user(), 404); + $this->validate($request, [ + 'q' => 'required|min:2|max:40', + 'g' => 'required', + 'v' => 'required|in:0.2' + ]); + $q = $request->input('q'); + $gid = $request->input('g'); + $pid = $request->user()->profile_id; + $group = Group::findOrFail($gid); + abort_if(!$group->isMember($pid), 404); + + $res = GroupMember::whereGroupId($gid) + ->join('profiles', 'group_members.profile_id', '=', 'profiles.id') + ->where('profiles.username', 'like', "%{$q}%") + ->take(10) + ->get() + ->map(function($gm) use ($gid) { + $a = AccountService::get($gm->profile_id); + return [ + 'username' => $a['username'], + 'url' => url("/groups/{$gid}/user/{$a['id']}?rf=group_search") + ]; + }); + return $res; + } + + public function searchAddRecent(Request $request) + { + $this->validate($request, [ + 'q' => 'required|min:2|max:40', + 'g' => 'required', + ]); + $q = $request->input('q'); + $gid = $request->input('g'); + $pid = $request->user()->profile_id; + $group = Group::findOrFail($gid); + abort_if(!$group->isMember($pid), 404); + + $key = 'groups:search:recent:'.$gid.':pid:'.$pid; + $ttl = now()->addDays(14); + $res = Cache::get($key); + if(!$res) { + $val = json_encode([$q]); + } else { + $ex = collect(json_decode($res)) + ->prepend($q) + ->unique('value') + ->slice(0, 3) + ->values() + ->all(); + $val = json_encode($ex); + } + Cache::put($key, $val, $ttl); + return 200; + } + + public function searchGetRecent(Request $request) + { + $gid = $request->input('g'); + $pid = $request->user()->profile_id; + $group = Group::findOrFail($gid); + abort_if(!$group->isMember($pid), 404); + $key = 'groups:search:recent:'.$gid.':pid:'.$pid; + return Cache::get($key); + } +} diff --git a/app/Http/Controllers/Groups/GroupsTopicController.php b/app/Http/Controllers/Groups/GroupsTopicController.php new file mode 100644 index 000000000..c3d8ecda7 --- /dev/null +++ b/app/Http/Controllers/Groups/GroupsTopicController.php @@ -0,0 +1,133 @@ +middleware('auth'); + } + + public function groupTopics(Request $request) + { + $this->validate($request, [ + 'gid' => 'required', + ]); + + abort_if(!$request->user(), 404); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $group = Group::findOrFail($gid); + + abort_if(!$group->isMember($pid), 403, 'Not a member of group.'); + + $posts = GroupPostHashtag::join('group_hashtags', 'group_hashtags.id', '=', 'group_post_hashtags.hashtag_id') + ->selectRaw('group_hashtags.*, group_post_hashtags.*, count(group_post_hashtags.hashtag_id) as ht_count') + ->where('group_post_hashtags.group_id', $gid) + ->orderByDesc('ht_count') + ->limit(10) + ->pluck('group_post_hashtags.hashtag_id', 'ht_count') + ->map(function($id, $key) use ($gid) { + $tag = GroupHashtag::find($id); + return [ + 'hid' => $id, + 'name' => $tag->name, + 'url' => url("/groups/{$gid}/topics/{$tag->slug}"), + 'count' => $key + ]; + })->values(); + + return response()->json($posts, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function groupTopicTag(Request $request) + { + $this->validate($request, [ + 'gid' => 'required', + 'name' => 'required' + ]); + + abort_if(!$request->user(), 404); + + $pid = $request->user()->profile_id; + $gid = $request->input('gid'); + $limit = $request->input('limit', 3); + $group = Group::findOrFail($gid); + + abort_if(!$group->isMember($pid), 403, 'Not a member of group.'); + + $name = $request->input('name'); + $hashtag = GroupHashtag::whereName($name)->first(); + + if(!$hashtag) { + return []; + } + + // $posts = GroupPost::whereGroupId($gid) + // ->select('status_hashtags.*', 'group_posts.*') + // ->where('status_hashtags.hashtag_id', $hashtag->id) + // ->join('status_hashtags', 'group_posts.status_id', '=', 'status_hashtags.status_id') + // ->orderByDesc('group_posts.status_id') + // ->simplePaginate($limit) + // ->map(function($gp) use($pid) { + // $status = StatusService::get($gp['status_id'], false); + // if(!$status) { + // return false; + // } + // $status['favourited'] = (bool) LikeService::liked($pid, $gp['status_id']); + // $status['favourites_count'] = LikeService::count($gp['status_id']); + // $status['pf_type'] = $gp['type']; + // $status['visibility'] = 'public'; + // $status['url'] = $gp->url(); + // return $status; + // }); + + $posts = GroupPostHashtag::whereGroupId($gid) + ->whereHashtagId($hashtag->id) + ->orderByDesc('id') + ->simplePaginate($limit) + ->map(function($gp) use($pid) { + $status = GroupPostService::get($gp['group_id'], $gp['status_id']); + if(!$status) { + return false; + } + $status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['status_id']); + $status['favourites_count'] = GroupsLikeService::count($gp['status_id']); + $status['pf_type'] = $status['pf_type']; + $status['visibility'] = 'public'; + return $status; + }); + + return response()->json($posts, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function showTopicFeed(Request $request, $gid, $tag) + { + abort_if(!$request->user(), 404); + + $pid = $request->user()->profile_id; + $group = Group::findOrFail($gid); + $gid = $group->id; + abort_if(!$group->isMember($pid), 403, 'Not a member of group.'); + return view('groups.topic-feed', compact('gid', 'tag')); + } +} diff --git a/app/Http/Controllers/Import/Instagram.php b/app/Http/Controllers/Import/Instagram.php index 95d290f61..f1b886d52 100644 --- a/app/Http/Controllers/Import/Instagram.php +++ b/app/Http/Controllers/Import/Instagram.php @@ -17,7 +17,7 @@ trait Instagram { public function instagram() { - if(config_cache('pixelfed.import.instagram.enabled') != true) { + if((bool) config_cache('pixelfed.import.instagram.enabled') != true) { abort(404, 'Feature not enabled'); } return view('settings.import.instagram.home'); @@ -25,6 +25,9 @@ trait Instagram public function instagramStart(Request $request) { + if((bool) config_cache('pixelfed.import.instagram.enabled') != true) { + abort(404, 'Feature not enabled'); + } $completed = ImportJob::whereProfileId(Auth::user()->profile->id) ->whereService('instagram') ->whereNotNull('completed_at') @@ -38,6 +41,9 @@ trait Instagram protected function instagramRedirectOrNew() { + if((bool) config_cache('pixelfed.import.instagram.enabled') != true) { + abort(404, 'Feature not enabled'); + } $profile = Auth::user()->profile; $exists = ImportJob::whereProfileId($profile->id) ->whereService('instagram') @@ -61,6 +67,9 @@ trait Instagram public function instagramStepOne(Request $request, $uuid) { + if((bool) config_cache('pixelfed.import.instagram.enabled') != true) { + abort(404, 'Feature not enabled'); + } $profile = Auth::user()->profile; $job = ImportJob::whereProfileId($profile->id) ->whereNull('completed_at') @@ -72,6 +81,9 @@ trait Instagram public function instagramStepOneStore(Request $request, $uuid) { + if((bool) config_cache('pixelfed.import.instagram.enabled') != true) { + abort(404, 'Feature not enabled'); + } $max = 'max:' . config('pixelfed.import.instagram.limits.size'); $this->validate($request, [ 'media.*' => 'required|mimes:bin,jpeg,png,gif|'.$max, @@ -114,6 +126,9 @@ trait Instagram public function instagramStepTwo(Request $request, $uuid) { + if((bool) config_cache('pixelfed.import.instagram.enabled') != true) { + abort(404, 'Feature not enabled'); + } $profile = Auth::user()->profile; $job = ImportJob::whereProfileId($profile->id) ->whereNull('completed_at') @@ -125,6 +140,9 @@ trait Instagram public function instagramStepTwoStore(Request $request, $uuid) { + if((bool) config_cache('pixelfed.import.instagram.enabled') != true) { + abort(404, 'Feature not enabled'); + } $this->validate($request, [ 'media' => 'required|file|max:1000' ]); @@ -150,6 +168,9 @@ trait Instagram public function instagramStepThree(Request $request, $uuid) { + if((bool) config_cache('pixelfed.import.instagram.enabled') != true) { + abort(404, 'Feature not enabled'); + } $profile = Auth::user()->profile; $job = ImportJob::whereProfileId($profile->id) ->whereService('instagram') @@ -162,6 +183,9 @@ trait Instagram public function instagramStepThreeStore(Request $request, $uuid) { + if((bool) config_cache('pixelfed.import.instagram.enabled') != true) { + abort(404, 'Feature not enabled'); + } $profile = Auth::user()->profile; try { diff --git a/app/Http/Controllers/ImportPostController.php b/app/Http/Controllers/ImportPostController.php index e814c2b3a..84f230622 100644 --- a/app/Http/Controllers/ImportPostController.php +++ b/app/Http/Controllers/ImportPostController.php @@ -83,6 +83,17 @@ class ImportPostController extends Controller ); } + public function formatHashtags($val = false) + { + if(!$val || !strlen($val)) { + return null; + } + + $groupedHashtagRegex = '/#\w+(?=#)/'; + + return preg_replace($groupedHashtagRegex, '$0 ', $val); + } + public function store(Request $request) { abort_unless(config('import.instagram.enabled'), 404); @@ -128,11 +139,11 @@ class ImportPostController extends Controller $ip->media = $c->map(function($m) { return [ 'uri' => $m['uri'], - 'title' => $m['title'], + 'title' => $this->formatHashtags($m['title']), 'creation_timestamp' => $m['creation_timestamp'] ]; })->toArray(); - $ip->caption = $c->count() > 1 ? $file['title'] : $ip->media[0]['title']; + $ip->caption = $c->count() > 1 ? $this->formatHashtags($file['title']) : $this->formatHashtags($ip->media[0]['title']); $ip->filename = last(explode('/', $ip->media[0]['uri'])); $ip->metadata = $c->map(function($m) { return [ @@ -168,7 +179,7 @@ class ImportPostController extends Controller 'required', 'file', $mimes, - 'max:' . config('pixelfed.max_photo_size') + 'max:' . config_cache('pixelfed.max_photo_size') ] ]); diff --git a/app/Http/Controllers/InternalApiController.php b/app/Http/Controllers/InternalApiController.php index 299c9ceb6..e2795d6fc 100644 --- a/app/Http/Controllers/InternalApiController.php +++ b/app/Http/Controllers/InternalApiController.php @@ -2,442 +2,424 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use App\{ - AccountInterstitial, - Bookmark, - DirectMessage, - DiscoverCategory, - Hashtag, - Follower, - Like, - Media, - MediaTag, - Notification, - Profile, - StatusHashtag, - Status, - User, - UserFilter, -}; -use Auth,Cache; -use Illuminate\Support\Facades\Redis; -use Carbon\Carbon; -use League\Fractal; -use App\Transformer\Api\{ - AccountTransformer, - StatusTransformer, - // StatusMediaContainerTransformer, -}; -use App\Util\Media\Filter; -use App\Jobs\StatusPipeline\NewStatusPipeline; +use App\AccountInterstitial; +use App\Bookmark; +use App\DirectMessage; +use App\DiscoverCategory; +use App\Follower; use App\Jobs\ModPipeline\HandleSpammerPipeline; -use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; -use Illuminate\Validation\Rule; -use Illuminate\Support\Str; -use App\Services\MediaTagService; +use App\Profile; +use App\Services\BookmarkService; +use App\Services\DiscoverService; use App\Services\ModLogService; use App\Services\PublicTimelineService; -use App\Services\SnowflakeService; use App\Services\StatusService; use App\Services\UserFilterService; -use App\Services\DiscoverService; -use App\Services\BookmarkService; +use App\Status; // StatusMediaContainerTransformer, +use App\Transformer\Api\StatusTransformer; +use App\User; +use Auth; +use Cache; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Redis; +use Illuminate\Validation\Rule; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; class InternalApiController extends Controller { - protected $fractal; + protected $fractal; - public function __construct() - { - $this->middleware('auth'); - $this->fractal = new Fractal\Manager(); - $this->fractal->setSerializer(new ArraySerializer()); - } + public function __construct() + { + $this->middleware('auth'); + $this->fractal = new Fractal\Manager; + $this->fractal->setSerializer(new ArraySerializer); + } - // deprecated v2 compose api - public function compose(Request $request) - { - return redirect('/'); - } + // deprecated v2 compose api + public function compose(Request $request) + { + return redirect('/'); + } - // deprecated - public function discover(Request $request) - { - return; - } + // deprecated + public function discover(Request $request) {} - public function discoverPosts(Request $request) - { - $pid = $request->user()->profile_id; - $filters = UserFilterService::filters($pid); - $forYou = DiscoverService::getForYou(); - $posts = $forYou->take(50)->map(function($post) { - return StatusService::get($post); - }) - ->filter(function($post) use($filters) { - return $post && - isset($post['account']) && - isset($post['account']['id']) && - !in_array($post['account']['id'], $filters); - }) - ->take(12) - ->values(); - return response()->json(compact('posts')); - } + public function discoverPosts(Request $request) + { + $pid = $request->user()->profile_id; + $filters = UserFilterService::filters($pid); + $forYou = DiscoverService::getForYou(); + $posts = $forYou->take(50)->map(function ($post) { + return StatusService::get($post); + }) + ->filter(function ($post) use ($filters) { + return $post && + isset($post['account']) && + isset($post['account']['id']) && + ! in_array($post['account']['id'], $filters); + }) + ->take(12) + ->values(); - public function directMessage(Request $request, $profileId, $threadId) - { - $profile = Auth::user()->profile; + return response()->json(compact('posts')); + } - if($profileId != $profile->id) { - abort(403); - } + public function directMessage(Request $request, $profileId, $threadId) + { + $profile = Auth::user()->profile; - $msg = DirectMessage::whereToId($profile->id) - ->orWhere('from_id',$profile->id) - ->findOrFail($threadId); + if ($profileId != $profile->id) { + abort(403); + } - $thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id]) - ->whereIn('from_id', [$profile->id,$msg->from_id]) - ->orderBy('created_at', 'asc') - ->paginate(30); + $msg = DirectMessage::whereToId($profile->id) + ->orWhere('from_id', $profile->id) + ->findOrFail($threadId); - return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT); - } + $thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id]) + ->whereIn('from_id', [$profile->id, $msg->from_id]) + ->orderBy('created_at', 'asc') + ->paginate(30); - public function statusReplies(Request $request, int $id) - { - $this->validate($request, [ - 'limit' => 'nullable|int|min:1|max:6' - ]); - $parent = Status::whereScope('public')->findOrFail($id); - $limit = $request->input('limit') ?? 3; - $children = Status::whereInReplyToId($parent->id) - ->orderBy('created_at', 'desc') - ->take($limit) - ->get(); - $resource = new Fractal\Resource\Collection($children, new StatusTransformer()); - $res = $this->fractal->createData($resource)->toArray(); + return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT); + } - return response()->json($res); - } + public function statusReplies(Request $request, int $id) + { + $this->validate($request, [ + 'limit' => 'nullable|int|min:1|max:6', + ]); + $parent = Status::whereScope('public')->findOrFail($id); + $limit = $request->input('limit') ?? 3; + $children = Status::whereInReplyToId($parent->id) + ->orderBy('created_at', 'desc') + ->take($limit) + ->get(); + $resource = new Fractal\Resource\Collection($children, new StatusTransformer); + $res = $this->fractal->createData($resource)->toArray(); - public function stories(Request $request) - { + return response()->json($res); + } - } + public function stories(Request $request) {} - public function discoverCategories(Request $request) - { - $categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get(); - $res = $categories->map(function($item) { - return [ - 'name' => $item->name, - 'url' => $item->url(), - 'thumb' => $item->thumb() - ]; - }); - return response()->json($res); - } + public function discoverCategories(Request $request) + { + $categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get(); + $res = $categories->map(function ($item) { + return [ + 'name' => $item->name, + 'url' => $item->url(), + 'thumb' => $item->thumb(), + ]; + }); - public function modAction(Request $request) - { - abort_unless(Auth::user()->is_admin, 400); - $this->validate($request, [ - 'action' => [ - 'required', - 'string', - Rule::in([ - 'addcw', - 'remcw', - 'unlist', - 'spammer' - ]) - ], - 'item_id' => 'required|integer|min:1', - 'item_type' => [ - 'required', - 'string', - Rule::in(['profile', 'status']) - ] - ]); + return response()->json($res); + } - $action = $request->input('action'); - $item_id = $request->input('item_id'); - $item_type = $request->input('item_type'); + public function modAction(Request $request) + { + abort_unless(Auth::user()->is_admin, 400); + $this->validate($request, [ + 'action' => [ + 'required', + 'string', + Rule::in([ + 'addcw', + 'remcw', + 'unlist', + 'spammer', + ]), + ], + 'item_id' => 'required|integer|min:1', + 'item_type' => [ + 'required', + 'string', + Rule::in(['profile', 'status']), + ], + ]); - $status = Status::findOrFail($item_id); - $author = User::whereProfileId($status->profile_id)->first(); - abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts'); + $action = $request->input('action'); + $item_id = $request->input('item_id'); + $item_type = $request->input('item_type'); - switch($action) { - case 'addcw': - $status->is_nsfw = true; - $status->save(); - ModLogService::boot() - ->user(Auth::user()) - ->objectUid($status->profile->user_id) - ->objectId($status->id) - ->objectType('App\Status::class') - ->action('admin.status.moderate') - ->metadata([ - 'action' => 'cw', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + $status = Status::findOrFail($item_id); + $author = User::whereProfileId($status->profile_id)->first(); + abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts'); - if($status->uri == null) { - $media = $status->media; - $ai = new AccountInterstitial; - $ai->user_id = $status->profile->user_id; - $ai->type = 'post.cw'; - $ai->view = 'account.moderation.post.cw'; - $ai->item_type = 'App\Status'; - $ai->item_id = $status->id; - $ai->has_media = (bool) $media->count(); - $ai->blurhash = $media->count() ? $media->first()->blurhash : null; - $ai->meta = json_encode([ - 'caption' => $status->caption, - 'created_at' => $status->created_at, - 'type' => $status->type, - 'url' => $status->url(), - 'is_nsfw' => $status->is_nsfw, - 'scope' => $status->scope, - 'reblog' => $status->reblog_of_id, - 'likes_count' => $status->likes_count, - 'reblogs_count' => $status->reblogs_count, - ]); - $ai->save(); + switch ($action) { + case 'addcw': + $status->is_nsfw = true; + $status->save(); + ModLogService::boot() + ->user(Auth::user()) + ->objectUid($status->profile->user_id) + ->objectId($status->id) + ->objectType('App\Status::class') + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'cw', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - $u = $status->profile->user; - $u->has_interstitial = true; - $u->save(); - } - break; + if ($status->uri == null) { + $media = $status->media; + $ai = new AccountInterstitial; + $ai->user_id = $status->profile->user_id; + $ai->type = 'post.cw'; + $ai->view = 'account.moderation.post.cw'; + $ai->item_type = 'App\Status'; + $ai->item_id = $status->id; + $ai->has_media = (bool) $media->count(); + $ai->blurhash = $media->count() ? $media->first()->blurhash : null; + $ai->meta = json_encode([ + 'caption' => $status->caption, + 'created_at' => $status->created_at, + 'type' => $status->type, + 'url' => $status->url(), + 'is_nsfw' => $status->is_nsfw, + 'scope' => $status->scope, + 'reblog' => $status->reblog_of_id, + 'likes_count' => $status->likes_count, + 'reblogs_count' => $status->reblogs_count, + ]); + $ai->save(); - case 'remcw': - $status->is_nsfw = false; - $status->save(); - ModLogService::boot() - ->user(Auth::user()) - ->objectUid($status->profile->user_id) - ->objectId($status->id) - ->objectType('App\Status::class') - ->action('admin.status.moderate') - ->metadata([ - 'action' => 'remove_cw', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); - if($status->uri == null) { - $ai = AccountInterstitial::whereUserId($status->profile->user_id) - ->whereType('post.cw') - ->whereItemId($status->id) - ->whereItemType('App\Status') - ->first(); - $ai->delete(); - } - break; + $u = $status->profile->user; + $u->has_interstitial = true; + $u->save(); + } + break; - case 'unlist': - $status->scope = $status->visibility = 'unlisted'; - $status->save(); - PublicTimelineService::del($status->id); - ModLogService::boot() - ->user(Auth::user()) - ->objectUid($status->profile->user_id) - ->objectId($status->id) - ->objectType('App\Status::class') - ->action('admin.status.moderate') - ->metadata([ - 'action' => 'unlist', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + case 'remcw': + $status->is_nsfw = false; + $status->save(); + ModLogService::boot() + ->user(Auth::user()) + ->objectUid($status->profile->user_id) + ->objectId($status->id) + ->objectType('App\Status::class') + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'remove_cw', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); + if ($status->uri == null) { + $ai = AccountInterstitial::whereUserId($status->profile->user_id) + ->whereType('post.cw') + ->whereItemId($status->id) + ->whereItemType('App\Status') + ->first(); + $ai->delete(); + } + break; - if($status->uri == null) { - $media = $status->media; - $ai = new AccountInterstitial; - $ai->user_id = $status->profile->user_id; - $ai->type = 'post.unlist'; - $ai->view = 'account.moderation.post.unlist'; - $ai->item_type = 'App\Status'; - $ai->item_id = $status->id; - $ai->has_media = (bool) $media->count(); - $ai->blurhash = $media->count() ? $media->first()->blurhash : null; - $ai->meta = json_encode([ - 'caption' => $status->caption, - 'created_at' => $status->created_at, - 'type' => $status->type, - 'url' => $status->url(), - 'is_nsfw' => $status->is_nsfw, - 'scope' => $status->scope, - 'reblog' => $status->reblog_of_id, - 'likes_count' => $status->likes_count, - 'reblogs_count' => $status->reblogs_count, - ]); - $ai->save(); + case 'unlist': + $status->scope = $status->visibility = 'unlisted'; + $status->save(); + PublicTimelineService::del($status->id); + ModLogService::boot() + ->user(Auth::user()) + ->objectUid($status->profile->user_id) + ->objectId($status->id) + ->objectType('App\Status::class') + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'unlist', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - $u = $status->profile->user; - $u->has_interstitial = true; - $u->save(); - } - break; + if ($status->uri == null) { + $media = $status->media; + $ai = new AccountInterstitial; + $ai->user_id = $status->profile->user_id; + $ai->type = 'post.unlist'; + $ai->view = 'account.moderation.post.unlist'; + $ai->item_type = 'App\Status'; + $ai->item_id = $status->id; + $ai->has_media = (bool) $media->count(); + $ai->blurhash = $media->count() ? $media->first()->blurhash : null; + $ai->meta = json_encode([ + 'caption' => $status->caption, + 'created_at' => $status->created_at, + 'type' => $status->type, + 'url' => $status->url(), + 'is_nsfw' => $status->is_nsfw, + 'scope' => $status->scope, + 'reblog' => $status->reblog_of_id, + 'likes_count' => $status->likes_count, + 'reblogs_count' => $status->reblogs_count, + ]); + $ai->save(); - case 'spammer': - HandleSpammerPipeline::dispatch($status->profile); - ModLogService::boot() - ->user(Auth::user()) - ->objectUid($status->profile->user_id) - ->objectId($status->id) - ->objectType('App\User::class') - ->action('admin.status.moderate') - ->metadata([ - 'action' => 'spammer', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); - break; - } + $u = $status->profile->user; + $u->has_interstitial = true; + $u->save(); + } + break; - StatusService::del($status->id, true); - return ['msg' => 200]; - } + case 'spammer': + HandleSpammerPipeline::dispatch($status->profile); + ModLogService::boot() + ->user(Auth::user()) + ->objectUid($status->profile->user_id) + ->objectId($status->id) + ->objectType('App\User::class') + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'spammer', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); + break; + } - public function composePost(Request $request) - { - abort(400, 'Endpoint deprecated'); - } + StatusService::del($status->id, true); - public function bookmarks(Request $request) - { - $pid = $request->user()->profile_id; - $res = Bookmark::whereProfileId($pid) - ->orderByDesc('created_at') - ->simplePaginate(10) - ->map(function($bookmark) use($pid) { - $status = StatusService::get($bookmark->status_id, false); - if(!$status) { - return false; - } - $status['bookmarked_at'] = str_replace('+00:00', 'Z', $bookmark->created_at->format(DATE_RFC3339_EXTENDED)); + return ['msg' => 200]; + } - if($status) { - BookmarkService::add($pid, $status['id']); - } - return $status; - }) - ->filter(function($bookmark) { - return $bookmark && isset($bookmark['id']); - }) - ->values(); + public function composePost(Request $request) + { + abort(400, 'Endpoint deprecated'); + } - return response()->json($res); - } + public function bookmarks(Request $request) + { + $pid = $request->user()->profile_id; + $res = Bookmark::whereProfileId($pid) + ->orderByDesc('created_at') + ->simplePaginate(10) + ->map(function ($bookmark) use ($pid) { + $status = StatusService::get($bookmark->status_id, false); + if (! $status) { + return false; + } + $status['bookmarked_at'] = str_replace('+00:00', 'Z', $bookmark->created_at->format(DATE_RFC3339_EXTENDED)); - public function accountStatuses(Request $request, $id) - { - $this->validate($request, [ - 'only_media' => 'nullable', - 'pinned' => 'nullable', - 'exclude_replies' => 'nullable', - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|min:1|max:24' - ]); + if ($status) { + BookmarkService::add($pid, $status['id']); + } - $profile = Profile::whereNull('status')->findOrFail($id); + return $status; + }) + ->filter(function ($bookmark) { + return $bookmark && isset($bookmark['id']); + }) + ->values(); - $limit = $request->limit ?? 9; - $max_id = $request->max_id; - $min_id = $request->min_id; - $scope = $request->only_media == true ? - ['photo', 'photo:album', 'video', 'video:album'] : - ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply']; + return response()->json($res); + } - if($profile->is_private) { - if(!Auth::check()) { - return response()->json([]); - } - $pid = Auth::user()->profile->id; - $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) { - $following = Follower::whereProfileId($pid)->pluck('following_id'); - return $following->push($pid)->toArray(); - }); - $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : []; - } else { - if(Auth::check()) { - $pid = Auth::user()->profile->id; - $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) { - $following = Follower::whereProfileId($pid)->pluck('following_id'); - return $following->push($pid)->toArray(); - }); - $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; - } else { - $visibility = ['public', 'unlisted']; - } - } + public function accountStatuses(Request $request, $id) + { + $this->validate($request, [ + 'only_media' => 'nullable', + 'pinned' => 'nullable', + 'exclude_replies' => 'nullable', + 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'limit' => 'nullable|integer|min:1|max:24', + ]); - $dir = $min_id ? '>' : '<'; - $id = $min_id ?? $max_id; - $timeline = Status::select( - 'id', - 'uri', - 'caption', - 'rendered', - 'profile_id', - 'type', - 'in_reply_to_id', - 'reblog_of_id', - 'is_nsfw', - 'likes_count', - 'reblogs_count', - 'scope', - 'local', - 'created_at', - 'updated_at' - )->whereProfileId($profile->id) - ->whereIn('type', $scope) - ->where('id', $dir, $id) - ->whereIn('visibility', $visibility) - ->latest() - ->limit($limit) - ->get(); + $profile = Profile::whereNull('status')->findOrFail($id); - $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer()); - $res = $this->fractal->createData($resource)->toArray(); + $limit = $request->limit ?? 9; + $max_id = $request->max_id; + $min_id = $request->min_id; + $scope = $request->only_media == true ? + ['photo', 'photo:album', 'video', 'video:album'] : + ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply']; - return response()->json($res); - } + if ($profile->is_private) { + if (! Auth::check()) { + return response()->json([]); + } + $pid = Auth::user()->profile->id; + $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) { + $following = Follower::whereProfileId($pid)->pluck('following_id'); - public function remoteProfile(Request $request, $id) - { - return redirect('/i/web/profile/' . $id); - } + return $following->push($pid)->toArray(); + }); + $visibility = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : []; + } else { + if (Auth::check()) { + $pid = Auth::user()->profile->id; + $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) { + $following = Follower::whereProfileId($pid)->pluck('following_id'); - public function remoteStatus(Request $request, $profileId, $statusId) - { - return redirect('/i/web/post/' . $statusId); - } + return $following->push($pid)->toArray(); + }); + $visibility = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; + } else { + $visibility = ['public', 'unlisted']; + } + } - public function requestEmailVerification(Request $request) - { - $pid = $request->user()->profile_id; - $exists = Redis::sismember('email:manual', $pid); - return view('account.email.request_verification', compact('exists')); - } + $dir = $min_id ? '>' : '<'; + $id = $min_id ?? $max_id; + $timeline = Status::select( + 'id', + 'uri', + 'caption', + 'profile_id', + 'type', + 'in_reply_to_id', + 'reblog_of_id', + 'is_nsfw', + 'likes_count', + 'reblogs_count', + 'scope', + 'local', + 'created_at', + 'updated_at' + )->whereProfileId($profile->id) + ->whereIn('type', $scope) + ->where('id', $dir, $id) + ->whereIn('visibility', $visibility) + ->latest() + ->limit($limit) + ->get(); - public function requestEmailVerificationStore(Request $request) - { - $pid = $request->user()->profile_id; - Redis::sadd('email:manual', $pid); - return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']); - } + $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer); + $res = $this->fractal->createData($resource)->toArray(); + + return response()->json($res); + } + + public function remoteProfile(Request $request, $id) + { + return redirect('/i/web/profile/'.$id); + } + + public function remoteStatus(Request $request, $profileId, $statusId) + { + return redirect('/i/web/post/'.$statusId); + } + + public function requestEmailVerification(Request $request) + { + $pid = $request->user()->profile_id; + $exists = Redis::sismember('email:manual', $pid); + + return view('account.email.request_verification', compact('exists')); + } + + public function requestEmailVerificationStore(Request $request) + { + $pid = $request->user()->profile_id; + Redis::sadd('email:manual', $pid); + + return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']); + } } diff --git a/app/Http/Controllers/LandingController.php b/app/Http/Controllers/LandingController.php index 5f9f0bba1..f90d84bc2 100644 --- a/app/Http/Controllers/LandingController.php +++ b/app/Http/Controllers/LandingController.php @@ -2,44 +2,43 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use App\Profile; -use App\Services\AccountService; use App\Http\Resources\DirectoryProfile; +use App\Profile; +use Illuminate\Http\Request; class LandingController extends Controller { public function directoryRedirect(Request $request) { - if($request->user()) { - return redirect('/'); - } + if ($request->user()) { + return redirect('/'); + } - abort_if(config_cache('instance.landing.show_directory') == false, 404); + abort_if((bool) config_cache('instance.landing.show_directory') == false, 404); - return view('site.index'); + return view('site.index'); } public function exploreRedirect(Request $request) { - if($request->user()) { - return redirect('/'); - } + if ($request->user()) { + return redirect('/'); + } - abort_if(config_cache('instance.landing.show_explore') == false, 404); + abort_if((bool) config_cache('instance.landing.show_explore') == false, 404); - return view('site.index'); + return view('site.index'); } public function getDirectoryApi(Request $request) { - abort_if(config_cache('instance.landing.show_directory') == false, 404); + abort_if((bool) config_cache('instance.landing.show_directory') == false, 404); - return DirectoryProfile::collection( - Profile::whereNull('domain') - ->whereIsSuggestable(true) - ->orderByDesc('updated_at') - ->cursorPaginate(20) - ); + return DirectoryProfile::collection( + Profile::whereNull('domain') + ->whereIsSuggestable(true) + ->orderByDesc('updated_at') + ->cursorPaginate(20) + ); } } diff --git a/app/Http/Controllers/MediaController.php b/app/Http/Controllers/MediaController.php index b10e75795..cbc08cb5a 100644 --- a/app/Http/Controllers/MediaController.php +++ b/app/Http/Controllers/MediaController.php @@ -2,30 +2,31 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; use App\Media; +use Illuminate\Http\Request; class MediaController extends Controller { - public function index(Request $request) - { - //return view('settings.drive.index'); - } + public function index(Request $request) + { + //return view('settings.drive.index'); + abort(404); + } - public function composeUpdate(Request $request, $id) - { + public function composeUpdate(Request $request, $id) + { abort(400, 'Endpoint deprecated'); - } + } - public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f) - { - abort_if(!config_cache('pixelfed.cloud_storage'), 404); - $path = 'public/m/_v2/' . $pid . '/' . $mhash . '/' . $uhash . '/' . $f; - $media = Media::whereProfileId($pid) - ->whereMediaPath($path) - ->whereNotNull('cdn_url') - ->firstOrFail(); + public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f) + { + abort_if(! (bool) config_cache('pixelfed.cloud_storage'), 404); + $path = 'public/m/_v2/'.$pid.'/'.$mhash.'/'.$uhash.'/'.$f; + $media = Media::whereProfileId($pid) + ->whereMediaPath($path) + ->whereNotNull('cdn_url') + ->firstOrFail(); - return redirect()->away($media->cdn_url); - } + return redirect()->away($media->cdn_url); + } } diff --git a/app/Http/Controllers/MicroController.php b/app/Http/Controllers/MicroController.php index 420083f0f..62e0ae969 100644 --- a/app/Http/Controllers/MicroController.php +++ b/app/Http/Controllers/MicroController.php @@ -2,66 +2,65 @@ namespace App\Http\Controllers; +use App\Status; +use Auth; +use DB; use Illuminate\Http\Request; -use App\{ - Profile, - Status, -}; -use Auth, DB, Purify; use Illuminate\Validation\Rule; class MicroController extends Controller { - public function __construct() - { - $this->middleware('auth'); - } + public function __construct() + { + $this->middleware('auth'); + } - public function composeText(Request $request) - { - $this->validate($request, [ - 'type' => [ - 'required', - 'string', - Rule::in(['text']) - ], - 'title' => 'nullable|string|max:140', - 'content' => 'required|string|max:500', - 'visibility' => [ - 'required', - 'string', - Rule::in([ - 'public', - 'unlisted', - 'private', - 'draft' - ]) - ] - ]); - $profile = Auth::user()->profile; - $title = $request->input('title'); - $content = $request->input('content'); - $visibility = $request->input('visibility'); + public function composeText(Request $request) + { + $this->validate($request, [ + 'type' => [ + 'required', + 'string', + Rule::in(['text']), + ], + 'title' => 'nullable|string|max:140', + 'content' => 'required|string|max:500', + 'visibility' => [ + 'required', + 'string', + Rule::in([ + 'public', + 'unlisted', + 'private', + 'draft', + ]), + ], + ]); + $profile = Auth::user()->profile; + $title = $request->input('title'); + $content = $request->input('content'); + $visibility = $request->input('visibility'); - $status = DB::transaction(function() use($profile, $content, $visibility, $title) { - $status = new Status; - $status->type = 'text'; - $status->profile_id = $profile->id; - $status->caption = strip_tags($content); - $status->rendered = Purify::clean($content); - $status->is_nsfw = false; + $status = DB::transaction(function () use ($profile, $content, $visibility, $title) { + $status = new Status; + $status->type = 'text'; + $status->profile_id = $profile->id; + $status->caption = strip_tags($content); + $status->is_nsfw = false; - // TODO: remove deprecated visibility in favor of scope - $status->visibility = $visibility; - $status->scope = $visibility; - $status->entities = json_encode(['title'=>$title]); - $status->save(); - return $status; - }); + // TODO: remove deprecated visibility in favor of scope + $status->visibility = $visibility; + $status->scope = $visibility; + $status->entities = json_encode(['title' => $title]); + $status->save(); - $fractal = new \League\Fractal\Manager(); - $fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer()); - $s = new \League\Fractal\Resource\Item($status, new \App\Transformer\Api\StatusTransformer()); - return $fractal->createData($s)->toArray(); - } + return $status; + }); + + $fractal = new \League\Fractal\Manager; + $fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer); + $s = new \League\Fractal\Resource\Item($status, new \App\Transformer\Api\StatusTransformer); + + return $fractal->createData($s)->toArray(); + } } diff --git a/app/Http/Controllers/OAuth/OobAuthorizationController.php b/app/Http/Controllers/OAuth/OobAuthorizationController.php new file mode 100644 index 000000000..f69368b79 --- /dev/null +++ b/app/Http/Controllers/OAuth/OobAuthorizationController.php @@ -0,0 +1,99 @@ +assertValidAuthToken($request); + + $authRequest = $this->getAuthRequestFromSession($request); + $authRequest->setAuthorizationApproved(true); + + return $this->withErrorHandling(function () use ($authRequest) { + $response = $this->server->completeAuthorizationRequest($authRequest, new Psr7Response); + + if ($this->isOutOfBandRequest($authRequest)) { + $code = $this->extractAuthorizationCode($response); + return response()->json([ + 'code' => $code, + 'state' => $authRequest->getState() + ]); + } + + return $this->convertResponse($response); + }); + } + + /** + * Check if the request is an out-of-band OAuth request. + * + * @param \League\OAuth2\Server\RequestTypes\AuthorizationRequest $authRequest + * @return bool + */ + protected function isOutOfBandRequest($authRequest) + { + return $authRequest->getRedirectUri() === 'urn:ietf:wg:oauth:2.0:oob'; + } + + /** + * Extract the authorization code from the PSR-7 response. + * + * @param \Psr\Http\Message\ResponseInterface $response + * @return string + * @throws \League\OAuth2\Server\Exception\OAuthServerException + */ + protected function extractAuthorizationCode($response) + { + $location = $response->getHeader('Location')[0] ?? ''; + + if (empty($location)) { + throw OAuthServerException::serverError('Missing authorization code in response'); + } + + parse_str(parse_url($location, PHP_URL_QUERY), $params); + + if (!isset($params['code'])) { + throw OAuthServerException::serverError('Invalid authorization code format'); + } + + return $params['code']; + } + + /** + * Handle OAuth errors for both redirect and OOB flows. + * + * @param \Closure $callback + * @return \Illuminate\Http\Response + */ + protected function withErrorHandling($callback) + { + try { + return $callback(); + } catch (OAuthServerException $e) { + if ($this->isOutOfBandRequest($this->getAuthRequestFromSession(request()))) { + return response()->json([ + 'error' => $e->getErrorType(), + 'message' => $e->getMessage(), + 'hint' => $e->getHint() + ], $e->getHttpStatusCode()); + } + + return $this->convertResponse( + $e->generateHttpResponse(new Psr7Response) + ); + } + } +} diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php new file mode 100644 index 000000000..7d8625863 --- /dev/null +++ b/app/Http/Controllers/ParentalControlsController.php @@ -0,0 +1,231 @@ +user(), 404); + abort_unless($request->user()->has_roles === 0, 404); + } + abort_unless(config('instance.parental_controls.enabled'), 404); + if(config_cache('pixelfed.open_registration') == false) { + abort_if(config('instance.parental_controls.limits.respect_open_registration'), 404); + } + if($maxUserCheck == true) { + $hasLimit = config('pixelfed.enforce_max_users'); + if($hasLimit) { + $count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count(); + $limit = (int) config('pixelfed.max_users'); + + abort_if($limit && $limit <= $count, 404); + } + } + } + + public function index(Request $request) + { + $this->authPreflight($request); + $children = ParentalControls::whereParentId($request->user()->id)->latest()->paginate(5); + return view('settings.parental-controls.index', compact('children')); + } + + public function add(Request $request) + { + $this->authPreflight($request, true); + return view('settings.parental-controls.add'); + } + + public function view(Request $request, $id) + { + $this->authPreflight($request); + $uid = $request->user()->id; + $pc = ParentalControls::whereParentId($uid)->findOrFail($id); + return view('settings.parental-controls.manage', compact('pc')); + } + + public function update(Request $request, $id) + { + $this->authPreflight($request); + $uid = $request->user()->id; + $ff = $this->requestFormFields($request); + $pc = ParentalControls::whereParentId($uid)->findOrFail($id); + $pc->permissions = $ff; + $pc->save(); + + $roles = UserRoleService::mapActions($pc->child_id, $ff); + if(isset($roles['account-force-private'])) { + $c = Profile::whereUserId($pc->child_id)->first(); + $c->is_private = $roles['account-force-private']; + $c->save(); + } + UserRoles::whereUserId($pc->child_id)->update(['roles' => $roles]); + return redirect($pc->manageUrl() . '?permissions'); + } + + public function store(Request $request) + { + $this->authPreflight($request, true); + $this->validate($request, [ + 'email' => 'required|email|unique:parental_controls,email|unique:users,email', + ]); + + $state = $this->requestFormFields($request); + + $pc = new ParentalControls; + $pc->parent_id = $request->user()->id; + $pc->email = $request->input('email'); + $pc->verify_code = str_random(32); + $pc->permissions = $state; + $pc->save(); + + DispatchChildInvitePipeline::dispatch($pc); + return redirect($pc->manageUrl()); + } + + public function inviteRegister(Request $request, $id, $code) + { + if($request->user()) { + $title = 'You cannot complete this action on this device.'; + $body = 'Please log out or use a different device or browser to complete the invitation registration.'; + return view('errors.custom', compact('title', 'body')); + } + + $this->authPreflight($request, true, false); + + $pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull(['email_verified_at', 'child_id'])->findOrFail($id); + abort_unless(User::whereId($pc->parent_id)->exists(), 404); + return view('settings.parental-controls.invite-register-form', compact('pc')); + } + + public function inviteRegisterStore(Request $request, $id, $code) + { + if($request->user()) { + $title = 'You cannot complete this action on this device.'; + $body = 'Please log out or use a different device or browser to complete the invitation registration.'; + return view('errors.custom', compact('title', 'body')); + } + + $this->authPreflight($request, true, false); + + $pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull('email_verified_at')->findOrFail($id); + + $fields = $request->all(); + $fields['email'] = $pc->email; + $defaults = UserRoleService::defaultRoles(); + $validator = (new RegisterController)->validator($fields); + $valid = $validator->validate(); + abort_if(!$valid, 404); + event(new Registered($user = (new RegisterController)->create($fields))); + sleep(5); + $user->has_roles = true; + $user->parent_id = $pc->parent_id; + if(config('instance.parental_controls.limits.auto_verify_email')) { + $user->email_verified_at = now(); + $user->save(); + sleep(3); + } else { + $user->save(); + sleep(3); + } + $ur = UserRoles::updateOrCreate([ + 'user_id' => $user->id, + ],[ + 'roles' => UserRoleService::mapInvite($user->id, $pc->permissions) + ]); + $pc->email_verified_at = now(); + $pc->child_id = $user->id; + $pc->save(); + sleep(2); + Auth::guard()->login($user); + + return redirect('/i/web'); + } + + public function cancelInvite(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + + return view('settings.parental-controls.delete-invite', compact('pc')); + } + + public function cancelInviteHandle(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + + $pc->delete(); + + return redirect('/settings/parental-controls'); + } + + public function stopManaging(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNotNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + + return view('settings.parental-controls.stop-managing', compact('pc')); + } + + public function stopManagingHandle(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNotNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + $pc->child()->update([ + 'has_roles' => false, + 'parent_id' => null, + ]); + $pc->delete(); + + return redirect('/settings/parental-controls'); + } + + protected function requestFormFields($request) + { + $state = []; + $fields = [ + 'post', + 'comment', + 'like', + 'share', + 'follow', + 'bookmark', + 'story', + 'collection', + 'discovery_feeds', + 'dms', + 'federation', + 'hide_network', + 'private', + 'hide_cw' + ]; + + foreach ($fields as $field) { + $state[$field] = $request->input($field) == 'on'; + } + + return $state; + } +} diff --git a/app/Http/Controllers/PixelfedDirectoryController.php b/app/Http/Controllers/PixelfedDirectoryController.php index 6290cd398..d6a014d07 100644 --- a/app/Http/Controllers/PixelfedDirectoryController.php +++ b/app/Http/Controllers/PixelfedDirectoryController.php @@ -2,37 +2,41 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; use App\Models\ConfigCache; -use Storage; use App\Services\AccountService; +use App\Services\InstanceService; use App\Services\StatusService; +use App\User; +use Cache; +use Illuminate\Http\Request; use Illuminate\Support\Str; +use Storage; class PixelfedDirectoryController extends Controller { public function get(Request $request) { - if(!$request->filled('sk')) { + if (! $request->filled('sk')) { abort(404); } - if(!config_cache('pixelfed.directory.submission-key')) { + if (! config_cache('pixelfed.directory.submission-key')) { abort(404); } - if(!hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) { + if (! hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) { abort(403); } $res = $this->buildListing(); - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + + return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } public function buildListing() { $res = config_cache('pixelfed.directory'); - if($res) { + if ($res) { $res = is_string($res) ? json_decode($res, true) : $res; } @@ -41,70 +45,72 @@ class PixelfedDirectoryController extends Controller $res['_ts'] = config_cache('pixelfed.directory.submission-ts'); $res['version'] = config_cache('pixelfed.version'); - if(empty($res['summary'])) { + if (empty($res['summary'])) { $summary = ConfigCache::whereK('app.short_description')->pluck('v'); $res['summary'] = $summary ? $summary[0] : null; } - if(isset($res['admin'])) { + if (isset($res['admin'])) { $res['admin'] = AccountService::get($res['admin'], true); } - if(isset($res['banner_image']) && !empty($res['banner_image'])) { + if (isset($res['banner_image']) && ! empty($res['banner_image'])) { $res['banner_image'] = url(Storage::url($res['banner_image'])); } - if(isset($res['favourite_posts'])) { - $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) { + if (isset($res['favourite_posts'])) { + $res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) { return StatusService::get($id); }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->map(function($post) { - return [ - 'avatar' => $post['account']['avatar'], - 'display_name' => $post['account']['display_name'], - 'username' => $post['account']['username'], - 'media' => $post['media_attachments'][0]['url'], - 'url' => $post['url'] - ]; - }) - ->values(); + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->map(function ($post) { + return [ + 'avatar' => $post['account']['avatar'], + 'display_name' => $post['account']['display_name'], + 'username' => $post['account']['username'], + 'media' => $post['media_attachments'][0]['url'], + 'url' => $post['url'], + ]; + }) + ->values(); } $guidelines = ConfigCache::whereK('app.rules')->first(); - if($guidelines) { + if ($guidelines) { $res['community_guidelines'] = json_decode($guidelines->v, true); } - $openRegistration = ConfigCache::whereK('pixelfed.open_registration')->first(); - if($openRegistration) { - $res['open_registration'] = (bool) $openRegistration; - } + $openRegistration = (bool) config_cache('pixelfed.open_registration'); + $res['open_registration'] = $openRegistration; + + $curatedOnboarding = (bool) config_cache('instance.curated_registration.enabled'); + $res['curated_onboarding'] = $curatedOnboarding; $oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first(); - if($oauthEnabled) { - $keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key')); + if ($oauthEnabled) { + $keys = (file_exists(storage_path('oauth-public.key')) || config_cache('passport.public_key')) && + (file_exists(storage_path('oauth-private.key')) || config_cache('passport.private_key')); $res['oauth_enabled'] = (bool) $oauthEnabled && $keys; } $activityPubEnabled = ConfigCache::whereK('federation.activitypub.enabled')->first(); - if($activityPubEnabled) { + if ($activityPubEnabled) { $res['activitypub_enabled'] = (bool) $activityPubEnabled; } $res['feature_config'] = [ 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), 'image_quality' => config_cache('pixelfed.image_quality'), - 'optimize_image' => config_cache('pixelfed.optimize_image'), + 'optimize_image' => (bool) config_cache('pixelfed.optimize_image'), 'max_photo_size' => config_cache('pixelfed.max_photo_size'), 'max_caption_length' => config_cache('pixelfed.max_caption_length'), 'max_altext_length' => config_cache('pixelfed.max_altext_length'), - 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'), + 'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'), 'max_account_size' => config_cache('pixelfed.max_account_size'), 'max_album_length' => config_cache('pixelfed.max_album_length'), - 'account_deletion' => config_cache('pixelfed.account_deletion'), + 'account_deletion' => (bool) config_cache('pixelfed.account_deletion'), ]; $res['is_eligible'] = $this->validVal($res, 'admin') && @@ -114,29 +120,34 @@ class PixelfedDirectoryController extends Controller $this->validVal($res, 'privacy_pledge') && $this->validVal($res, 'location'); - if(config_cache('pixelfed.directory.testimonials')) { + if (config_cache('pixelfed.directory.testimonials')) { $res['testimonials'] = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true)) - ->map(function($testimonial) { + ->map(function ($testimonial) { $profile = AccountService::get($testimonial['profile_id']); + return [ 'profile' => [ 'username' => $profile['username'], 'display_name' => $profile['display_name'], 'avatar' => $profile['avatar'], - 'created_at' => $profile['created_at'] + 'created_at' => $profile['created_at'], ], - 'body' => $testimonial['body'] + 'body' => $testimonial['body'], ]; }); } $res['features_enabled'] = [ - 'stories' => (bool) config_cache('instance.stories.enabled') + 'stories' => (bool) config_cache('instance.stories.enabled'), ]; + $statusesCount = InstanceService::totalLocalStatuses(); + $usersCount = Cache::remember('api:nodeinfo:users', 43200, function () { + return User::count(); + }); $res['stats'] = [ - 'user_count' => \App\User::count(), - 'post_count' => \App\Status::whereNull('uri')->count(), + 'user_count' => (int) $usersCount, + 'post_count' => (int) $statusesCount, ]; $res['primary_locale'] = config('app.locale'); @@ -149,19 +160,18 @@ class PixelfedDirectoryController extends Controller protected function validVal($res, $val, $count = false, $minLen = false) { - if(!isset($res[$val])) { + if (! isset($res[$val])) { return false; } - if($count) { + if ($count) { return count($res[$val]) >= $count; } - if($minLen) { + if ($minLen) { return strlen($res[$val]) >= $minLen; } return $res[$val]; } - } diff --git a/app/Http/Controllers/ProfileAliasController.php b/app/Http/Controllers/ProfileAliasController.php index 024005a8e..559dcb9a6 100644 --- a/app/Http/Controllers/ProfileAliasController.php +++ b/app/Http/Controllers/ProfileAliasController.php @@ -2,11 +2,13 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use App\Util\Lexer\Nickname; -use App\Util\Webfinger\WebfingerUrl; use App\Models\ProfileAlias; +use App\Models\ProfileMigration; +use App\Services\AccountService; use App\Services\WebfingerService; +use App\Util\Lexer\Nickname; +use Cache; +use Illuminate\Http\Request; class ProfileAliasController extends Controller { @@ -18,31 +20,47 @@ class ProfileAliasController extends Controller public function index(Request $request) { $aliases = $request->user()->profile->aliases; + return view('settings.aliases.index', compact('aliases')); } public function store(Request $request) { $this->validate($request, [ - 'acct' => 'required' + 'acct' => 'required', ]); $acct = $request->input('acct'); - if($request->user()->profile->aliases->count() >= 3) { + $nn = Nickname::normalizeProfileUrl($acct); + if (! $nn) { + return back()->with('error', 'Invalid account alias.'); + } + + if ($nn['domain'] === config('pixelfed.domain.app')) { + if (strtolower($nn['username']) == ($request->user()->username)) { + return back()->with('error', 'You cannot add an alias to your own account.'); + } + } + + if ($request->user()->profile->aliases->count() >= 3) { return back()->with('error', 'You can only add 3 account aliases.'); } $webfingerService = WebfingerService::lookup($acct); - if(!$webfingerService || !isset($webfingerService['url'])) { + $webfingerUrl = WebfingerService::rawGet($acct); + + if (! $webfingerService || ! isset($webfingerService['url']) || ! $webfingerUrl || empty($webfingerUrl)) { return back()->with('error', 'Invalid account, cannot add alias at this time.'); } $alias = new ProfileAlias; $alias->profile_id = $request->user()->profile_id; $alias->acct = $acct; - $alias->uri = $webfingerService['url']; + $alias->uri = $webfingerUrl; $alias->save(); + Cache::forget('pf:activitypub:user-object:by-id:'.$request->user()->profile_id); + return back()->with('status', 'Successfully added alias!'); } @@ -50,14 +68,25 @@ class ProfileAliasController extends Controller { $this->validate($request, [ 'acct' => 'required', - 'id' => 'required|exists:profile_aliases' + 'id' => 'required|exists:profile_aliases', ]); - - $alias = ProfileAlias::where('profile_id', $request->user()->profile_id) - ->where('acct', $request->input('acct')) + $pid = $request->user()->profile_id; + $acct = $request->input('acct'); + $alias = ProfileAlias::where('profile_id', $pid) + ->where('acct', $acct) ->findOrFail($request->input('id')); + $migration = ProfileMigration::whereProfileId($pid) + ->whereAcct($acct) + ->first(); + if ($migration) { + $request->user()->profile->update([ + 'moved_to_profile_id' => null, + ]); + } $alias->delete(); + Cache::forget('pf:activitypub:user-object:by-id:'.$pid); + AccountService::del($pid); return back()->with('status', 'Successfully deleted alias!'); } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 26e9e5398..5c6e4b082 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -2,356 +2,393 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use Auth; -use Cache; -use DB; -use View; use App\AccountInterstitial; use App\Follower; use App\FollowRequest; use App\Profile; -use App\Story; -use App\Status; -use App\User; -use App\UserSetting; -use App\UserFilter; -use League\Fractal; use App\Services\AccountService; use App\Services\FollowerService; use App\Services\StatusService; -use App\Util\Lexer\Nickname; -use App\Util\Webfinger\Webfinger; -use App\Transformer\ActivityPub\ProfileOutbox; +use App\Status; +use App\Story; use App\Transformer\ActivityPub\ProfileTransformer; +use App\User; +use App\UserFilter; +use App\UserSetting; +use Auth; +use Cache; +use Illuminate\Http\Request; +use League\Fractal; +use View; class ProfileController extends Controller { - public function show(Request $request, $username) - { - // redirect authed users to Metro 2.0 - if($request->user()) { - // unless they force static view - if(!$request->has('fs') || $request->input('fs') != '1') { - $pid = AccountService::usernameToId($username); - if($pid) { - return redirect('/i/web/profile/' . $pid); - } - } - } + public function show(Request $request, $username) + { + if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) { + $user = $this->getCachedUser($username, true); + abort_if(! $user, 404, 'Not found'); - $user = Profile::whereNull('domain') - ->whereNull('status') - ->whereUsername($username) - ->firstOrFail(); + return $this->showActivityPub($request, $user); + } + // redirect authed users to Metro 2.0 + if ($request->user() && !$request->filled('carousel')) { + // unless they force static view + if (! $request->has('fs') || $request->input('fs') != '1') { + $pid = AccountService::usernameToId($username); + if ($pid) { + return redirect('/i/web/profile/'.$pid); + } + } + } - if($request->wantsJson() && config_cache('federation.activitypub.enabled')) { - return $this->showActivityPub($request, $user); - } + $user = $this->getCachedUser($username); - $aiCheck = Cache::remember('profile:ai-check:spam-login:' . $user->id, 86400, function() use($user) { - $exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count(); - if($exists) { - return true; - } + abort_unless($user, 404); - return false; - }); - if($aiCheck) { - return redirect('/login'); - } - return $this->buildProfile($request, $user); - } + $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$user->id, 3600, function () use ($user) { + $exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count(); + if ($exists) { + return true; + } - protected function buildProfile(Request $request, $user) - { - $username = $user->username; - $loggedIn = Auth::check(); - $isPrivate = false; - $isBlocked = false; - if(!$loggedIn) { - $key = 'profile:settings:' . $user->id; - $ttl = now()->addHours(6); - $settings = Cache::remember($key, $ttl, function() use($user) { - return $user->user->settings; - }); + return false; + }); + if ($aiCheck) { + return redirect('/login'); + } - if ($user->is_private == true) { - $profile = null; - return view('profile.private', compact('user')); - } + return $this->buildProfile($request, $user); + } - $owner = false; - $is_following = false; + protected function buildProfile(Request $request, $user) + { + $carousel = (bool) $request->filled('carousel'); + $username = $user->username; + $loggedIn = Auth::check(); + $isPrivate = false; + $isBlocked = false; + if (! $loggedIn) { + $key = 'profile:settings:'.$user->id; + $ttl = now()->addHours(6); + $settings = Cache::remember($key, $ttl, function () use ($user) { + return $user->user->settings; + }); - $profile = $user; - $settings = [ - 'crawlable' => $settings->crawlable, - 'following' => [ - 'count' => $settings->show_profile_following_count, - 'list' => $settings->show_profile_following - ], - 'followers' => [ - 'count' => $settings->show_profile_follower_count, - 'list' => $settings->show_profile_followers - ] - ]; - return view('profile.show', compact('profile', 'settings')); - } else { - $key = 'profile:settings:' . $user->id; - $ttl = now()->addHours(6); - $settings = Cache::remember($key, $ttl, function() use($user) { - return $user->user->settings; - }); + if ($user->is_private == true) { + $profile = null; - if ($user->is_private == true) { - $isPrivate = $this->privateProfileCheck($user, $loggedIn); - } + return view('profile.private', compact('user')); + } - $isBlocked = $this->blockedProfileCheck($user); + $owner = false; + $is_following = false; - $owner = $loggedIn && Auth::id() === $user->user_id; - $is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false; + $profile = $user; + $settings = [ + 'crawlable' => $settings->crawlable, + 'following' => [ + 'count' => $settings->show_profile_following_count, + 'list' => $settings->show_profile_following, + ], + 'followers' => [ + 'count' => $settings->show_profile_follower_count, + 'list' => $settings->show_profile_followers, + ], + ]; - if ($isPrivate == true || $isBlocked == true) { - $requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id) - ->whereFollowingId($user->id) - ->exists() : false; - return view('profile.private', compact('user', 'is_following', 'requested')); - } + if($carousel) { + return view('profile.show_carousel', compact('profile', 'settings')); + } + return view('profile.show', compact('profile', 'settings')); + } else { + $key = 'profile:settings:'.$user->id; + $ttl = now()->addHours(6); + $settings = Cache::remember($key, $ttl, function () use ($user) { + return $user->user->settings; + }); - $is_admin = is_null($user->domain) ? $user->user->is_admin : false; - $profile = $user; - $settings = [ - 'crawlable' => $settings->crawlable, - 'following' => [ - 'count' => $settings->show_profile_following_count, - 'list' => $settings->show_profile_following - ], - 'followers' => [ - 'count' => $settings->show_profile_follower_count, - 'list' => $settings->show_profile_followers - ] - ]; - return view('profile.show', compact('profile', 'settings')); - } - } + if ($user->is_private == true) { + $isPrivate = $this->privateProfileCheck($user, $loggedIn); + } - public function permalinkRedirect(Request $request, $username) - { - $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); + $isBlocked = $this->blockedProfileCheck($user); - if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) { - return $this->showActivityPub($request, $user); - } + $owner = $loggedIn && Auth::id() === $user->user_id; + $is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false; - return redirect($user->url()); - } + if ($isPrivate == true || $isBlocked == true) { + $requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id) + ->whereFollowingId($user->id) + ->exists() : false; - protected function privateProfileCheck(Profile $profile, $loggedIn) - { - if (!Auth::check()) { - return true; - } + return view('profile.private', compact('user', 'is_following', 'requested')); + } - $user = Auth::user()->profile; - if($user->id == $profile->id || !$profile->is_private) { - return false; - } + $is_admin = is_null($user->domain) ? $user->user->is_admin : false; + $profile = $user; + $settings = [ + 'crawlable' => $settings->crawlable, + 'following' => [ + 'count' => $settings->show_profile_following_count, + 'list' => $settings->show_profile_following, + ], + 'followers' => [ + 'count' => $settings->show_profile_follower_count, + 'list' => $settings->show_profile_followers, + ], + ]; + if($carousel) { + return view('profile.show_carousel', compact('profile', 'settings')); + } + return view('profile.show', compact('profile', 'settings')); + } + } - $follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists(); - if ($follows == false) { - return true; - } + protected function getCachedUser($username, $withTrashed = false) + { + $val = str_replace(['_', '.', '-'], '', $username); + if (! ctype_alnum($val)) { + return; + } + $hash = ($withTrashed ? 'wt:' : 'wot:').strtolower($username); - return false; - } + return Cache::remember('pfc:cached-user:'.$hash, ($withTrashed ? 14400 : 900), function () use ($username, $withTrashed) { + if (! $withTrashed) { + return Profile::whereNull(['domain', 'status']) + ->whereUsername($username) + ->first(); + } else { + return Profile::withTrashed() + ->whereNull('domain') + ->whereUsername($username) + ->first(); + } + }); + } - public static function accountCheck(Profile $profile) - { - switch ($profile->status) { - case 'disabled': - case 'suspended': - case 'delete': - return view('profile.disabled'); - break; + public function permalinkRedirect(Request $request, $username) + { + if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) { + $user = $this->getCachedUser($username, true); - default: - break; - } - return abort(404); - } + return $this->showActivityPub($request, $user); + } - protected function blockedProfileCheck(Profile $profile) - { - $pid = Auth::user()->profile->id; - $blocks = UserFilter::whereUserId($profile->id) - ->whereFilterType('block') - ->whereFilterableType('App\Profile') - ->pluck('filterable_id') - ->toArray(); - if (in_array($pid, $blocks)) { - return true; - } + $user = $this->getCachedUser($username); - return false; - } + abort_if(! $user, 404); - public function showActivityPub(Request $request, $user) - { - abort_if(!config_cache('federation.activitypub.enabled'), 404); - abort_if($user->domain, 404); + return redirect($user->url()); + } - return Cache::remember('pf:activitypub:user-object:by-id:' . $user->id, 3600, function() use($user) { - $fractal = new Fractal\Manager(); - $resource = new Fractal\Resource\Item($user, new ProfileTransformer); - $res = $fractal->createData($resource)->toArray(); - return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json'); - }); - } + protected function privateProfileCheck(Profile $profile, $loggedIn) + { + if (! Auth::check()) { + return true; + } - public function showAtomFeed(Request $request, $user) - { - abort_if(!config('federation.atom.enabled'), 404); + $user = Auth::user()->profile; + if ($user->id == $profile->id || ! $profile->is_private) { + return false; + } - $pid = AccountService::usernameToId($user); + $follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists(); + if ($follows == false) { + return true; + } - abort_if(!$pid, 404); + return false; + } - $profile = AccountService::get($pid, true); + public static function accountCheck(Profile $profile) + { + switch ($profile->status) { + case 'disabled': + case 'suspended': + case 'delete': + return view('profile.disabled'); + break; - abort_if(!$profile || $profile['locked'] || !$profile['local'], 404); + default: + break; + } - $aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile['id'], 86400, function() use($profile) { - $uid = User::whereProfileId($profile['id'])->first(); - if(!$uid) { - return true; - } - $exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count(); - if($exists) { - return true; - } + return abort(404); + } - return false; - }); + protected function blockedProfileCheck(Profile $profile) + { + $pid = Auth::user()->profile->id; + $blocks = UserFilter::whereUserId($profile->id) + ->whereFilterType('block') + ->whereFilterableType('App\Profile') + ->pluck('filterable_id') + ->toArray(); + if (in_array($pid, $blocks)) { + return true; + } - abort_if($aiCheck, 404); + return false; + } - $enabled = Cache::remember('profile:atom:enabled:' . $profile['id'], 84600, function() use ($profile) { - $uid = User::whereProfileId($profile['id'])->first(); - if(!$uid) { - return false; - } - $settings = UserSetting::whereUserId($uid->id)->first(); - if(!$settings) { - return false; - } + public function showActivityPub(Request $request, $user) + { + abort_if(! config_cache('federation.activitypub.enabled'), 404); + abort_if(! $user, 404, 'Not found'); + abort_if($user->domain, 404); - return $settings->show_atom; - }); + return Cache::remember('pf:activitypub:user-object:by-id:'.$user->id, 1800, function () use ($user) { + $fractal = new Fractal\Manager(); + $resource = new Fractal\Resource\Item($user, new ProfileTransformer); + $res = $fractal->createData($resource)->toArray(); - abort_if(!$enabled, 404); + return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json'); + }); + } - $data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 900, function() use($pid, $profile) { - $items = Status::whereProfileId($pid) - ->whereScope('public') - ->whereIn('type', ['photo', 'photo:album']) - ->orderByDesc('id') - ->take(10) - ->get() - ->map(function($status) { - return StatusService::get($status->id, true); - }) - ->filter(function($status) { - return $status && - isset($status['account']) && - isset($status['media_attachments']) && - count($status['media_attachments']); - }) - ->values(); - $permalink = config('app.url') . "/users/{$profile['username']}.atom"; - $headers = ['Content-Type' => 'application/atom+xml']; + public function showAtomFeed(Request $request, $user) + { + abort_if(! config('federation.atom.enabled'), 404); - if($items && $items->count()) { - $headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String(); - } + $pid = AccountService::usernameToId($user); - return compact('items', 'permalink', 'headers'); - }); - abort_if(!$data || !isset($data['items']) || !isset($data['permalink']), 404); - return response() - ->view('atom.user', - [ - 'profile' => $profile, - 'items' => $data['items'], - 'permalink' => $data['permalink'] - ] - ) - ->withHeaders($data['headers']); - } + abort_if(! $pid, 404); - public function meRedirect() - { - abort_if(!Auth::check(), 404); - return redirect(Auth::user()->url()); - } + $profile = AccountService::get($pid, true); - public function embed(Request $request, $username) - { - $res = view('profile.embed-removed'); + abort_if(! $profile || $profile['locked'] || ! $profile['local'], 404); - if(!config('instance.embed.profile')) { - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 3600, function () use ($profile) { + $uid = User::whereProfileId($profile['id'])->first(); + if (! $uid) { + return true; + } + $exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count(); + if ($exists) { + return true; + } - if(strlen($username) > 15 || strlen($username) < 2) { - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + return false; + }); - $profile = Profile::whereUsername($username) - ->whereIsPrivate(false) - ->whereNull('status') - ->whereNull('domain') - ->first(); + abort_if($aiCheck, 404); - if(!$profile) { - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + $enabled = Cache::remember('profile:atom:enabled:'.$profile['id'], 86400, function () use ($profile) { + $uid = User::whereProfileId($profile['id'])->first(); + if (! $uid) { + return false; + } + $settings = UserSetting::whereUserId($uid->id)->first(); + if (! $settings) { + return false; + } - $aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) { - $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); - if($exists) { - return true; - } + return $settings->show_atom; + }); - return false; - }); + abort_if(! $enabled, 404); - if($aiCheck) { - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + $data = Cache::remember('pf:atom:user-feed:by-id:'.$profile['id'], 14400, function () use ($pid, $profile) { + $items = Status::whereProfileId($pid) + ->whereScope('public') + ->whereIn('type', ['photo', 'photo:album']) + ->orderByDesc('id') + ->take(10) + ->get() + ->map(function ($status) { + return StatusService::get($status->id, true); + }) + ->filter(function ($status) { + return $status && + isset($status['account']) && + isset($status['media_attachments']) && + count($status['media_attachments']); + }) + ->values(); + $permalink = config('app.url')."/users/{$profile['username']}.atom"; + $headers = ['Content-Type' => 'application/atom+xml']; - if(AccountService::canEmbed($profile->user_id) == false) { - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + if ($items && $items->count()) { + $headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String(); + } - $profile = AccountService::get($profile->id); - $res = view('profile.embed', compact('profile')); - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } + return compact('items', 'permalink', 'headers'); + }); + abort_if(! $data || ! isset($data['items']) || ! isset($data['permalink']), 404); - public function stories(Request $request, $username) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); - $pid = $profile->id; - $authed = Auth::user()->profile_id; - abort_if($pid != $authed && !FollowerService::follows($authed, $pid), 404); - $exists = Story::whereProfileId($pid) - ->whereActive(true) - ->exists(); - abort_unless($exists, 404); - return view('profile.story', compact('pid', 'profile')); - } + return response() + ->view('atom.user', + [ + 'profile' => $profile, + 'items' => $data['items'], + 'permalink' => $data['permalink'], + ] + ) + ->withHeaders($data['headers']); + } + + public function meRedirect() + { + abort_if(! Auth::check(), 404); + + return redirect(Auth::user()->url()); + } + + public function embed(Request $request, $username) + { + $res = view('profile.embed-removed'); + + if (! (bool) config_cache('instance.embed.profile')) { + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + if (strlen($username) > 15 || strlen($username) < 2) { + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + $profile = $this->getCachedUser($username); + + if (! $profile || $profile->is_private) { + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 3600, function () use ($profile) { + $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); + if ($exists) { + return true; + } + + return false; + }); + + if ($aiCheck) { + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + if (AccountService::canEmbed($profile->id) == false) { + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + $profile = AccountService::get($profile->id); + $res = view('profile.embed', compact('profile')); + + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + public function stories(Request $request, $username) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); + $pid = $profile->id; + $authed = Auth::user()->profile_id; + abort_if($pid != $authed && ! FollowerService::follows($authed, $pid), 404); + $exists = Story::whereProfileId($pid) + ->whereActive(true) + ->exists(); + abort_unless($exists, 404); + + return view('profile.story', compact('pid', 'profile')); + } } diff --git a/app/Http/Controllers/ProfileMigrationController.php b/app/Http/Controllers/ProfileMigrationController.php new file mode 100644 index 000000000..d62b96175 --- /dev/null +++ b/app/Http/Controllers/ProfileMigrationController.php @@ -0,0 +1,72 @@ +middleware('auth'); + } + + public function index(Request $request) + { + abort_if((bool) config_cache('federation.activitypub.enabled') === false, 404); + if ((bool) config_cache('federation.migration') === false) { + return redirect(route('help.account-migration')); + } + $hasExistingMigration = ProfileMigration::whereProfileId($request->user()->profile_id) + ->where('created_at', '>', now()->subDays(30)) + ->exists(); + + return view('settings.migration.index', compact('hasExistingMigration')); + } + + public function store(ProfileMigrationStoreRequest $request) + { + abort_if((bool) config_cache('federation.activitypub.enabled') === false, 404); + $acct = WebfingerService::rawGet($request->safe()->acct); + if (! $acct) { + return redirect()->back()->withErrors(['acct' => 'The new account you provided is not responding to our requests.']); + } + $newAccount = Helpers::profileFetch($acct); + if (! $newAccount) { + return redirect()->back()->withErrors(['acct' => 'An error occured, please try again later. Code: res-failed-account-fetch']); + } + $user = $request->user(); + ProfileAlias::updateOrCreate([ + 'profile_id' => $user->profile_id, + 'acct' => $request->safe()->acct, + 'uri' => $acct, + ]); + $migration = ProfileMigration::create([ + 'profile_id' => $request->user()->profile_id, + 'acct' => $request->safe()->acct, + 'followers_count' => $request->user()->profile->followers_count, + 'target_profile_id' => $newAccount['id'], + ]); + $user->profile->update([ + 'moved_to_profile_id' => $newAccount->id, + 'indexable' => false, + ]); + AccountService::del($user->profile_id); + + Bus::batch([ + new ProfileMigrationDeliverMoveActivityPipeline($migration, $user->profile, $newAccount), + new ProfileMigrationMoveFollowersPipeline($user->profile_id, $newAccount->id), + ])->onQueue('follow')->dispatch(); + + return redirect()->back()->with(['status' => 'Succesfully migrated account!']); + } +} diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index f888eb512..1c9781935 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -2,46 +2,28 @@ namespace App\Http\Controllers; +use App\Follower; +use App\Profile; +use App\Services\AccountService; +use App\Services\BookmarkService; +use App\Services\FollowerService; +use App\Services\InstanceService; +use App\Services\LikeService; +use App\Services\NetworkTimelineService; +use App\Services\PublicTimelineService; +use App\Services\ReblogService; +use App\Services\RelationshipService; +use App\Services\SnowflakeService; +use App\Services\StatusService; +use App\Services\UserFilterService; +use App\Status; +use App\Transformer\Api\StatusStatelessTransformer; +use Auth; +use Cache; use Illuminate\Http\Request; -use App\{ - Hashtag, - Follower, - Like, - Media, - Notification, - Profile, - StatusHashtag, - Status, - StatusView, - UserFilter -}; -use Auth, Cache, DB; -use Illuminate\Support\Facades\Redis; -use Carbon\Carbon; use League\Fractal; -use App\Transformer\Api\{ - AccountTransformer, - RelationshipTransformer, - StatusTransformer, - StatusStatelessTransformer -}; -use App\Services\{ - AccountService, - BookmarkService, - FollowerService, - LikeService, - PublicTimelineService, - ProfileService, - NetworkTimelineService, - ReblogService, - RelationshipService, - StatusService, - SnowflakeService, - UserFilterService -}; -use App\Jobs\StatusPipeline\NewStatusPipeline; -use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use League\Fractal\Serializer\ArraySerializer; class PublicApiController extends Controller { @@ -49,13 +31,13 @@ class PublicApiController extends Controller public function __construct() { - $this->fractal = new Fractal\Manager(); - $this->fractal->setSerializer(new ArraySerializer()); + $this->fractal = new Fractal\Manager; + $this->fractal->setSerializer(new ArraySerializer); } protected function getUserData($user) { - if(!$user) { + if (! $user) { return []; } else { return AccountService::get($user->profile_id); @@ -64,22 +46,22 @@ class PublicApiController extends Controller public function getStatus(Request $request, $id) { - abort_if(!$request->user(), 403); - $status = StatusService::get($id, false); - abort_if(!$status, 404); - if(in_array($status['visibility'], ['public', 'unlisted'])) { - return $status; - } - $pid = $request->user()->profile_id; - if($status['account']['id'] == $pid) { - return $status; - } - if($status['visibility'] == 'private') { - if(FollowerService::follows($pid, $status['account']['id'])) { - return $status; - } - } - abort(404); + abort_if(! $request->user(), 403); + $status = StatusService::get($id, false); + abort_if(! $status, 404); + if (in_array($status['visibility'], ['public', 'unlisted'])) { + return $status; + } + $pid = $request->user()->profile_id; + if ($status['account']['id'] == $pid) { + return $status; + } + if ($status['visibility'] == 'private') { + if (FollowerService::follows($pid, $status['account']['id'])) { + return $status; + } + } + abort(404); } public function status(Request $request, $username, int $postid) @@ -87,12 +69,12 @@ class PublicApiController extends Controller $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail(); $status = Status::whereProfileId($profile->id)->findOrFail($postid); $this->scopeCheck($profile, $status); - if(!$request->user()) { + if (! $request->user()) { $cached = StatusService::get($status->id, false); - abort_if(!in_array($cached['visibility'], ['public', 'unlisted']), 403); + abort_if(! in_array($cached['visibility'], ['public', 'unlisted']), 403); $res = ['status' => $cached]; } else { - $item = new Fractal\Resource\Item($status, new StatusStatelessTransformer()); + $item = new Fractal\Resource\Item($status, new StatusStatelessTransformer); $res = [ 'status' => $this->fractal->createData($item)->toArray(), ]; @@ -106,7 +88,7 @@ class PublicApiController extends Controller $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail(); $status = Status::whereProfileId($profile->id)->findOrFail($postid); $this->scopeCheck($profile, $status); - if(!Auth::check()) { + if (! Auth::check()) { $res = [ 'user' => [], 'likes' => [], @@ -117,6 +99,7 @@ class PublicApiController extends Controller 'bookmarked' => false, ], ]; + return response()->json($res); } $res = [ @@ -129,15 +112,16 @@ class PublicApiController extends Controller 'bookmarked' => (bool) $status->bookmarked(), ], ]; + return response()->json($res); } public function statusComments(Request $request, $username, int $postId) { $this->validate($request, [ - 'min_id' => 'nullable|integer|min:1', - 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, - 'limit' => 'nullable|integer|min:5|max:50' + 'min_id' => 'nullable|integer|min:1', + 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, + 'limit' => 'nullable|integer|min:5|max:50', ]); $limit = $request->limit ?? 10; @@ -145,50 +129,51 @@ class PublicApiController extends Controller $status = Status::whereProfileId($profile->id)->whereCommentsDisabled(false)->findOrFail($postId); $this->scopeCheck($profile, $status); - if(Auth::check()) { + if (Auth::check()) { $p = Auth::user()->profile; - $scope = $p->id == $status->profile_id || FollowerService::follows($p->id, $profile->id) ? ['public', 'private', 'unlisted'] : ['public','unlisted']; + $scope = $p->id == $status->profile_id || FollowerService::follows($p->id, $profile->id) ? ['public', 'private', 'unlisted'] : ['public', 'unlisted']; } else { $scope = ['public', 'unlisted']; } - if($request->filled('min_id') || $request->filled('max_id')) { - if($request->filled('min_id')) { + if ($request->filled('min_id') || $request->filled('max_id')) { + if ($request->filled('min_id')) { $replies = $status->comments() - ->whereNull('reblog_of_id') - ->whereIn('scope', $scope) - ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') - ->where('id', '>=', $request->min_id) - ->orderBy('id', 'desc') - ->paginate($limit); + ->whereNull('reblog_of_id') + ->whereIn('scope', $scope) + ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') + ->where('id', '>=', $request->min_id) + ->orderBy('id', 'desc') + ->paginate($limit); } - if($request->filled('max_id')) { + if ($request->filled('max_id')) { $replies = $status->comments() - ->whereNull('reblog_of_id') - ->whereIn('scope', $scope) - ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') - ->where('id', '<=', $request->max_id) - ->orderBy('id', 'desc') - ->paginate($limit); + ->whereNull('reblog_of_id') + ->whereIn('scope', $scope) + ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') + ->where('id', '<=', $request->max_id) + ->orderBy('id', 'desc') + ->paginate($limit); } } else { $replies = Status::whereInReplyToId($status->id) - ->whereNull('reblog_of_id') - ->whereIn('scope', $scope) - ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') - ->orderBy('id', 'desc') - ->paginate($limit); + ->whereNull('reblog_of_id') + ->whereIn('scope', $scope) + ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') + ->orderBy('id', 'desc') + ->paginate($limit); } - $resource = new Fractal\Resource\Collection($replies, new StatusStatelessTransformer(), 'data'); + $resource = new Fractal\Resource\Collection($replies, new StatusStatelessTransformer, 'data'); $resource->setPaginator(new IlluminatePaginatorAdapter($replies)); $res = $this->fractal->createData($resource)->toArray(); + return response()->json($res, 200, [], JSON_PRETTY_PRINT); } protected function scopeCheck(Profile $profile, Status $status) { - if($profile->is_private == true && Auth::check() == false) { + if ($profile->is_private == true && Auth::check() == false) { abort(404); } @@ -198,11 +183,11 @@ class PublicApiController extends Controller break; case 'private': $user = Auth::check() ? Auth::user() : false; - if(!$user) { + if (! $user) { abort(403); } else { - $follows = $profile->followedBy($user->profile); - if($follows == false && $profile->id !== $user->profile->id && $user->is_admin == false) { + $follows = FollowerService::follows($profile->id, $user->profile_id); + if ($follows == false && $profile->id !== $user->profile_id && $user->is_admin == false) { abort(404); } } @@ -224,14 +209,14 @@ class PublicApiController extends Controller public function publicTimelineApi(Request $request) { - $this->validate($request,[ - 'page' => 'nullable|integer|max:40', - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|max:30' + $this->validate($request, [ + 'page' => 'nullable|integer|max:40', + 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'limit' => 'nullable|integer|max:30', ]); - if(!$request->user()) { + if (! $request->user()) { return response('', 403); } @@ -243,123 +228,125 @@ class PublicApiController extends Controller $filtered = $user ? UserFilterService::filters($user->profile_id) : []; $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); - if(config('exp.cached_public_timeline') == false) { - if($min || $max) { + if (config('exp.cached_public_timeline') == false) { + if ($min || $max) { $dir = $min ? '>' : '<'; $id = $min ?? $max; $timeline = Status::select( - 'id', - 'profile_id', - 'type', - 'scope', - 'local' - ) - ->where('id', $dir, $id) - ->whereNull(['in_reply_to_id', 'reblog_of_id']) - ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) - ->whereLocal(true) - ->when($hideNsfw, function($q, $hideNsfw) { - return $q->where('is_nsfw', false); - }) - ->whereScope('public') - ->orderBy('id', 'desc') - ->limit($limit) - ->get() - ->map(function($s) use ($user) { - $status = StatusService::getFull($s->id, $user->profile_id); - if(!$status) { - return false; - } - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); - return $status; - }) - ->filter(function($s) use($filtered) { - return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; - }) - ->values(); + 'id', + 'profile_id', + 'type', + 'scope', + 'local' + ) + ->where('id', $dir, $id) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereLocal(true) + ->when($hideNsfw, function ($q, $hideNsfw) { + return $q->where('is_nsfw', false); + }) + ->whereScope('public') + ->orderBy('id', 'desc') + ->limit($limit) + ->get() + ->map(function ($s) use ($user) { + $status = StatusService::getFull($s->id, $user->profile_id); + if (! $status) { + return false; + } + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); + + return $status; + }) + ->filter(function ($s) use ($filtered) { + return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; + }) + ->values(); $res = $timeline->toArray(); } else { $timeline = Status::select( - 'id', - 'uri', - 'caption', - 'rendered', - 'profile_id', - 'type', - 'in_reply_to_id', - 'reblog_of_id', - 'is_nsfw', - 'scope', - 'local', - 'reply_count', - 'comments_disabled', - 'created_at', - 'place_id', - 'likes_count', - 'reblogs_count', - 'updated_at' - ) - ->whereNull(['in_reply_to_id', 'reblog_of_id']) - ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) - ->whereLocal(true) - ->when($hideNsfw, function($q, $hideNsfw) { - return $q->where('is_nsfw', false); - }) - ->whereScope('public') - ->orderBy('id', 'desc') - ->limit($limit) - ->get() - ->map(function($s) use ($user) { - $status = StatusService::getFull($s->id, $user->profile_id); - if(!$status) { - return false; - } - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); - return $status; - }) - ->filter(function($s) use($filtered) { - return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; - }) - ->values(); + 'id', + 'uri', + 'caption', + 'profile_id', + 'type', + 'in_reply_to_id', + 'reblog_of_id', + 'is_nsfw', + 'scope', + 'local', + 'reply_count', + 'comments_disabled', + 'created_at', + 'place_id', + 'likes_count', + 'reblogs_count', + 'updated_at' + ) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereLocal(true) + ->when($hideNsfw, function ($q, $hideNsfw) { + return $q->where('is_nsfw', false); + }) + ->whereScope('public') + ->orderBy('id', 'desc') + ->limit($limit) + ->get() + ->map(function ($s) use ($user) { + $status = StatusService::getFull($s->id, $user->profile_id); + if (! $status) { + return false; + } + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); + + return $status; + }) + ->filter(function ($s) use ($filtered) { + return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; + }) + ->values(); $res = $timeline->toArray(); } } else { - Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { - if(PublicTimelineService::count() == 0) { + Cache::remember('api:v1:timelines:public:cache_check', 10368000, function () { + if (PublicTimelineService::count() == 0) { PublicTimelineService::warmCache(true, 400); } }); if ($max) { $feed = PublicTimelineService::getRankedMaxId($max, $limit); - } else if ($min) { + } elseif ($min) { $feed = PublicTimelineService::getRankedMinId($min, $limit); } else { $feed = PublicTimelineService::get(0, $limit); } $res = collect($feed) - ->take($limit) - ->map(function($k) use($user) { - $status = StatusService::get($k); - if($status && isset($status['account']) && $user) { - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); - $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $k); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $k); - $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']); - } - return $status; - }) - ->filter(function($s) use($filtered) { - return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; - }) - ->values() - ->toArray(); + ->take($limit) + ->map(function ($k) use ($user) { + $status = StatusService::get($k); + if ($status && isset($status['account']) && $user) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $k); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $k); + $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']); + } + + return $status; + }) + ->filter(function ($s) use ($filtered) { + return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; + }) + ->values() + ->toArray(); } return response()->json($res); @@ -367,17 +354,17 @@ class PublicApiController extends Controller public function homeTimelineApi(Request $request) { - if(!$request->user()) { + if (! $request->user()) { return response('', 403); } - $this->validate($request,[ - 'page' => 'nullable|integer|max:40', - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|max:40', - 'recent_feed' => 'nullable', - 'recent_min' => 'nullable|integer' + $this->validate($request, [ + 'page' => 'nullable|integer|max:40', + 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'limit' => 'nullable|integer|max:40', + 'recent_feed' => 'nullable', + 'recent_min' => 'nullable|integer', ]); $recentFeed = $request->input('recent_feed') == 'true'; @@ -389,7 +376,7 @@ class PublicApiController extends Controller $user = $request->user(); $key = 'user:last_active_at:id:'.$user->id; - if(Cache::get($key) == null) { + if (Cache::get($key) == null) { $user->last_active_at = now(); $user->save(); Cache::put($key, true, 43200); @@ -397,8 +384,9 @@ class PublicApiController extends Controller $pid = $user->profile_id; - $following = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { + $following = Cache::remember('profile:following:'.$pid, 1209600, function () use ($pid) { $following = Follower::whereProfileId($pid)->pluck('following_id'); + return $following->push($pid)->toArray(); }); @@ -408,123 +396,124 @@ class PublicApiController extends Controller $textOnlyReplies = false; - if($min || $max) { + if ($min || $max) { $dir = $min ? '>' : '<'; $id = $min ?? $max; - return Status::select( - 'id', - 'uri', - 'caption', - 'rendered', - 'profile_id', - 'type', - 'in_reply_to_id', - 'reblog_of_id', - 'is_nsfw', - 'scope', - 'local', - 'reply_count', - 'comments_disabled', - 'place_id', - 'likes_count', - 'reblogs_count', - 'created_at', - 'updated_at' - ) - ->whereIn('type', $types) - ->when(!$textOnlyReplies, function($q, $textOnlyReplies) { - return $q->whereNull('in_reply_to_id'); - }) - ->where('id', $dir, $id) - ->whereIn('profile_id', $following) - ->whereIn('visibility',['public', 'unlisted', 'private']) - ->orderBy('created_at', 'desc') - ->limit($limit) - ->get() - ->map(function($s) use ($user) { - try { - $status = StatusService::get($s->id, false); - if(!$status) { - return false; - } - } catch(\Exception $e) { - return false; - } - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); - return $status; - }) - ->filter(function($s) use($filtered) { - return $s && in_array($s['account']['id'], $filtered) == false; - }) - ->values() - ->toArray(); + + return Status::select( + 'id', + 'uri', + 'caption', + 'profile_id', + 'type', + 'in_reply_to_id', + 'reblog_of_id', + 'is_nsfw', + 'scope', + 'local', + 'reply_count', + 'comments_disabled', + 'place_id', + 'likes_count', + 'reblogs_count', + 'created_at', + 'updated_at' + ) + ->whereIn('type', $types) + ->when(! $textOnlyReplies, function ($q, $textOnlyReplies) { + return $q->whereNull('in_reply_to_id'); + }) + ->where('id', $dir, $id) + ->whereIn('profile_id', $following) + ->whereIn('visibility', ['public', 'unlisted', 'private']) + ->orderBy('created_at', 'desc') + ->limit($limit) + ->get() + ->map(function ($s) use ($user) { + try { + $status = StatusService::get($s->id, false); + if (! $status) { + return false; + } + } catch (\Exception $e) { + return false; + } + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); + + return $status; + }) + ->filter(function ($s) use ($filtered) { + return $s && in_array($s['account']['id'], $filtered) == false; + }) + ->values() + ->toArray(); } else { return Status::select( - 'id', - 'uri', - 'caption', - 'rendered', - 'profile_id', - 'type', - 'in_reply_to_id', - 'reblog_of_id', - 'is_nsfw', - 'scope', - 'local', - 'reply_count', - 'comments_disabled', - 'place_id', - 'likes_count', - 'reblogs_count', - 'created_at', - 'updated_at' - ) - ->whereIn('type', $types) - ->when(!$textOnlyReplies, function($q, $textOnlyReplies) { - return $q->whereNull('in_reply_to_id'); - }) - ->whereIn('profile_id', $following) - ->whereIn('visibility',['public', 'unlisted', 'private']) - ->orderBy('created_at', 'desc') - ->limit($limit) - ->get() - ->map(function($s) use ($user) { - try { - $status = StatusService::get($s->id, false); - if(!$status) { - return false; - } - } catch(\Exception $e) { - return false; - } - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); - return $status; - }) - ->filter(function($s) use($filtered) { - return $s && in_array($s['account']['id'], $filtered) == false; - }) - ->values() - ->toArray(); + 'id', + 'uri', + 'caption', + 'profile_id', + 'type', + 'in_reply_to_id', + 'reblog_of_id', + 'is_nsfw', + 'scope', + 'local', + 'reply_count', + 'comments_disabled', + 'place_id', + 'likes_count', + 'reblogs_count', + 'created_at', + 'updated_at' + ) + ->whereIn('type', $types) + ->when(! $textOnlyReplies, function ($q, $textOnlyReplies) { + return $q->whereNull('in_reply_to_id'); + }) + ->whereIn('profile_id', $following) + ->whereIn('visibility', ['public', 'unlisted', 'private']) + ->orderBy('created_at', 'desc') + ->limit($limit) + ->get() + ->map(function ($s) use ($user) { + try { + $status = StatusService::get($s->id, false); + if (! $status) { + return false; + } + } catch (\Exception $e) { + return false; + } + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); + + return $status; + }) + ->filter(function ($s) use ($filtered) { + return $s && in_array($s['account']['id'], $filtered) == false; + }) + ->values() + ->toArray(); } } public function networkTimelineApi(Request $request) { - if(!$request->user()) { + if (! $request->user()) { return response('', 403); } abort_if(config('federation.network_timeline') == false, 404); - $this->validate($request,[ - 'page' => 'nullable|integer|max:40', - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|max:30' + $this->validate($request, [ + 'page' => 'nullable|integer|max:40', + 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'limit' => 'nullable|integer|max:30', ]); $page = $request->input('page'); @@ -537,99 +526,102 @@ class PublicApiController extends Controller $filtered = $user ? UserFilterService::filters($user->profile_id) : []; $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); - if(config('instance.timeline.network.cached') == false) { - if($min || $max) { - $dir = $min ? '>' : '<'; - $id = $min ?? $max; - $timeline = Status::select( - 'id', - 'uri', - 'type', - 'scope', - 'created_at', - ) - ->where('id', $dir, $id) - ->when($hideNsfw, function($q, $hideNsfw) { - return $q->where('is_nsfw', false); - }) - ->whereNull(['in_reply_to_id', 'reblog_of_id']) - ->whereNotIn('profile_id', $filtered) - ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) - ->whereNotNull('uri') - ->whereScope('public') - ->where('id', '>', $amin) - ->orderBy('created_at', 'desc') - ->limit($limit) - ->get() - ->map(function($s) use ($user) { - $status = StatusService::get($s->id); - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); - return $status; - }); - $res = $timeline->toArray(); - } else { - $timeline = Status::select( - 'id', - 'uri', - 'type', - 'scope', - 'created_at', - ) - ->whereNull(['in_reply_to_id', 'reblog_of_id']) - ->whereNotIn('profile_id', $filtered) - ->when($hideNsfw, function($q, $hideNsfw) { - return $q->where('is_nsfw', false); - }) - ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) - ->whereNotNull('uri') - ->whereScope('public') - ->where('id', '>', $amin) - ->orderBy('created_at', 'desc') - ->limit($limit) - ->get() - ->map(function($s) use ($user) { - $status = StatusService::get($s->id); - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); - return $status; - }); - $res = $timeline->toArray(); - } - } else { - Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() { - if(NetworkTimelineService::count() == 0) { + if (config('instance.timeline.network.cached') == false) { + if ($min || $max) { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + $timeline = Status::select( + 'id', + 'uri', + 'type', + 'scope', + 'created_at', + ) + ->where('id', $dir, $id) + ->when($hideNsfw, function ($q, $hideNsfw) { + return $q->where('is_nsfw', false); + }) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->whereNotIn('profile_id', $filtered) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereNotNull('uri') + ->whereScope('public') + ->where('id', '>', $amin) + ->orderBy('created_at', 'desc') + ->limit($limit) + ->get() + ->map(function ($s) use ($user) { + $status = StatusService::get($s->id); + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); + + return $status; + }); + $res = $timeline->toArray(); + } else { + $timeline = Status::select( + 'id', + 'uri', + 'type', + 'scope', + 'created_at', + ) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->whereNotIn('profile_id', $filtered) + ->when($hideNsfw, function ($q, $hideNsfw) { + return $q->where('is_nsfw', false); + }) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereNotNull('uri') + ->whereScope('public') + ->where('id', '>', $amin) + ->orderBy('created_at', 'desc') + ->limit($limit) + ->get() + ->map(function ($s) use ($user) { + $status = StatusService::get($s->id); + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); + + return $status; + }); + $res = $timeline->toArray(); + } + } else { + Cache::remember('api:v1:timelines:network:cache_check', 10368000, function () { + if (NetworkTimelineService::count() == 0) { NetworkTimelineService::warmCache(true, 400); } }); if ($max) { $feed = NetworkTimelineService::getRankedMaxId($max, $limit); - } else if ($min) { + } elseif ($min) { $feed = NetworkTimelineService::getRankedMinId($min, $limit); } else { $feed = NetworkTimelineService::get(0, $limit); } $res = collect($feed) - ->take($limit) - ->map(function($k) use($user) { - $status = StatusService::get($k); - if($status && isset($status['account']) && $user) { - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); - $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $k); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $k); - $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']); - } - return $status; - }) - ->filter(function($s) use($filtered) { - return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; - }) - ->values() - ->toArray(); + ->take($limit) + ->map(function ($k) use ($user) { + $status = StatusService::get($k); + if ($status && isset($status['account']) && $user) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $k); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $k); + $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']); + } + + return $status; + }) + ->filter(function ($s) use ($filtered) { + return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; + }) + ->values() + ->toArray(); } return response()->json($res); @@ -637,23 +629,23 @@ class PublicApiController extends Controller public function relationships(Request $request) { - if(!Auth::check()) { + if (! Auth::check()) { return response()->json([]); } $pid = $request->user()->profile_id; $this->validate($request, [ - 'id' => 'required|array|min:1|max:20', - 'id.*' => 'required|integer' + 'id' => 'required|array|min:1|max:20', + 'id.*' => 'required|integer', ]); $ids = collect($request->input('id')); - $res = $ids->filter(function($v) use($pid) { + $res = $ids->filter(function ($v) use ($pid) { return $v != $pid; }) - ->map(function($id) use($pid) { - return RelationshipService::get($pid, $id); - }); + ->map(function ($id) use ($pid) { + return RelationshipService::get($pid, $id); + }); return response()->json($res); } @@ -661,6 +653,11 @@ class PublicApiController extends Controller public function account(Request $request, $id) { $res = AccountService::get($id); + if ($res && isset($res['local'], $res['url']) && ! $res['local']) { + $domain = parse_url($res['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + return response()->json($res); } @@ -670,15 +667,20 @@ class PublicApiController extends Controller 'only_media' => 'nullable', 'pinned' => 'nullable', 'exclude_replies' => 'nullable', - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|min:1|max:24' + 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, + 'limit' => 'nullable|integer|min:1|max:24', ]); $user = $request->user(); $profile = AccountService::get($id); - abort_if(!$profile, 404); + abort_if(! $profile, 404); + + if ($profile && isset($profile['local'], $profile['url']) && ! $profile['local']) { + $domain = parse_url($profile['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } $limit = $request->limit ?? 9; $max_id = $request->max_id; @@ -686,28 +688,30 @@ class PublicApiController extends Controller $scope = ['photo', 'photo:album', 'video', 'video:album']; $onlyMedia = $request->input('only_media', true); - if(!$min_id && !$max_id) { - $min_id = 1; + if (! $min_id && ! $max_id) { + $min_id = 1; } - if($profile['locked']) { - if(!$user) { + if ($profile['locked']) { + if (! $user) { return response()->json([]); } $pid = $user->profile_id; - $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) { + $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) { $following = Follower::whereProfileId($pid)->pluck('following_id'); + return $following->push($pid)->toArray(); }); - $visibility = true == in_array($profile['id'], $following) ? ['public', 'unlisted', 'private'] : []; + $visibility = in_array($profile['id'], $following) == true ? ['public', 'unlisted', 'private'] : []; } else { - if($user) { + if ($user) { $pid = $user->profile_id; - $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) { + $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) { $following = Follower::whereProfileId($pid)->pluck('following_id'); + return $following->push($pid)->toArray(); }); - $visibility = true == in_array($profile['id'], $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; + $visibility = in_array($profile['id'], $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; } else { $visibility = ['public', 'unlisted']; } @@ -715,38 +719,40 @@ class PublicApiController extends Controller $dir = $min_id ? '>' : '<'; $id = $min_id ?? $max_id; $res = Status::whereProfileId($profile['id']) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->whereIn('type', $scope) - ->where('id', $dir, $id) - ->whereIn('scope', $visibility) - ->limit($limit) - ->orderByDesc('id') - ->get() - ->map(function($s) use($user) { - try { - $status = StatusService::get($s->id, false); - } catch (\Exception $e) { - $status = false; - } - if($user && $status) { - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - } - return $status; - }) - ->filter(function($s) use($onlyMedia) { - if($onlyMedia) { - if( - !isset($s['media_attachments']) || - !is_array($s['media_attachments']) || - empty($s['media_attachments']) - ) { - return false; - } - } - return $s; - }) - ->values(); + ->whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->whereIn('type', $scope) + ->where('id', $dir, $id) + ->whereIn('scope', $visibility) + ->limit($limit) + ->orderByDesc('id') + ->get() + ->map(function ($s) use ($user) { + try { + $status = StatusService::get($s->id, false); + } catch (\Exception $e) { + $status = false; + } + if ($user && $status) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + } + + return $status; + }) + ->filter(function ($s) use ($onlyMedia) { + if ($onlyMedia) { + if ( + ! isset($s['media_attachments']) || + ! is_array($s['media_attachments']) || + empty($s['media_attachments']) + ) { + return false; + } + } + + return $s; + }) + ->values(); return response()->json($res); } diff --git a/app/Http/Controllers/RemoteAuthController.php b/app/Http/Controllers/RemoteAuthController.php index e068f5d75..e0afd82ef 100644 --- a/app/Http/Controllers/RemoteAuthController.php +++ b/app/Http/Controllers/RemoteAuthController.php @@ -2,22 +2,20 @@ namespace App\Http\Controllers; -use Illuminate\Support\Str; -use Illuminate\Http\Request; -use App\Services\Account\RemoteAuthService; use App\Models\RemoteAuth; -use App\Profile; -use App\Instance; -use App\User; -use Purify; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; -use Illuminate\Auth\Events\Registered; -use App\Util\Lexer\RestrictedNames; +use App\Services\Account\RemoteAuthService; use App\Services\EmailService; use App\Services\MediaStorageService; +use App\User; use App\Util\ActivityPub\Helpers; +use App\Util\Lexer\RestrictedNames; +use Illuminate\Auth\Events\Registered; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; use InvalidArgumentException; +use Purify; class RemoteAuthController extends Controller { @@ -30,9 +28,10 @@ class RemoteAuthController extends Controller config('remote-auth.mastodon.ignore_closed_state') && config('remote-auth.mastodon.enabled') ), 404); - if($request->user()) { + if ($request->user()) { return redirect('/'); } + return view('auth.remote.start'); } @@ -51,25 +50,27 @@ class RemoteAuthController extends Controller config('remote-auth.mastodon.enabled') ), 404); - if(config('remote-auth.mastodon.domains.only_custom')) { + if (config('remote-auth.mastodon.domains.only_custom')) { $res = config('remote-auth.mastodon.domains.custom'); - if(!$res || !strlen($res)) { + if (! $res || ! strlen($res)) { return []; } $res = explode(',', $res); + return response()->json($res); } - if( config('remote-auth.mastodon.domains.custom') && - !config('remote-auth.mastodon.domains.only_default') && + if (config('remote-auth.mastodon.domains.custom') && + ! config('remote-auth.mastodon.domains.only_default') && strlen(config('remote-auth.mastodon.domains.custom')) > 3 && strpos(config('remote-auth.mastodon.domains.custom'), '.') > -1 ) { $res = config('remote-auth.mastodon.domains.custom'); - if(!$res || !strlen($res)) { + if (! $res || ! strlen($res)) { return []; } $res = explode(',', $res); + return response()->json($res); } @@ -93,57 +94,62 @@ class RemoteAuthController extends Controller $domain = $request->input('domain'); - if(str_starts_with(strtolower($domain), 'http')) { + if (str_starts_with(strtolower($domain), 'http')) { $res = [ 'domain' => $domain, 'ready' => false, - 'action' => 'incompatible_domain' + 'action' => 'incompatible_domain', ]; + return response()->json($res); } - $validateInstance = Helpers::validateUrl('https://' . $domain . '/?block-check=' . time()); + $validateInstance = Helpers::validateUrl('https://'.$domain.'/?block-check='.time()); - if(!$validateInstance) { - $res = [ + if (! $validateInstance) { + $res = [ 'domain' => $domain, 'ready' => false, - 'action' => 'blocked_domain' + 'action' => 'blocked_domain', ]; + return response()->json($res); } $compatible = RemoteAuthService::isDomainCompatible($domain); - if(!$compatible) { + if (! $compatible) { $res = [ 'domain' => $domain, 'ready' => false, - 'action' => 'incompatible_domain' + 'action' => 'incompatible_domain', ]; + return response()->json($res); } - if(config('remote-auth.mastodon.domains.only_default')) { + if (config('remote-auth.mastodon.domains.only_default')) { $defaultDomains = explode(',', config('remote-auth.mastodon.domains.default')); - if(!in_array($domain, $defaultDomains)) { + if (! in_array($domain, $defaultDomains)) { $res = [ 'domain' => $domain, 'ready' => false, - 'action' => 'incompatible_domain' + 'action' => 'incompatible_domain', ]; + return response()->json($res); } } - if(config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) { + if (config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) { $customDomains = explode(',', config('remote-auth.mastodon.domains.custom')); - if(!in_array($domain, $customDomains)) { + if (! in_array($domain, $customDomains)) { $res = [ 'domain' => $domain, 'ready' => false, - 'action' => 'incompatible_domain' + 'action' => 'incompatible_domain', ]; + return response()->json($res); } } @@ -163,13 +169,13 @@ class RemoteAuthController extends Controller 'state' => $state, ]); - $request->session()->put('oauth_redirect_to', 'https://' . $domain . '/oauth/authorize?' . $query); + $request->session()->put('oauth_redirect_to', 'https://'.$domain.'/oauth/authorize?'.$query); $dsh = Str::random(17); $res = [ 'domain' => $domain, 'ready' => true, - 'dsh' => $dsh + 'dsh' => $dsh, ]; return response()->json($res); @@ -185,7 +191,7 @@ class RemoteAuthController extends Controller config('remote-auth.mastodon.enabled') ), 404); - if(!$request->filled('d') || !$request->filled('dsh') || !$request->session()->exists('oauth_redirect_to')) { + if (! $request->filled('d') || ! $request->filled('dsh') || ! $request->session()->exists('oauth_redirect_to')) { return redirect('/login'); } @@ -204,7 +210,7 @@ class RemoteAuthController extends Controller $domain = $request->session()->get('oauth_domain'); - if($request->filled('code')) { + if ($request->filled('code')) { $code = $request->input('code'); $state = $request->session()->pull('state'); @@ -216,12 +222,14 @@ class RemoteAuthController extends Controller $res = RemoteAuthService::getToken($domain, $code); - if(!$res || !isset($res['access_token'])) { + if (! $res || ! isset($res['access_token'])) { $request->session()->regenerate(); + return redirect('/login'); } $request->session()->put('oauth_remote_session_token', $res['access_token']); + return redirect('/auth/mastodon/getting-started'); } @@ -237,9 +245,10 @@ class RemoteAuthController extends Controller config('remote-auth.mastodon.ignore_closed_state') && config('remote-auth.mastodon.enabled') ), 404); - if($request->user()) { + if ($request->user()) { return redirect('/'); } + return view('auth.remote.onboarding'); } @@ -261,36 +270,36 @@ class RemoteAuthController extends Controller $res = RemoteAuthService::getVerifyCredentials($domain, $token); - abort_if(!$res || !isset($res['acct']), 403, 'Invalid credentials'); + abort_if(! $res || ! isset($res['acct']), 403, 'Invalid credentials'); - $webfinger = strtolower('@' . $res['acct'] . '@' . $domain); + $webfinger = strtolower('@'.$res['acct'].'@'.$domain); $request->session()->put('oauth_masto_webfinger', $webfinger); - if(config('remote-auth.mastodon.max_uses.enabled')) { + if (config('remote-auth.mastodon.max_uses.enabled')) { $limit = config('remote-auth.mastodon.max_uses.limit'); $uses = RemoteAuthService::lookupWebfingerUses($webfinger); - if($uses >= $limit) { + if ($uses >= $limit) { return response()->json([ 'code' => 200, 'msg' => 'Success!', - 'action' => 'max_uses_reached' + 'action' => 'max_uses_reached', ]); } } $exists = RemoteAuth::whereDomain($domain)->where('webfinger', $webfinger)->whereNotNull('user_id')->first(); - if($exists && $exists->user_id) { + if ($exists && $exists->user_id) { return response()->json([ 'code' => 200, 'msg' => 'Success!', - 'action' => 'redirect_existing_user' + 'action' => 'redirect_existing_user', ]); } return response()->json([ 'code' => 200, 'msg' => 'Success!', - 'action' => 'onboard' + 'action' => 'onboard', ]); } @@ -311,7 +320,7 @@ class RemoteAuthController extends Controller $token = $request->session()->get('oauth_remote_session_token'); $res = RemoteAuthService::getVerifyCredentials($domain, $token); - $res['_webfinger'] = strtolower('@' . $res['acct'] . '@' . $domain); + $res['_webfinger'] = strtolower('@'.$res['acct'].'@'.$domain); $res['_domain'] = strtolower($domain); $request->session()->put('oauth_remasto_id', $res['id']); @@ -324,7 +333,7 @@ class RemoteAuthController extends Controller 'bearer_token' => $token, 'verify_credentials' => $res, 'last_verify_credentials_at' => now(), - 'last_successful_login_at' => now() + 'last_successful_login_at' => now(), ]); $request->session()->put('oauth_masto_raid', $ra->id); @@ -355,24 +364,24 @@ class RemoteAuthController extends Controller $underscore = substr_count($value, '_'); $period = substr_count($value, '.'); - if(ends_with($value, ['.php', '.js', '.css'])) { + if (ends_with($value, ['.php', '.js', '.css'])) { return $fail('Username is invalid.'); } - if(($dash + $underscore + $period) > 1) { + if (($dash + $underscore + $period) > 1) { return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); } - if (!ctype_alnum($value[0])) { + if (! ctype_alnum($value[0])) { return $fail('Username is invalid. Must start with a letter or number.'); } - if (!ctype_alnum($value[strlen($value) - 1])) { + if (! ctype_alnum($value[strlen($value) - 1])) { return $fail('Username is invalid. Must end with a letter or number.'); } $val = str_replace(['_', '.', '-'], '', $value); - if(!ctype_alnum($val)) { + if (! ctype_alnum($val)) { return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); } @@ -380,8 +389,8 @@ class RemoteAuthController extends Controller if (in_array(strtolower($value), array_map('strtolower', $restricted))) { return $fail('Username cannot be used.'); } - } - ] + }, + ], ]); $username = strtolower($request->input('username')); @@ -390,7 +399,7 @@ class RemoteAuthController extends Controller return response()->json([ 'code' => 200, 'username' => $username, - 'exists' => $exists + 'exists' => $exists, ]); } @@ -411,7 +420,7 @@ class RemoteAuthController extends Controller 'email' => [ 'required', 'email:strict,filter_unicode,dns,spoof', - ] + ], ]); $email = $request->input('email'); @@ -422,7 +431,7 @@ class RemoteAuthController extends Controller 'code' => 200, 'email' => $email, 'exists' => $exists, - 'banned' => $banned + 'banned' => $banned, ]); } @@ -445,18 +454,18 @@ class RemoteAuthController extends Controller $res = RemoteAuthService::getFollowing($domain, $token, $id); - if(!$res) { + if (! $res) { return response()->json([ 'code' => 200, - 'following' => [] + 'following' => [], ]); } - $res = collect($res)->filter(fn($acct) => Helpers::validateUrl($acct['url']))->values()->toArray(); + $res = collect($res)->filter(fn ($acct) => Helpers::validateUrl($acct['url']))->values()->toArray(); return response()->json([ 'code' => 200, - 'following' => $res + 'following' => $res, ]); } @@ -487,24 +496,24 @@ class RemoteAuthController extends Controller $underscore = substr_count($value, '_'); $period = substr_count($value, '.'); - if(ends_with($value, ['.php', '.js', '.css'])) { + if (ends_with($value, ['.php', '.js', '.css'])) { return $fail('Username is invalid.'); } - if(($dash + $underscore + $period) > 1) { + if (($dash + $underscore + $period) > 1) { return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); } - if (!ctype_alnum($value[0])) { + if (! ctype_alnum($value[0])) { return $fail('Username is invalid. Must start with a letter or number.'); } - if (!ctype_alnum($value[strlen($value) - 1])) { + if (! ctype_alnum($value[strlen($value) - 1])) { return $fail('Username is invalid. Must end with a letter or number.'); } $val = str_replace(['_', '.', '-'], '', $value); - if(!ctype_alnum($val)) { + if (! ctype_alnum($val)) { return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); } @@ -512,10 +521,10 @@ class RemoteAuthController extends Controller if (in_array(strtolower($value), array_map('strtolower', $restricted))) { return $fail('Username cannot be used.'); } - } + }, ], 'password' => 'required|string|min:8|confirmed', - 'name' => 'nullable|max:30' + 'name' => 'nullable|max:30', ]); $email = $request->input('email'); @@ -527,7 +536,7 @@ class RemoteAuthController extends Controller 'name' => $name, 'username' => $username, 'password' => $password, - 'email' => $email + 'email' => $email, ]); $raid = $request->session()->pull('oauth_masto_raid'); @@ -541,7 +550,7 @@ class RemoteAuthController extends Controller return [ 'code' => 200, 'msg' => 'Success', - 'token' => $token + 'token' => $token, ]; } @@ -585,7 +594,7 @@ class RemoteAuthController extends Controller abort_unless($request->session()->exists('oauth_remasto_id'), 403); $this->validate($request, [ - 'account' => 'required|url' + 'account' => 'required|url', ]); $account = $request->input('account'); @@ -594,10 +603,10 @@ class RemoteAuthController extends Controller $host = strtolower(config('pixelfed.domain.app')); $domain = strtolower(parse_url($account, PHP_URL_HOST)); - if($domain == $host) { + if ($domain == $host) { $username = Str::of($account)->explode('/')->last(); $user = User::where('username', $username)->first(); - if($user) { + if ($user) { return ['id' => (string) $user->profile_id]; } else { return []; @@ -605,7 +614,7 @@ class RemoteAuthController extends Controller } else { try { $profile = Helpers::profileFetch($account); - if($profile) { + if ($profile) { return ['id' => (string) $profile->id]; } else { return []; @@ -635,13 +644,13 @@ class RemoteAuthController extends Controller $user = $request->user(); $profile = $user->profile; - abort_if(!$profile->avatar, 404, 'Missing avatar'); + abort_if(! $profile->avatar, 404, 'Missing avatar'); $avatar = $profile->avatar; $avatar->remote_url = $request->input('avatar_url'); $avatar->save(); - MediaStorageService::avatar($avatar, config_cache('pixelfed.cloud_storage') == false); + MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false); return [200]; } @@ -657,7 +666,7 @@ class RemoteAuthController extends Controller ), 404); abort_unless($request->user(), 404); - $currentWebfinger = '@' . $request->user()->username . '@' . config('pixelfed.domain.app'); + $currentWebfinger = '@'.$request->user()->username.'@'.config('pixelfed.domain.app'); $ra = RemoteAuth::where('user_id', $request->user()->id)->firstOrFail(); RemoteAuthService::submitToBeagle( $ra->webfinger, @@ -691,19 +700,20 @@ class RemoteAuthController extends Controller $user = User::findOrFail($ra->user_id); abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action'); Auth::loginUsingId($ra->user_id); + return [200]; } protected function createUser($data) { event(new Registered($user = User::create([ - 'name' => Purify::clean($data['name']), + 'name' => Purify::clean($data['name']), 'username' => $data['username'], - 'email' => $data['email'], + 'email' => $data['email'], 'password' => Hash::make($data['password']), 'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null, 'app_register_ip' => request()->ip(), - 'register_source' => 'mastodon' + 'register_source' => 'mastodon', ]))); $this->guarder()->login($user); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index cbf21518b..8e4296e5b 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -2,368 +2,376 @@ namespace App\Http\Controllers; -use Auth; use App\Hashtag; use App\Place; use App\Profile; +use App\Services\WebfingerService; use App\Status; -use Illuminate\Http\Request; use App\Util\ActivityPub\Helpers; +use App\Util\Lexer\Autolink; +use Auth; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; -use App\Transformer\Api\{ - AccountTransformer, - HashtagTransformer, - StatusTransformer, -}; -use App\Services\WebfingerService; class SearchController extends Controller { - public $tokens = []; - public $term = ''; - public $hash = ''; - public $cacheKey = 'api:search:tag:'; + public $tokens = []; - public function __construct() - { - $this->middleware('auth'); - } + public $term = ''; - public function searchAPI(Request $request) - { - $this->validate($request, [ - 'q' => 'required|string|min:3|max:120', - 'src' => 'required|string|in:metro', - 'v' => 'required|integer|in:2', - 'scope' => 'required|in:all,hashtag,profile,remote,webfinger' - ]); + public $hash = ''; - $scope = $request->input('scope') ?? 'all'; - $this->term = e(urldecode($request->input('q'))); - $this->hash = hash('sha256', $this->term); + public $cacheKey = 'api:search:tag:'; - switch ($scope) { - case 'all': - $this->getHashtags(); - $this->getPosts(); - $this->getProfiles(); - // $this->getPlaces(); - break; + public function __construct() + { + $this->middleware('auth'); + } - case 'hashtag': - $this->getHashtags(); - break; + public function searchAPI(Request $request) + { + $this->validate($request, [ + 'q' => 'required|string|min:3|max:120', + 'src' => 'required|string|in:metro', + 'v' => 'required|integer|in:2', + 'scope' => 'required|in:all,hashtag,profile,remote,webfinger', + ]); - case 'profile': - $this->getProfiles(); - break; + $scope = $request->input('scope') ?? 'all'; + $this->term = e(urldecode($request->input('q'))); + $this->hash = hash('sha256', $this->term); - case 'webfinger': - $this->webfingerSearch(); - break; + switch ($scope) { + case 'all': + $this->getHashtags(); + $this->getPosts(); + $this->getProfiles(); + // $this->getPlaces(); + break; - case 'remote': - $this->remoteLookupSearch(); - break; + case 'hashtag': + $this->getHashtags(); + break; - case 'place': - $this->getPlaces(); - break; + case 'profile': + $this->getProfiles(); + break; - default: - break; - } + case 'webfinger': + $this->webfingerSearch(); + break; - return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT); - } + case 'remote': + $this->remoteLookupSearch(); + break; - protected function getPosts() - { - $tag = $this->term; - $hash = hash('sha256', $tag); - if( Helpers::validateUrl($tag) != false && - Helpers::validateLocalUrl($tag) != true && - config_cache('federation.activitypub.enabled') == true && - config('federation.activitypub.remoteFollow') == true - ) { - $remote = Helpers::fetchFromUrl($tag); - if( isset($remote['type']) && - $remote['type'] == 'Note') { - $item = Helpers::statusFetch($tag); - $this->tokens['posts'] = [[ - 'count' => 0, - 'url' => $item->url(), - 'type' => 'status', - 'value' => "by {$item->profile->username} {$item->created_at->diffForHumans(null, true, true)}", - 'tokens' => [$item->caption], - 'name' => $item->caption, - 'thumb' => $item->thumb(), - ]]; - } - } else { - $posts = Status::select('id', 'profile_id', 'caption', 'created_at') - ->whereHas('media') - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->whereProfileId(Auth::user()->profile_id) - ->where('caption', 'like', '%'.$tag.'%') - ->latest() - ->limit(10) - ->get(); + case 'place': + $this->getPlaces(); + break; - if($posts->count() > 0) { - $posts = $posts->map(function($item, $key) { - return [ - 'count' => 0, - 'url' => $item->url(), - 'type' => 'status', - 'value' => "by {$item->profile->username} {$item->created_at->diffForHumans(null, true, true)}", - 'tokens' => [$item->caption], - 'name' => $item->caption, - 'thumb' => $item->thumb(), - 'filter' => $item->firstMedia()->filter_class - ]; - }); - $this->tokens['posts'] = $posts; - } - } - } + default: + break; + } - protected function getHashtags() - { - $tag = $this->term; - $key = $this->cacheKey . 'hashtags:' . $this->hash; - $ttl = now()->addMinutes(1); - $tokens = Cache::remember($key, $ttl, function() use($tag) { - $htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag; - $hashtags = Hashtag::select('id', 'name', 'slug') - ->where('slug', 'like', '%'.$htag.'%') - ->whereHas('posts') - ->limit(20) - ->get(); - if($hashtags->count() > 0) { - $tags = $hashtags->map(function ($item, $key) { - return [ - 'count' => $item->posts()->count(), - 'url' => $item->url(), - 'type' => 'hashtag', - 'value' => $item->name, - 'tokens' => '', - 'name' => null, - ]; - }); - return $tags; - } - }); - $this->tokens['hashtags'] = $tokens; - } + return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT); + } - protected function getPlaces() - { - $tag = $this->term; - // $key = $this->cacheKey . 'places:' . $this->hash; - // $ttl = now()->addHours(12); - // $tokens = Cache::remember($key, $ttl, function() use($tag) { - $htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag]; - $hashtags = Place::select('id', 'name', 'slug', 'country') - ->where('name', 'like', '%'.$htag[0].'%') - ->paginate(20); - $tags = []; - if($hashtags->count() > 0) { - $tags = $hashtags->map(function ($item, $key) { - return [ - 'count' => null, - 'url' => $item->url(), - 'type' => 'place', - 'value' => $item->name . ', ' . $item->country, - 'tokens' => '', - 'name' => null, - 'city' => $item->name, - 'country' => $item->country - ]; - }); - // return $tags; - } - // }); - $this->tokens['places'] = $tags; - $this->tokens['placesPagination'] = [ - 'total' => $hashtags->total(), - 'current_page' => $hashtags->currentPage(), - 'last_page' => $hashtags->lastPage() - ]; - } + protected function getPosts() + { + $tag = $this->term; + $hash = hash('sha256', $tag); + if (Helpers::validateUrl($tag) != false && + Helpers::validateLocalUrl($tag) != true && + (bool) config_cache('federation.activitypub.enabled') == true && + config('federation.activitypub.remoteFollow') == true + ) { + $remote = Helpers::fetchFromUrl($tag); + if (isset($remote['type']) && + in_array($remote['type'], ['Note', 'Question']) + ) { + $item = Helpers::statusFetch($tag); + $this->tokens['posts'] = [[ + 'count' => 0, + 'url' => $item->url(), + 'type' => 'status', + 'value' => "by {$item->profile->username} {$item->created_at->diffForHumans(null, true, true)}", + 'tokens' => [$item->caption], + 'name' => $item->caption, + 'thumb' => $item->thumb(), + ]]; + } + } else { + $posts = Status::select('id', 'profile_id', 'caption', 'created_at') + ->whereHas('media') + ->whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->whereProfileId(Auth::user()->profile_id) + ->where('caption', 'like', '%'.$tag.'%') + ->latest() + ->limit(10) + ->get(); - protected function getProfiles() - { - $tag = $this->term; - $remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash; - $key = $this->cacheKey . 'profiles:' . $this->hash; - $remoteTtl = now()->addMinutes(15); - $ttl = now()->addHours(2); - if( Helpers::validateUrl($tag) != false && - Helpers::validateLocalUrl($tag) != true && - config_cache('federation.activitypub.enabled') == true && - config('federation.activitypub.remoteFollow') == true - ) { - $remote = Helpers::fetchFromUrl($tag); - if( isset($remote['type']) && - $remote['type'] == 'Person' - ) { - $this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) { - $item = Helpers::profileFirstOrNew($tag); - $tokens = [[ - 'count' => 1, - 'url' => $item->url(), - 'type' => 'profile', - 'value' => $item->username, - 'tokens' => [$item->username], - 'name' => $item->name, - 'entity' => [ - 'id' => (string) $item->id, - 'following' => $item->followedBy(Auth::user()->profile), - 'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id), - 'thumb' => $item->avatarUrl(), - 'local' => (bool) !$item->domain, - 'post_count' => $item->statuses()->count() - ] - ]]; - return $tokens; - }); - } - } + if ($posts->count() > 0) { + $posts = $posts->map(function ($item, $key) { + return [ + 'count' => 0, + 'url' => $item->url(), + 'type' => 'status', + 'value' => "by {$item->profile->username} {$item->created_at->diffForHumans(null, true, true)}", + 'tokens' => [$item->caption], + 'name' => $item->caption, + 'thumb' => $item->thumb(), + 'filter' => $item->firstMedia()->filter_class, + ]; + }); + $this->tokens['posts'] = $posts; + } + } + } - else { - $this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) { - if(Str::startsWith($tag, '@')) { - $tag = substr($tag, 1); - } - $users = Profile::select('status', 'domain', 'username', 'name', 'id') - ->whereNull('status') - ->where('username', 'like', '%'.$tag.'%') - ->limit(20) - ->orderBy('domain') - ->get(); + protected function getHashtags() + { + $tag = $this->term; + $key = $this->cacheKey.'hashtags:'.$this->hash; + $ttl = now()->addMinutes(1); + $tokens = Cache::remember($key, $ttl, function () use ($tag) { + $htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag; + $hashtags = Hashtag::select('id', 'name', 'slug') + ->where('slug', 'like', '%'.$htag.'%') + ->whereHas('posts') + ->limit(20) + ->get(); + if ($hashtags->count() > 0) { + $tags = $hashtags->map(function ($item, $key) { + return [ + 'count' => $item->posts()->count(), + 'url' => $item->url(), + 'type' => 'hashtag', + 'value' => $item->name, + 'tokens' => '', + 'name' => null, + ]; + }); - if($users->count() > 0) { - return $users->map(function ($item, $key) { - return [ - 'count' => 0, - 'url' => $item->url(), - 'type' => 'profile', - 'value' => $item->username, - 'tokens' => [$item->username], - 'name' => $item->name, - 'avatar' => $item->avatarUrl(), - 'id' => (string) $item->id, - 'entity' => [ - 'id' => (string) $item->id, - 'following' => $item->followedBy(Auth::user()->profile), - 'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id), - 'thumb' => $item->avatarUrl(), - 'local' => (bool) !$item->domain, - 'post_count' => $item->statuses()->count() - ] - ]; - }); - } - }); - } - } + return $tags; + } + }); + $this->tokens['hashtags'] = $tokens; + } - public function results(Request $request) - { - $this->validate($request, [ - 'q' => 'required|string|min:1', - ]); + protected function getPlaces() + { + $tag = $this->term; + // $key = $this->cacheKey . 'places:' . $this->hash; + // $ttl = now()->addHours(12); + // $tokens = Cache::remember($key, $ttl, function() use($tag) { + $htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag]; + $hashtags = Place::select('id', 'name', 'slug', 'country') + ->where('name', 'like', '%'.$htag[0].'%') + ->paginate(20); + $tags = []; + if ($hashtags->count() > 0) { + $tags = $hashtags->map(function ($item, $key) { + return [ + 'count' => null, + 'url' => $item->url(), + 'type' => 'place', + 'value' => $item->name.', '.$item->country, + 'tokens' => '', + 'name' => null, + 'city' => $item->name, + 'country' => $item->country, + ]; + }); + // return $tags; + } + // }); + $this->tokens['places'] = $tags; + $this->tokens['placesPagination'] = [ + 'total' => $hashtags->total(), + 'current_page' => $hashtags->currentPage(), + 'last_page' => $hashtags->lastPage(), + ]; + } - return view('search.results'); - } + protected function getProfiles() + { + $tag = $this->term; + $remoteKey = $this->cacheKey.'profiles:remote:'.$this->hash; + $key = $this->cacheKey.'profiles:'.$this->hash; + $remoteTtl = now()->addMinutes(15); + $ttl = now()->addHours(2); + if (Helpers::validateUrl($tag) != false && + Helpers::validateLocalUrl($tag) != true && + (bool) config_cache('federation.activitypub.enabled') == true && + config('federation.activitypub.remoteFollow') == true + ) { + $remote = Helpers::fetchFromUrl($tag); + if (isset($remote['type']) && + $remote['type'] == 'Person' + ) { + $this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function () use ($tag) { + $item = Helpers::profileFirstOrNew($tag); + $tokens = [[ + 'count' => 1, + 'url' => $item->url(), + 'type' => 'profile', + 'value' => $item->username, + 'tokens' => [$item->username], + 'name' => $item->name, + 'entity' => [ + 'id' => (string) $item->id, + 'following' => $item->followedBy(Auth::user()->profile), + 'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id), + 'thumb' => $item->avatarUrl(), + 'local' => (bool) ! $item->domain, + 'post_count' => $item->statuses()->count(), + ], + ]]; - protected function webfingerSearch() - { - $wfs = WebfingerService::lookup($this->term); + return $tokens; + }); + } + } else { + $this->tokens['profiles'] = Cache::remember($key, $ttl, function () use ($tag) { + if (Str::startsWith($tag, '@')) { + $tag = substr($tag, 1); + } + $users = Profile::select('status', 'domain', 'username', 'name', 'id') + ->whereNull('status') + ->where('username', 'like', '%'.$tag.'%') + ->limit(20) + ->orderBy('domain') + ->get(); - if(empty($wfs)) { - return; - } + if ($users->count() > 0) { + return $users->map(function ($item, $key) { + return [ + 'count' => 0, + 'url' => $item->url(), + 'type' => 'profile', + 'value' => $item->username, + 'tokens' => [$item->username], + 'name' => $item->name, + 'avatar' => $item->avatarUrl(), + 'id' => (string) $item->id, + 'entity' => [ + 'id' => (string) $item->id, + 'following' => $item->followedBy(Auth::user()->profile), + 'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id), + 'thumb' => $item->avatarUrl(), + 'local' => (bool) ! $item->domain, + 'post_count' => $item->statuses()->count(), + ], + ]; + }); + } + }); + } + } - $this->tokens['profiles'] = [ - [ - 'count' => 1, - 'url' => $wfs['url'], - 'type' => 'profile', - 'value' => $wfs['username'], - 'tokens' => [$wfs['username']], - 'name' => $wfs['display_name'], - 'entity' => [ - 'id' => (string) $wfs['id'], - 'following' => null, - 'follow_request' => null, - 'thumb' => $wfs['avatar'], - 'local' => (bool) $wfs['local'] - ] - ] - ]; - return; - } + public function results(Request $request) + { + $this->validate($request, [ + 'q' => 'required|string|min:1', + ]); - protected function remotePostLookup() - { - $tag = $this->term; - $hash = hash('sha256', $tag); - $local = Helpers::validateLocalUrl($tag); - $valid = Helpers::validateUrl($tag); + return view('search.results'); + } - if($valid == false || $local == true) { - return; - } + protected function webfingerSearch() + { + $wfs = WebfingerService::lookup($this->term); - if(Status::whereUri($tag)->whereLocal(false)->exists()) { - $item = Status::whereUri($tag)->first(); - $media = $item->firstMedia(); - $url = null; - if($media) { - $url = $media->remote_url; - } - $this->tokens['posts'] = [[ - 'count' => 0, - 'url' => "/i/web/post/_/$item->profile_id/$item->id", - 'type' => 'status', - 'username' => $item->profile->username, - 'caption' => $item->rendered ?? $item->caption, - 'thumb' => $url, - 'timestamp' => $item->created_at->diffForHumans() - ]]; - } + if (empty($wfs)) { + return; + } - $remote = Helpers::fetchFromUrl($tag); + $this->tokens['profiles'] = [ + [ + 'count' => 1, + 'url' => $wfs['url'], + 'type' => 'profile', + 'value' => $wfs['username'], + 'tokens' => [$wfs['username']], + 'name' => $wfs['display_name'], + 'entity' => [ + 'id' => (string) $wfs['id'], + 'following' => null, + 'follow_request' => null, + 'thumb' => $wfs['avatar'], + 'local' => (bool) $wfs['local'], + ], + ], + ]; - if(isset($remote['type']) && $remote['type'] == 'Note') { - $item = Helpers::statusFetch($tag); - $media = $item->firstMedia(); - $url = null; - if($media) { - $url = $media->remote_url; - } - $this->tokens['posts'] = [[ - 'count' => 0, - 'url' => "/i/web/post/_/$item->profile_id/$item->id", - 'type' => 'status', - 'username' => $item->profile->username, - 'caption' => $item->rendered ?? $item->caption, - 'thumb' => $url, - 'timestamp' => $item->created_at->diffForHumans() - ]]; - } - } + } - protected function remoteLookupSearch() - { - if(!Helpers::validateUrl($this->term)) { - return; - } - $this->getProfiles(); - $this->remotePostLookup(); - } + protected function remotePostLookup() + { + $tag = $this->term; + $hash = hash('sha256', $tag); + $local = Helpers::validateLocalUrl($tag); + $valid = Helpers::validateUrl($tag); + + if ($valid == false || $local == true) { + return; + } + + if (Status::whereUri($tag)->whereLocal(false)->exists()) { + $item = Status::whereUri($tag)->first(); + if (! $item) { + return; + } + $media = $item->firstMedia(); + $url = null; + if ($media) { + $url = $media->remote_url; + } + $content = $item->caption ? Autolink::create()->autolink($item->caption) : null; + $this->tokens['posts'] = [[ + 'count' => 0, + 'url' => "/i/web/post/_/$item->profile_id/$item->id", + 'type' => 'status', + 'username' => $item->profile->username, + 'caption' => $content, + 'thumb' => $url, + 'timestamp' => $item->created_at->diffForHumans(), + ]]; + } + + $remote = Helpers::fetchFromUrl($tag); + + if (isset($remote['type']) && $remote['type'] == 'Note') { + $item = Helpers::statusFetch($tag); + if (! $item) { + return; + } + $media = $item->firstMedia(); + $url = null; + if ($media) { + $url = $media->remote_url; + } + $content = $item->caption ? Autolink::create()->autolink($item->caption) : null; + $this->tokens['posts'] = [[ + 'count' => 0, + 'url' => "/i/web/post/_/$item->profile_id/$item->id", + 'type' => 'status', + 'username' => $item->profile->username, + 'caption' => $content, + 'thumb' => $url, + 'timestamp' => $item->created_at->diffForHumans(), + ]]; + } + } + + protected function remoteLookupSearch() + { + if (! Helpers::validateUrl($this->term)) { + return; + } + $this->getProfiles(); + $this->remotePostLookup(); + } } diff --git a/app/Http/Controllers/Settings/HomeSettings.php b/app/Http/Controllers/Settings/HomeSettings.php index 082a72af0..ce411e4fd 100644 --- a/app/Http/Controllers/Settings/HomeSettings.php +++ b/app/Http/Controllers/Settings/HomeSettings.php @@ -4,207 +4,206 @@ namespace App\Http\Controllers\Settings; use App\AccountLog; use App\EmailVerification; +use App\Mail\PasswordChange; use App\Media; -use App\Profile; -use App\User; -use App\UserFilter; +use App\Services\AccountService; +use App\Services\PronounService; use App\Util\Lexer\Autolink; use App\Util\Lexer\PrettyNumber; use Auth; use Cache; -use DB; +use Illuminate\Http\Request; use Mail; use Purify; -use App\Mail\PasswordChange; -use Illuminate\Http\Request; -use App\Services\AccountService; -use App\Services\PronounService; trait HomeSettings { - public function home() - { - $id = Auth::user()->profile->id; - $storage = []; - $used = Media::whereProfileId($id)->sum('size'); - $storage['limit'] = config_cache('pixelfed.max_account_size') * 1024; - $storage['used'] = $used; - $storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100); - $storage['limitPretty'] = PrettyNumber::size($storage['limit']); - $storage['usedPretty'] = PrettyNumber::size($storage['used']); - $pronouns = PronounService::get($id); + public function home() + { + $id = Auth::user()->profile->id; + $storage = []; + $used = Media::whereProfileId($id)->sum('size'); + $storage['limit'] = config_cache('pixelfed.max_account_size') * 1024; + $storage['used'] = $used; + $storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100); + $storage['limitPretty'] = PrettyNumber::size($storage['limit']); + $storage['usedPretty'] = PrettyNumber::size($storage['used']); + $pronouns = PronounService::get($id); - return view('settings.home', compact('storage', 'pronouns')); - } + return view('settings.home', compact('storage', 'pronouns')); + } - public function homeUpdate(Request $request) - { - $this->validate($request, [ - 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'), - 'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'), - 'website' => 'nullable|url', - 'language' => 'nullable|string|min:2|max:5', - 'pronouns' => 'nullable|array|max:4' - ]); + public function homeUpdate(Request $request) + { + $this->validate($request, [ + 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'), + 'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'), + 'website' => 'nullable|url', + 'language' => 'nullable|string|min:2|max:5', + 'pronouns' => 'nullable|array|max:4', + ]); - $changes = false; - $name = strip_tags(Purify::clean($request->input('name'))); - $bio = $request->filled('bio') ? strip_tags(Purify::clean($request->input('bio'))) : null; - $website = $request->input('website'); - $language = $request->input('language'); - $user = Auth::user(); - $profile = $user->profile; - $pronouns = $request->input('pronouns'); - $existingPronouns = PronounService::get($profile->id); - $layout = $request->input('profile_layout'); - if($layout) { - $layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout; - } + $changes = false; + $name = strip_tags(Purify::clean($request->input('name'))); + $bio = $request->filled('bio') ? strip_tags(Purify::clean($request->input('bio'))) : null; + $website = $request->input('website'); + $language = $request->input('language'); + $user = Auth::user(); + $profile = $user->profile; + $pronouns = $request->input('pronouns'); + $existingPronouns = PronounService::get($profile->id); + $layout = $request->input('profile_layout'); + if ($layout) { + $layout = ! in_array($layout, ['metro', 'moment']) ? 'metro' : $layout; + } - $enforceEmailVerification = config_cache('pixelfed.enforce_email_verification'); + $enforceEmailVerification = config_cache('pixelfed.enforce_email_verification'); - // Only allow email to be updated if not yet verified - if (!$enforceEmailVerification || !$changes && $user->email_verified_at) { - if ($profile->name != $name) { - $changes = true; - $user->name = $name; - $profile->name = $name; - } + // Only allow email to be updated if not yet verified + if (! $enforceEmailVerification || ! $changes && $user->email_verified_at) { + if ($profile->name != $name) { + $changes = true; + $user->name = $name; + $profile->name = $name; + } - if ($profile->website != $website) { - $changes = true; - $profile->website = $website; - } + if ($profile->website != $website) { + $changes = true; + $profile->website = $website; + } - if (strip_tags($profile->bio) != $bio) { - $changes = true; - $profile->bio = Autolink::create()->autolink($bio); - } + if (strip_tags($profile->bio) != $bio) { + $changes = true; + $profile->bio = Autolink::create()->autolink($bio); + } - if($user->language != $language && - in_array($language, \App\Util\Localization\Localization::languages()) - ) { - $changes = true; - $user->language = $language; - session()->put('locale', $language); - } + if ($user->language != $language && + in_array($language, \App\Util\Localization\Localization::languages()) + ) { + $changes = true; + $user->language = $language; + session()->put('locale', $language); + } - if($existingPronouns != $pronouns) { - if($pronouns && in_array('Select Pronoun(s)', $pronouns)) { - PronounService::clear($profile->id); - } else { - PronounService::put($profile->id, $pronouns); - } - } - } + if ($existingPronouns != $pronouns) { + if ($pronouns && in_array('Select Pronoun(s)', $pronouns)) { + PronounService::clear($profile->id); + } else { + PronounService::put($profile->id, $pronouns); + } + } + } - if ($changes === true) { - $user->save(); - $profile->save(); - Cache::forget('user:account:id:'.$user->id); - AccountService::del($profile->id); - return redirect('/settings/home')->with('status', 'Profile successfully updated!'); - } + if ($changes === true) { + $user->save(); + $profile->save(); + Cache::forget('user:account:id:'.$user->id); + AccountService::forgetAccountSettings($profile->id); + AccountService::del($profile->id); - return redirect('/settings/home'); - } + return redirect('/settings/home')->with('status', 'Profile successfully updated!'); + } - public function password() - { - return view('settings.password'); - } + return redirect('/settings/home'); + } - public function passwordUpdate(Request $request) - { - $this->validate($request, [ - 'current' => 'required|string', - 'password' => 'required|string', - 'password_confirmation' => 'required|string', - ]); + public function password() + { + return view('settings.password'); + } - $current = $request->input('current'); - $new = $request->input('password'); - $confirm = $request->input('password_confirmation'); + public function passwordUpdate(Request $request) + { + $this->validate($request, [ + 'current' => 'required|string', + 'password' => 'required|string', + 'password_confirmation' => 'required|string', + ]); - $user = Auth::user(); + $current = $request->input('current'); + $new = $request->input('password'); + $confirm = $request->input('password_confirmation'); - if (password_verify($current, $user->password) && $new === $confirm) { - $user->password = bcrypt($new); - $user->save(); + $user = Auth::user(); - $log = new AccountLog(); - $log->user_id = $user->id; - $log->item_id = $user->id; - $log->item_type = 'App\User'; - $log->action = 'account.edit.password'; - $log->message = 'Password changed'; - $log->link = null; - $log->ip_address = $request->ip(); - $log->user_agent = $request->userAgent(); - $log->save(); + if (password_verify($current, $user->password) && $new === $confirm) { + $user->password = bcrypt($new); + $user->save(); - Mail::to($request->user())->send(new PasswordChange($user)); - return redirect('/settings/home')->with('status', 'Password successfully updated!'); - } else { - return redirect()->back()->with('error', 'There was an error with your request! Please try again.'); - } + $log = new AccountLog(); + $log->user_id = $user->id; + $log->item_id = $user->id; + $log->item_type = 'App\User'; + $log->action = 'account.edit.password'; + $log->message = 'Password changed'; + $log->link = null; + $log->ip_address = $request->ip(); + $log->user_agent = $request->userAgent(); + $log->save(); - } + Mail::to($request->user())->send(new PasswordChange($user)); - public function email() - { - return view('settings.email'); - } + return redirect('/settings/home')->with('status', 'Password successfully updated!'); + } else { + return redirect()->back()->with('error', 'There was an error with your request! Please try again.'); + } - public function emailUpdate(Request $request) - { - $this->validate($request, [ - 'email' => 'required|email|unique:users,email', - ]); - $changes = false; - $email = $request->input('email'); - $user = Auth::user(); - $profile = $user->profile; + } - $validate = config_cache('pixelfed.enforce_email_verification'); + public function email() + { + return view('settings.email'); + } - if ($user->email != $email) { - $changes = true; - $user->email = $email; + public function emailUpdate(Request $request) + { + $this->validate($request, [ + 'email' => 'required|email|unique:users,email', + ]); + $changes = false; + $email = $request->input('email'); + $user = Auth::user(); + $profile = $user->profile; - if ($validate) { - $user->email_verified_at = null; - // Prevent old verifications from working - EmailVerification::whereUserId($user->id)->delete(); - } + $validate = config_cache('pixelfed.enforce_email_verification'); - $log = new AccountLog(); - $log->user_id = $user->id; - $log->item_id = $user->id; - $log->item_type = 'App\User'; - $log->action = 'account.edit.email'; - $log->message = 'Email changed'; - $log->link = null; - $log->ip_address = $request->ip(); - $log->user_agent = $request->userAgent(); - $log->save(); - } + if ($user->email != $email) { + $changes = true; + $user->email = $email; - if ($changes === true) { - Cache::forget('user:account:id:'.$user->id); - $user->save(); - $profile->save(); + if ($validate) { + // auto verify admin email addresses + $user->email_verified_at = $user->is_admin == true ? now() : null; + // Prevent old verifications from working + EmailVerification::whereUserId($user->id)->delete(); + } - return redirect('/settings/home')->with('status', 'Email successfully updated!'); - } else { - return redirect('/settings/email'); - } + $log = new AccountLog(); + $log->user_id = $user->id; + $log->item_id = $user->id; + $log->item_type = 'App\User'; + $log->action = 'account.edit.email'; + $log->message = 'Email changed'; + $log->link = null; + $log->ip_address = $request->ip(); + $log->user_agent = $request->userAgent(); + $log->save(); + } - } + if ($changes === true) { + Cache::forget('user:account:id:'.$user->id); + $user->save(); + $profile->save(); - public function avatar() - { - return view('settings.avatar'); - } + return redirect('/settings/email')->with('status', 'Email successfully updated!'); + } else { + return redirect('/settings/email'); + } + } + + public function avatar() + { + return view('settings.avatar'); + } } diff --git a/app/Http/Controllers/Settings/PrivacySettings.php b/app/Http/Controllers/Settings/PrivacySettings.php index 9a5febe83..c9caa168d 100644 --- a/app/Http/Controllers/Settings/PrivacySettings.php +++ b/app/Http/Controllers/Settings/PrivacySettings.php @@ -2,29 +2,31 @@ namespace App\Http\Controllers\Settings; -use App\AccountLog; -use App\EmailVerification; -use App\Instance; use App\Follower; -use App\Media; use App\Profile; -use App\User; +use App\Services\AccountService; +use App\Services\RelationshipService; use App\UserFilter; -use App\Util\Lexer\PrettyNumber; -use App\Util\ActivityPub\Helpers; -use Auth, Cache, DB; +use Auth; +use Cache; +use DB; use Illuminate\Http\Request; trait PrivacySettings { - public function privacy() { $user = Auth::user(); $settings = $user->settings; $profile = $user->profile; $is_private = $profile->is_private; + $cachedSettings = AccountService::getAccountSettings($profile->id); $settings['is_private'] = (bool) $is_private; + if ($cachedSettings && isset($cachedSettings['disable_embeds'])) { + $settings['disable_embeds'] = (bool) $cachedSettings['disable_embeds']; + } else { + $settings['disable_embeds'] = false; + } return view('settings.privacy', compact('settings', 'profile')); } @@ -33,20 +35,31 @@ trait PrivacySettings { $settings = $request->user()->settings; $profile = $request->user()->profile; + $other = $settings->other; $fields = [ - 'is_private', - 'crawlable', - 'public_dm', - 'show_profile_follower_count', - 'show_profile_following_count', - 'indexable', - 'show_atom', + 'is_private', + 'crawlable', + 'public_dm', + 'show_profile_follower_count', + 'show_profile_following_count', + 'indexable', + 'show_atom', ]; $profile->indexable = $request->input('indexable') == 'on'; $profile->is_suggestable = $request->input('is_suggestable') == 'on'; $profile->save(); + if ($request->has('disable_embeds')) { + $other['disable_embeds'] = true; + $settings->other = $other; + $settings->save(); + } else { + $other['disable_embeds'] = false; + $settings->other = $other; + $settings->save(); + } + foreach ($fields as $field) { $form = $request->input($field); if ($field == 'is_private') { @@ -66,7 +79,7 @@ trait PrivacySettings } else { $settings->{$field} = true; } - } elseif ($field == 'public_dm') { + } elseif ($field == 'public_dm') { if ($form == 'on') { $settings->{$field} = true; } else { @@ -83,29 +96,37 @@ trait PrivacySettings } $settings->save(); } - Cache::forget('profile:settings:' . $profile->id); - Cache::forget('user:account:id:' . $profile->user_id); - Cache::forget('profile:follower_count:' . $profile->id); - Cache::forget('profile:following_count:' . $profile->id); - Cache::forget('profile:atom:enabled:' . $profile->id); - Cache::forget('profile:embed:' . $profile->id); - Cache::forget('pf:acct:settings:hidden-followers:' . $profile->id); - Cache::forget('pf:acct:settings:hidden-following:' . $profile->id); + $pid = $profile->id; + Cache::forget('profile:settings:'.$pid); + Cache::forget('user:account:id:'.$profile->user_id); + Cache::forget('profile:follower_count:'.$pid); + Cache::forget('profile:following_count:'.$pid); + Cache::forget('profile:atom:enabled:'.$pid); + Cache::forget('profile:embed:'.$pid); + Cache::forget('pf:acct:settings:hidden-followers:'.$pid); + Cache::forget('pf:acct:settings:hidden-following:'.$pid); + Cache::forget('pf:acct-trans:hideFollowing:'.$pid); + Cache::forget('pf:acct-trans:hideFollowers:'.$pid); + Cache::forget('pfc:cached-user:wt:'.strtolower($profile->username)); + Cache::forget('pfc:cached-user:wot:'.strtolower($profile->username)); + AccountService::forgetAccountSettings($profile->id); + return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!'); } public function mutedUsers() - { + { $pid = Auth::user()->profile->id; $ids = (new UserFilter())->mutedUserIds($pid); $users = Profile::whereIn('id', $ids)->simplePaginate(15); + return view('settings.privacy.muted', compact('users')); } public function mutedUsersUpdate(Request $request) - { + { $this->validate($request, [ - 'profile_id' => 'required|integer|min:1' + 'profile_id' => 'required|integer|min:1', ]); $fid = $request->input('profile_id'); $pid = Auth::user()->profile->id; @@ -117,6 +138,8 @@ trait PrivacySettings ->firstOrFail(); $filter->delete(); }); + RelationshipService::refresh($pid, $fid); + return redirect()->back(); } @@ -125,14 +148,14 @@ trait PrivacySettings $pid = Auth::user()->profile->id; $ids = (new UserFilter())->blockedUserIds($pid); $users = Profile::whereIn('id', $ids)->simplePaginate(15); + return view('settings.privacy.blocked', compact('users')); } - public function blockedUsersUpdate(Request $request) - { + { $this->validate($request, [ - 'profile_id' => 'required|integer|min:1' + 'profile_id' => 'required|integer|min:1', ]); $fid = $request->input('profile_id'); $pid = Auth::user()->profile->id; @@ -144,52 +167,32 @@ trait PrivacySettings ->firstOrFail(); $filter->delete(); }); + RelationshipService::refresh($pid, $fid); + return redirect()->back(); } public function blockedInstances() { - $pid = Auth::user()->profile->id; - $filters = UserFilter::whereUserId($pid) - ->whereFilterableType('App\Instance') - ->whereFilterType('block') - ->orderByDesc('id') - ->paginate(10); - return view('settings.privacy.blocked-instances', compact('filters')); + // deprecated + abort(404); + } + + public function domainBlocks() + { + return view('settings.privacy.domain-blocks'); } public function blockedInstanceStore(Request $request) { - $this->validate($request, [ - 'domain' => 'required|url|min:1|max:120' - ]); - $domain = $request->input('domain'); - if(Helpers::validateUrl($domain) == false) { - return abort(400, 'Invalid domain'); - } - $domain = parse_url($domain, PHP_URL_HOST); - $instance = Instance::firstOrCreate(['domain' => $domain]); - $filter = new UserFilter; - $filter->user_id = Auth::user()->profile->id; - $filter->filterable_id = $instance->id; - $filter->filterable_type = 'App\Instance'; - $filter->filter_type = 'block'; - $filter->save(); - return response()->json(['msg' => 200]); + // deprecated + abort(404); } public function blockedInstanceUnblock(Request $request) { - $this->validate($request, [ - 'id' => 'required|integer|min:1' - ]); - $pid = Auth::user()->profile->id; - - $filter = UserFilter::whereFilterableType('App\Instance') - ->whereUserId($pid) - ->findOrFail($request->input('id')); - $filter->delete(); - return redirect(route('settings.privacy.blocked-instances')); + // deprecated + abort(404); } public function blockedKeywords() @@ -210,7 +213,7 @@ trait PrivacySettings $profile = Auth::user()->profile; $settings = Auth::user()->settings; - if($mode !== 'keep-all') { + if ($mode !== 'keep-all') { switch ($mode) { case 'mutual-only': $following = $profile->following()->pluck('profiles.id'); @@ -225,9 +228,9 @@ trait PrivacySettings case 'remove-all': Follower::whereFollowingId($profile->id)->delete(); break; - + default: - # code... + // code... break; } } @@ -237,6 +240,7 @@ trait PrivacySettings $settings->save(); $profile->save(); Cache::forget('profiles:private'); + return [200]; } } diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 2eb9df65f..981c47784 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -2,270 +2,275 @@ namespace App\Http\Controllers; -use App\AccountLog; -use App\Following; -use App\ProfileSponsor; -use App\Report; -use App\UserFilter; -use App\UserSetting; -use Auth, Cookie, DB, Cache, Purify; -use Illuminate\Support\Facades\Redis; -use Carbon\Carbon; -use Illuminate\Http\Request; -use Illuminate\Support\Str; -use App\Http\Controllers\Settings\{ - ExportSettings, - LabsSettings, - HomeSettings, - PrivacySettings, - RelationshipSettings, - SecuritySettings -}; +use App\Http\Controllers\Settings\ExportSettings; +use App\Http\Controllers\Settings\HomeSettings; +use App\Http\Controllers\Settings\LabsSettings; +use App\Http\Controllers\Settings\PrivacySettings; +use App\Http\Controllers\Settings\RelationshipSettings; +use App\Http\Controllers\Settings\SecuritySettings; use App\Jobs\DeletePipeline\DeleteAccountPipeline; use App\Jobs\MediaPipeline\MediaSyncLicensePipeline; +use App\ProfileSponsor; use App\Services\AccountService; +use App\UserSetting; +use Auth; +use Cache; +use Carbon\Carbon; +use Cookie; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Redis; +use Illuminate\Support\Str; class SettingsController extends Controller { - use ExportSettings, - LabsSettings, - HomeSettings, - PrivacySettings, - RelationshipSettings, - SecuritySettings; + use ExportSettings, + HomeSettings, + LabsSettings, + PrivacySettings, + RelationshipSettings, + SecuritySettings; - public function __construct() - { - $this->middleware('auth'); - } + public function __construct() + { + $this->middleware('auth'); + } - public function accessibility() - { - $settings = Auth::user()->settings; + public function accessibility() + { + $settings = Auth::user()->settings; - return view('settings.accessibility', compact('settings')); - } + return view('settings.accessibility', compact('settings')); + } - public function accessibilityStore(Request $request) - { - $settings = Auth::user()->settings; - $fields = [ - 'compose_media_descriptions', - 'reduce_motion', - 'optimize_screen_reader', - 'high_contrast_mode', - 'video_autoplay', - ]; - foreach ($fields as $field) { - $form = $request->input($field); - if ($form == 'on') { - $settings->{$field} = true; - } else { - $settings->{$field} = false; - } - $settings->save(); - } + public function accessibilityStore(Request $request) + { + $user = $request->user(); + $settings = $user->settings; + $fields = [ + 'compose_media_descriptions', + 'reduce_motion', + 'optimize_screen_reader', + 'high_contrast_mode', + 'video_autoplay', + ]; + foreach ($fields as $field) { + $form = $request->input($field); + if ($form == 'on') { + $settings->{$field} = true; + } else { + $settings->{$field} = false; + } + $settings->save(); + } + AccountService::forgetAccountSettings($user->profile_id); - return redirect(route('settings.accessibility'))->with('status', 'Settings successfully updated!'); - } + return redirect(route('settings.accessibility'))->with('status', 'Settings successfully updated!'); + } - public function notifications() - { - return view('settings.notifications'); - } + public function notifications() + { + return view('settings.notifications'); + } - public function applications() - { - return view('settings.applications'); - } + public function applications() + { + return view('settings.applications'); + } - public function dataImport() - { - return view('settings.import.home'); - } + public function dataImport() + { + return view('settings.import.home'); + } - public function dataImportInstagram() - { - abort(404); - } + public function dataImportInstagram() + { + abort(404); + } - public function developers() - { - return view('settings.developers'); - } + public function developers() + { + return view('settings.developers'); + } - public function removeAccountTemporary(Request $request) - { - $user = Auth::user(); - abort_if(!config('pixelfed.account_deletion'), 403); - abort_if($user->is_admin, 403); + public function removeAccountTemporary(Request $request) + { + $user = Auth::user(); + abort_if(! config('pixelfed.account_deletion'), 403); + abort_if($user->is_admin, 403); - return view('settings.remove.temporary'); - } + return view('settings.remove.temporary'); + } - public function removeAccountTemporarySubmit(Request $request) - { - $user = Auth::user(); - abort_if(!config('pixelfed.account_deletion'), 403); - abort_if($user->is_admin, 403); - $profile = $user->profile; - $user->status = 'disabled'; - $profile->status = 'disabled'; - $user->save(); - $profile->save(); - Auth::logout(); - Cache::forget('profiles:private'); - return redirect('/'); - } + public function removeAccountTemporarySubmit(Request $request) + { + $user = Auth::user(); + abort_if(! config('pixelfed.account_deletion'), 403); + abort_if($user->is_admin, 403); + $profile = $user->profile; + $user->status = 'disabled'; + $profile->status = 'disabled'; + $user->save(); + $profile->save(); + Auth::logout(); + Cache::forget('profiles:private'); - public function removeAccountPermanent(Request $request) - { - $user = Auth::user(); - abort_if($user->is_admin, 403); - return view('settings.remove.permanent'); - } + return redirect('/'); + } - public function removeAccountPermanentSubmit(Request $request) - { - if(config('pixelfed.account_deletion') == false) { - abort(404); - } - $user = Auth::user(); - abort_if(!config('pixelfed.account_deletion'), 403); - abort_if($user->is_admin, 403); - $profile = $user->profile; - $ts = Carbon::now()->addMonth(); - $user->email = $user->id; - $user->password = ''; - $user->status = 'delete'; - $profile->status = 'delete'; - $user->delete_after = $ts; - $profile->delete_after = $ts; - $user->save(); - $profile->save(); - Cache::forget('profiles:private'); - AccountService::del($profile->id); - Auth::logout(); - DeleteAccountPipeline::dispatch($user)->onQueue('low'); - return redirect('/'); - } + public function removeAccountPermanent(Request $request) + { + $user = Auth::user(); + abort_if($user->is_admin, 403); - public function requestFullExport(Request $request) - { - $user = Auth::user(); - return view('settings.export.show'); - } + return view('settings.remove.permanent'); + } - public function metroDarkMode(Request $request) - { - $this->validate($request, [ - 'mode' => 'required|string|in:light,dark' - ]); + public function removeAccountPermanentSubmit(Request $request) + { + if (config('pixelfed.account_deletion') == false) { + abort(404); + } + $user = Auth::user(); + abort_if(! config('pixelfed.account_deletion'), 403); + abort_if($user->is_admin, 403); + $profile = $user->profile; + $ts = Carbon::now()->addMonth(); + $user->email = $user->id; + $user->password = ''; + $user->status = 'delete'; + $profile->status = 'delete'; + $user->delete_after = $ts; + $profile->delete_after = $ts; + $user->save(); + $profile->save(); + Cache::forget('profiles:private'); + AccountService::del($profile->id); + Auth::logout(); + DeleteAccountPipeline::dispatch($user)->onQueue('low'); - $mode = $request->input('mode'); + return redirect('/'); + } - if($mode == 'dark') { - $cookie = Cookie::make('dark-mode', 'true', 43800); - } else { - $cookie = Cookie::forget('dark-mode'); - } + public function requestFullExport(Request $request) + { + $user = Auth::user(); - return response()->json([200])->cookie($cookie); - } + return view('settings.export.show'); + } - public function sponsor() - { - $default = [ - 'patreon' => null, - 'liberapay' => null, - 'opencollective' => null - ]; - $sponsors = ProfileSponsor::whereProfileId(Auth::user()->profile->id)->first(); - $sponsors = $sponsors ? json_decode($sponsors->sponsors, true) : $default; - return view('settings.sponsor', compact('sponsors')); - } - - public function sponsorStore(Request $request) - { - $this->validate($request, [ - 'patreon' => 'nullable|string', - 'liberapay' => 'nullable|string', - 'opencollective' => 'nullable|string' - ]); - - $patreon = Str::startsWith($request->input('patreon'), 'https://') ? - substr($request->input('patreon'), 8) : - $request->input('patreon'); - - $liberapay = Str::startsWith($request->input('liberapay'), 'https://') ? - substr($request->input('liberapay'), 8) : - $request->input('liberapay'); - - $opencollective = Str::startsWith($request->input('opencollective'), 'https://') ? - substr($request->input('opencollective'), 8) : - $request->input('opencollective'); - - $patreon = Str::startsWith($patreon, 'patreon.com/') ? e($patreon) : null; - $liberapay = Str::startsWith($liberapay, 'liberapay.com/') ? e($liberapay) : null; - $opencollective = Str::startsWith($opencollective, 'opencollective.com/') ? e($opencollective) : null; - - if(empty($patreon) && empty($liberapay) && empty($opencollective)) { - return redirect(route('settings'))->with('error', 'An error occured. Please try again later.'); - } - - $res = [ - 'patreon' => $patreon, - 'liberapay' => $liberapay, - 'opencollective' => $opencollective - ]; - - $sponsors = ProfileSponsor::firstOrCreate([ - 'profile_id' => Auth::user()->profile_id ?? Auth::user()->profile->id - ]); - $sponsors->sponsors = json_encode($res); - $sponsors->save(); - $sponsors = $res; - return redirect(route('settings'))->with('status', 'Sponsor settings successfully updated!'); - } - - public function timelineSettings(Request $request) - { - $uid = $request->user()->id; - $pid = $request->user()->profile_id; - $top = Redis::zscore('pf:tl:top', $pid) != false; - $replies = Redis::zscore('pf:tl:replies', $pid) != false; - $userSettings = UserSetting::firstOrCreate([ - 'user_id' => $uid + public function metroDarkMode(Request $request) + { + $this->validate($request, [ + 'mode' => 'required|string|in:light,dark', ]); - if(!$userSettings || !$userSettings->other) { + + $mode = $request->input('mode'); + + if ($mode == 'dark') { + $cookie = Cookie::make('dark-mode', 'true', 43800); + } else { + $cookie = Cookie::forget('dark-mode'); + } + + return response()->json([200])->cookie($cookie); + } + + public function sponsor() + { + $default = [ + 'patreon' => null, + 'liberapay' => null, + 'opencollective' => null, + ]; + $sponsors = ProfileSponsor::whereProfileId(Auth::user()->profile->id)->first(); + $sponsors = $sponsors ? json_decode($sponsors->sponsors, true) : $default; + + return view('settings.sponsor', compact('sponsors')); + } + + public function sponsorStore(Request $request) + { + $this->validate($request, [ + 'patreon' => 'nullable|string', + 'liberapay' => 'nullable|string', + 'opencollective' => 'nullable|string', + ]); + + $patreon = Str::startsWith($request->input('patreon'), 'https://') ? + substr($request->input('patreon'), 8) : + $request->input('patreon'); + + $liberapay = Str::startsWith($request->input('liberapay'), 'https://') ? + substr($request->input('liberapay'), 8) : + $request->input('liberapay'); + + $opencollective = Str::startsWith($request->input('opencollective'), 'https://') ? + substr($request->input('opencollective'), 8) : + $request->input('opencollective'); + + $patreon = Str::startsWith($patreon, 'patreon.com/') ? e($patreon) : null; + $liberapay = Str::startsWith($liberapay, 'liberapay.com/') ? e($liberapay) : null; + $opencollective = Str::startsWith($opencollective, 'opencollective.com/') ? e($opencollective) : null; + + if (empty($patreon) && empty($liberapay) && empty($opencollective)) { + return redirect(route('settings'))->with('error', 'An error occured. Please try again later.'); + } + + $res = [ + 'patreon' => $patreon, + 'liberapay' => $liberapay, + 'opencollective' => $opencollective, + ]; + + $sponsors = ProfileSponsor::firstOrCreate([ + 'profile_id' => Auth::user()->profile_id ?? Auth::user()->profile->id, + ]); + $sponsors->sponsors = json_encode($res); + $sponsors->save(); + $sponsors = $res; + + return redirect(route('settings'))->with('status', 'Sponsor settings successfully updated!'); + } + + public function timelineSettings(Request $request) + { + $uid = $request->user()->id; + $pid = $request->user()->profile_id; + $top = Redis::zscore('pf:tl:top', $pid) != false; + $replies = Redis::zscore('pf:tl:replies', $pid) != false; + $userSettings = UserSetting::firstOrCreate([ + 'user_id' => $uid, + ]); + if (! $userSettings || ! $userSettings->other) { $userSettings = [ 'enable_reblogs' => false, - 'photo_reblogs_only' => false + 'photo_reblogs_only' => false, ]; } else { $userSettings = array_merge([ 'enable_reblogs' => false, - 'photo_reblogs_only' => false + 'photo_reblogs_only' => false, ], - $userSettings->other); + $userSettings->other); } - return view('settings.timeline', compact('top', 'replies', 'userSettings')); - } - public function updateTimelineSettings(Request $request) - { + return view('settings.timeline', compact('top', 'replies', 'userSettings')); + } + + public function updateTimelineSettings(Request $request) + { $pid = $request->user()->profile_id; - $uid = $request->user()->id; + $uid = $request->user()->id; $this->validate($request, [ 'enable_reblogs' => 'sometimes', - 'photo_reblogs_only' => 'sometimes' + 'photo_reblogs_only' => 'sometimes', ]); - Redis::zrem('pf:tl:top', $pid); - Redis::zrem('pf:tl:replies', $pid); + Redis::zrem('pf:tl:top', $pid); + Redis::zrem('pf:tl:replies', $pid); $userSettings = UserSetting::firstOrCreate([ - 'user_id' => $uid + 'user_id' => $uid, ]); - if($userSettings->other) { + if ($userSettings->other) { $other = $userSettings->other; $other['enable_reblogs'] = $request->has('enable_reblogs'); $other['photo_reblogs_only'] = $request->has('photo_reblogs_only'); @@ -275,72 +280,74 @@ class SettingsController extends Controller } $userSettings->other = $other; $userSettings->save(); - return redirect(route('settings'))->with('status', 'Timeline settings successfully updated!'); - } - public function mediaSettings(Request $request) - { - $setting = UserSetting::whereUserId($request->user()->id)->firstOrFail(); - $compose = $setting->compose_settings ? ( - is_string($setting->compose_settings) ? json_decode($setting->compose_settings, true) : $setting->compose_settings - ) : [ - 'default_license' => null, - 'media_descriptions' => false - ]; - return view('settings.media', compact('compose')); - } + return redirect(route('settings'))->with('status', 'Timeline settings successfully updated!'); + } - public function updateMediaSettings(Request $request) - { - $this->validate($request, [ - 'default' => 'required|int|min:1|max:16', - 'sync' => 'nullable', - 'media_descriptions' => 'nullable' - ]); + public function mediaSettings(Request $request) + { + $setting = UserSetting::whereUserId($request->user()->id)->firstOrFail(); + $compose = $setting->compose_settings ? ( + is_string($setting->compose_settings) ? json_decode($setting->compose_settings, true) : $setting->compose_settings + ) : [ + 'default_license' => null, + 'media_descriptions' => false, + ]; - $license = $request->input('default'); - $sync = $request->input('sync') == 'on'; - $media_descriptions = $request->input('media_descriptions') == 'on'; - $uid = $request->user()->id; + return view('settings.media', compact('compose')); + } - $setting = UserSetting::whereUserId($uid)->firstOrFail(); - $compose = is_string($setting->compose_settings) ? json_decode($setting->compose_settings, true) : $setting->compose_settings; - $changed = false; + public function updateMediaSettings(Request $request) + { + $this->validate($request, [ + 'default' => 'required|int|min:1|max:16', + 'sync' => 'nullable', + 'media_descriptions' => 'nullable', + ]); - if($sync) { - $key = 'pf:settings:mls_recently:'.$uid; - if(Cache::get($key) == 2) { - $msg = 'You can only sync licenses twice per 24 hours. Try again later.'; - return redirect(route('settings')) - ->with('error', $msg); - } - } + $license = $request->input('default'); + $sync = $request->input('sync') == 'on'; + $media_descriptions = $request->input('media_descriptions') == 'on'; + $uid = $request->user()->id; - if(!isset($compose['default_license']) || $compose['default_license'] !== $license) { - $compose['default_license'] = (int) $license; - $changed = true; - } + $setting = UserSetting::whereUserId($uid)->firstOrFail(); + $compose = is_string($setting->compose_settings) ? json_decode($setting->compose_settings, true) : $setting->compose_settings; + $changed = false; - if(!isset($compose['media_descriptions']) || $compose['media_descriptions'] !== $media_descriptions) { - $compose['media_descriptions'] = $media_descriptions; - $changed = true; - } + if ($sync) { + $key = 'pf:settings:mls_recently:'.$uid; + if (Cache::get($key) == 2) { + $msg = 'You can only sync licenses twice per 24 hours. Try again later.'; - if($changed) { - $setting->compose_settings = $compose; - $setting->save(); - Cache::forget('profile:compose:settings:' . $request->user()->id); - } + return redirect(route('settings')) + ->with('error', $msg); + } + } - if($sync) { - $val = Cache::has($key) ? 2 : 1; - Cache::put($key, $val, 86400); - MediaSyncLicensePipeline::dispatch($uid, $license); - return redirect(route('settings'))->with('status', 'Media licenses successfully synced! It may take a few minutes to take effect for every post.'); - } + if (! isset($compose['default_license']) || $compose['default_license'] !== $license) { + $compose['default_license'] = (int) $license; + $changed = true; + } - return redirect(route('settings'))->with('status', 'Media settings successfully updated!'); - } + if (! isset($compose['media_descriptions']) || $compose['media_descriptions'] !== $media_descriptions) { + $compose['media_descriptions'] = $media_descriptions; + $changed = true; + } + if ($changed) { + $setting->compose_settings = $compose; + $setting->save(); + Cache::forget('profile:compose:settings:'.$request->user()->id); + } + + if ($sync) { + $val = Cache::has($key) ? 2 : 1; + Cache::put($key, $val, 86400); + MediaSyncLicensePipeline::dispatch($uid, $license); + + return redirect(route('settings'))->with('status', 'Media licenses successfully synced! It may take a few minutes to take effect for every post.'); + } + + return redirect(route('settings'))->with('status', 'Media settings successfully updated!'); + } } - diff --git a/app/Http/Controllers/SiteController.php b/app/Http/Controllers/SiteController.php index 379b24505..8c13e0b59 100644 --- a/app/Http/Controllers/SiteController.php +++ b/app/Http/Controllers/SiteController.php @@ -2,166 +2,202 @@ namespace App\Http\Controllers; +use App\Page; +use App\Profile; +use App\Services\FollowerService; +use App\Services\StatusService; +use App\User; +use App\Util\ActivityPub\Helpers; +use App\Util\Localization\Localization; +use Auth; +use Cache; use Illuminate\Http\Request; use Illuminate\Support\Str; -use App, Auth, Cache, View; -use App\Util\Lexer\PrettyNumber; -use App\{Follower, Page, Profile, Status, User, UserFilter}; -use App\Util\Localization\Localization; -use App\Services\FollowerService; -use App\Util\ActivityPub\Helpers; +use View; class SiteController extends Controller { - public function home(Request $request) - { - if (Auth::check()) { - return $this->homeTimeline($request); - } else { - return $this->homeGuest(); - } - } + public function home(Request $request) + { + if (Auth::check()) { + return $this->homeTimeline($request); + } else { + return $this->homeGuest(); + } + } - public function homeGuest() - { - return view('site.index'); - } + public function homeGuest() + { + return view('site.index'); + } - public function homeTimeline(Request $request) - { - if($request->has('force_old_ui')) { - return view('timeline.home', ['layout' => 'feed']); - } + public function homeTimeline(Request $request) + { + if ($request->has('force_old_ui')) { + return view('timeline.home', ['layout' => 'feed']); + } - return redirect('/i/web'); - } + return redirect('/i/web'); + } - public function changeLocale(Request $request, $locale) - { - // todo: add other locales after pushing new l10n strings - $locales = Localization::languages(); - if(in_array($locale, $locales)) { - if($request->user()) { - $user = $request->user(); - $user->language = $locale; - $user->save(); - } - session()->put('locale', $locale); - } + public function changeLocale(Request $request, $locale) + { + // todo: add other locales after pushing new l10n strings + $locales = Localization::languages(); + if (in_array($locale, $locales)) { + if ($request->user()) { + $user = $request->user(); + $user->language = $locale; + $user->save(); + } + session()->put('locale', $locale); + } - return redirect(route('site.language')); - } + return redirect(route('site.language')); + } - public function about() - { - return Cache::remember('site.about_v2', now()->addMinutes(15), function() { - $user_count = number_format(User::count()); - $post_count = number_format(Status::count()); - $rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null; - return view('site.about', compact('rules', 'user_count', 'post_count'))->render(); - }); - } + public function about() + { + return Cache::remember('site.about_v2', now()->addMinutes(15), function () { + $user_count = number_format(User::count()); + $post_count = number_format(StatusService::totalLocalStatuses()); + $rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null; - public function language() - { - return view('site.language'); - } + return view('site.about', compact('rules', 'user_count', 'post_count'))->render(); + }); + } - public function communityGuidelines(Request $request) - { - return Cache::remember('site:help:community-guidelines', now()->addDays(120), function() { - $slug = '/site/kb/community-guidelines'; - $page = Page::whereSlug($slug)->whereActive(true)->first(); - return View::make('site.help.community-guidelines')->with(compact('page'))->render(); - }); - } + public function language() + { + return view('site.language'); + } - public function privacy(Request $request) - { - $page = Cache::remember('site:privacy', now()->addDays(120), function() { - $slug = '/site/privacy'; - return Page::whereSlug($slug)->whereActive(true)->first(); - }); - return View::make('site.privacy')->with(compact('page'))->render(); - } + public function communityGuidelines(Request $request) + { + return Cache::remember('site:help:community-guidelines', now()->addDays(120), function () { + $slug = '/site/kb/community-guidelines'; + $page = Page::whereSlug($slug)->whereActive(true)->first(); - public function terms(Request $request) - { - $page = Cache::remember('site:terms', now()->addDays(120), function() { - $slug = '/site/terms'; - return Page::whereSlug($slug)->whereActive(true)->first(); - }); - return View::make('site.terms')->with(compact('page'))->render(); - } + return View::make('site.help.community-guidelines')->with(compact('page'))->render(); + }); + } - public function redirectUrl(Request $request) - { - abort_if(!$request->user(), 404); - $this->validate($request, [ - 'url' => 'required|url' - ]); - $url = request()->input('url'); - abort_if(Helpers::validateUrl($url) == false, 404); - return view('site.redirect', compact('url')); - } + public function privacy(Request $request) + { + $page = Cache::remember('site:privacy', now()->addDays(120), function () { + $slug = '/site/privacy'; - public function followIntent(Request $request) - { - $this->validate($request, [ - 'user' => 'string|min:1|max:15|exists:users,username', - ]); - $profile = Profile::whereUsername($request->input('user'))->firstOrFail(); - $user = $request->user(); - abort_if($user && $profile->id == $user->profile_id, 404); - $following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false; - return view('site.intents.follow', compact('profile', 'user', 'following')); - } + return Page::whereSlug($slug)->whereActive(true)->first(); + }); - public function legacyProfileRedirect(Request $request, $username) - { - $username = Str::contains($username, '@') ? '@' . $username : $username; - if(str_contains($username, '@')) { - $profile = Profile::whereUsername($username) - ->firstOrFail(); + return View::make('site.privacy')->with(compact('page'))->render(); + } - if($profile->domain == null) { - $url = "/$profile->username"; - } else { - $url = "/i/web/profile/_/{$profile->id}"; - } + public function terms(Request $request) + { + $page = Cache::remember('site:terms', now()->addDays(120), function () { + $slug = '/site/terms'; - } else { - $profile = Profile::whereUsername($username) - ->whereNull('domain') - ->firstOrFail(); - $url = "/$profile->username"; - } + return Page::whereSlug($slug)->whereActive(true)->first(); + }); - return redirect($url); - } + return View::make('site.terms')->with(compact('page'))->render(); + } - public function legacyWebfingerRedirect(Request $request, $username, $domain) - { - $un = '@'.$username.'@'.$domain; - $profile = Profile::whereUsername($un) - ->firstOrFail(); + public function redirectUrl(Request $request) + { + abort_if(! $request->user(), 404); + $this->validate($request, [ + 'url' => 'required|url', + ]); + $url = request()->input('url'); + abort_if(Helpers::validateUrl($url) == false, 404); - if($profile->domain == null) { - $url = "/$profile->username"; - } else { - $url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url(); - } + return view('site.redirect', compact('url')); + } - return redirect($url); - } + public function followIntent(Request $request) + { + $this->validate($request, [ + 'user' => 'string|min:1|max:15|exists:users,username', + ]); + $profile = Profile::whereUsername($request->input('user'))->firstOrFail(); + $user = $request->user(); + abort_if($user && $profile->id == $user->profile_id, 404); + $following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false; - public function legalNotice(Request $request) - { - $page = Cache::remember('site:legal-notice', now()->addDays(120), function() { - $slug = '/site/legal-notice'; - return Page::whereSlug($slug)->whereActive(true)->first(); - }); - abort_if(!$page, 404); - return View::make('site.legal-notice')->with(compact('page'))->render(); - } + return view('site.intents.follow', compact('profile', 'user', 'following')); + } + + public function legacyProfileRedirect(Request $request, $username) + { + $username = Str::contains($username, '@') ? '@'.$username : $username; + if (str_contains($username, '@')) { + $profile = Profile::whereUsername($username) + ->firstOrFail(); + + if ($profile->domain == null) { + $url = "/$profile->username"; + } else { + $url = "/i/web/profile/_/{$profile->id}"; + } + + } else { + $profile = Profile::whereUsername($username) + ->whereNull('domain') + ->firstOrFail(); + $url = "/$profile->username"; + } + + return redirect($url); + } + + public function legacyWebfingerRedirect(Request $request, $username, $domain) + { + $un = '@'.$username.'@'.$domain; + $profile = Profile::whereUsername($un) + ->firstOrFail(); + + if ($profile->domain == null) { + $url = "/$profile->username"; + } else { + $url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url(); + } + + return redirect($url); + } + + public function legalNotice(Request $request) + { + $page = Cache::remember('site:legal-notice', now()->addDays(120), function () { + $slug = '/site/legal-notice'; + + return Page::whereSlug($slug)->whereActive(true)->first(); + }); + abort_if(! $page, 404); + + return View::make('site.legal-notice')->with(compact('page'))->render(); + } + + public function curatedOnboarding(Request $request) + { + if ($request->user()) { + return redirect('/i/web'); + } + + $regOpen = (bool) config_cache('pixelfed.open_registration'); + $curOnboarding = (bool) config_cache('instance.curated_registration.enabled'); + $curOnlyClosed = (bool) config('instance.curated_registration.state.only_enabled_on_closed_reg'); + if ($regOpen) { + if ($curOnlyClosed) { + return redirect('/register'); + } + } else { + if (! $curOnboarding) { + return redirect('/'); + } + } + + return view('auth.curated-register.index', ['step' => 1]); + } } diff --git a/app/Http/Controllers/SoftwareUpdateController.php b/app/Http/Controllers/SoftwareUpdateController.php new file mode 100644 index 000000000..e29359830 --- /dev/null +++ b/app/Http/Controllers/SoftwareUpdateController.php @@ -0,0 +1,21 @@ +middleware('auth'); + $this->middleware('admin'); + } + + public function getSoftwareUpdateCheck(Request $request) + { + $res = SoftwareUpdateService::get(); + return $res; + } +} diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index 873f5eace..b523f8add 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -2,458 +2,502 @@ namespace App\Http\Controllers; -use App\Jobs\ImageOptimizePipeline\ImageOptimize; -use App\Jobs\StatusPipeline\NewStatusPipeline; -use App\Jobs\StatusPipeline\StatusDelete; -use App\Jobs\StatusPipeline\RemoteStatusDelete; +use App\AccountInterstitial; use App\Jobs\SharePipeline\SharePipeline; use App\Jobs\SharePipeline\UndoSharePipeline; -use App\AccountInterstitial; -use App\Media; +use App\Jobs\StatusPipeline\RemoteStatusDelete; +use App\Jobs\StatusPipeline\StatusDelete; use App\Profile; +use App\Services\AccountService; +use App\Services\HashidService; +use App\Services\ReblogService; +use App\Services\StatusService; use App\Status; -use App\StatusArchived; use App\StatusView; -use App\Transformer\ActivityPub\StatusTransformer; use App\Transformer\ActivityPub\Verb\Note; use App\Transformer\ActivityPub\Verb\Question; -use App\User; -use Auth, DB, Cache; +use App\Util\Media\License; +use Auth; +use Cache; +use DB; use Illuminate\Http\Request; use League\Fractal; -use App\Util\Media\Filter; -use Illuminate\Support\Str; -use App\Services\HashidService; -use App\Services\StatusService; -use App\Util\Media\License; -use App\Services\ReblogService; class StatusController extends Controller { - public function show(Request $request, $username, $id) - { - // redirect authed users to Metro 2.0 - if($request->user()) { - // unless they force static view - if(!$request->has('fs') || $request->input('fs') != '1') { - return redirect('/i/web/post/' . $id); - } - } - - $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); - - if($user->status != null) { - return ProfileController::accountCheck($user); - } - - $status = Status::whereProfileId($user->id) - ->whereNull('reblog_of_id') - ->whereIn('scope', ['public','unlisted', 'private']) - ->findOrFail($id); - - if($status->uri || $status->url) { - $url = $status->uri ?? $status->url; - if(ends_with($url, '/activity')) { - $url = str_replace('/activity', '', $url); - } - return redirect($url); - } - - if($status->visibility == 'private' || $user->is_private) { - if(!Auth::check()) { - abort(404); - } - $pid = Auth::user()->profile; - if($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) { - abort(404); - } - } - - if($status->type == 'archived') { - if(Auth::user()->profile_id !== $status->profile_id) { - abort(404); - } - } - - if($request->user() && $request->user()->profile_id != $status->profile_id) { - StatusView::firstOrCreate([ - 'status_id' => $status->id, - 'status_profile_id' => $status->profile_id, - 'profile_id' => $request->user()->profile_id - ]); - } - - if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) { - return $this->showActivityPub($request, $status); - } - - $template = $status->in_reply_to_id ? 'status.reply' : 'status.show'; - return view($template, compact('user', 'status')); - } - - public function shortcodeRedirect(Request $request, $id) - { - abort(404); - } - - public function showId(int $id) - { - abort(404); - $status = Status::whereNull('reblog_of_id') - ->whereIn('scope', ['public', 'unlisted']) - ->findOrFail($id); - return redirect($status->url()); - } - - public function showEmbed(Request $request, $username, int $id) - { - if(!config('instance.embed.post')) { - $res = view('status.embed-removed'); - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } - - $profile = Profile::whereNull(['domain','status']) - ->whereIsPrivate(false) - ->whereUsername($username) - ->first(); - - if(!$profile) { - $content = view('status.embed-removed'); - return response($content)->header('X-Frame-Options', 'ALLOWALL'); - } - - $aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) { - $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); - if($exists) { - return true; - } - - return false; - }); - - if($aiCheck) { - $res = view('status.embed-removed'); - return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } - $status = Status::whereProfileId($profile->id) - ->whereNull('uri') - ->whereScope('public') - ->whereIsNsfw(false) - ->whereIn('type', ['photo', 'video','photo:album']) - ->find($id); - if(!$status) { - $content = view('status.embed-removed'); - return response($content)->header('X-Frame-Options', 'ALLOWALL'); - } - $showLikes = $request->filled('likes') && $request->likes == true; - $showCaption = $request->filled('caption') && $request->caption !== false; - $layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full'; - $content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout')); - return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); - } - - public function showObject(Request $request, $username, int $id) - { - $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); - - if($user->status != null) { - return ProfileController::accountCheck($user); - } - - $status = Status::whereProfileId($user->id) - ->whereNotIn('visibility',['draft','direct']) - ->findOrFail($id); - - abort_if($status->uri, 404); - - if($status->visibility == 'private' || $user->is_private) { - if(!Auth::check()) { - abort(403); - } - $pid = Auth::user()->profile; - if($user->followedBy($pid) == false && $user->id !== $pid->id) { - abort(403); - } - } - - return $this->showActivityPub($request, $status); - } - - public function compose() - { - $this->authCheck(); - - return view('status.compose'); - } - - public function store(Request $request) - { - return; - } - - public function delete(Request $request) - { - $this->authCheck(); - - $this->validate($request, [ - 'item' => 'required|integer|min:1', - ]); - - $status = Status::findOrFail($request->input('item')); - - $user = Auth::user(); - - if($status->profile_id != $user->profile->id && - $user->is_admin == true && - $status->uri == null - ) { - $media = $status->media; - - $ai = new AccountInterstitial; - $ai->user_id = $status->profile->user_id; - $ai->type = 'post.removed'; - $ai->view = 'account.moderation.post.removed'; - $ai->item_type = 'App\Status'; - $ai->item_id = $status->id; - $ai->has_media = (bool) $media->count(); - $ai->blurhash = $media->count() ? $media->first()->blurhash : null; - $ai->meta = json_encode([ - 'caption' => $status->caption, - 'created_at' => $status->created_at, - 'type' => $status->type, - 'url' => $status->url(), - 'is_nsfw' => $status->is_nsfw, - 'scope' => $status->scope, - 'reblog' => $status->reblog_of_id, - 'likes_count' => $status->likes_count, - 'reblogs_count' => $status->reblogs_count, - ]); - $ai->save(); - - $u = $status->profile->user; - $u->has_interstitial = true; - $u->save(); - } - - if($status->in_reply_to_id) { - $parent = Status::find($status->in_reply_to_id); - if($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) { - Cache::forget('_api:statuses:recent_9:' . $status->profile_id); - Cache::forget('profile:status_count:' . $status->profile_id); - Cache::forget('profile:embed:' . $status->profile_id); - StatusService::del($status->id, true); - Cache::forget('profile:status_count:'.$status->profile_id); - $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); - } - } else if ($status->profile_id == $user->profile_id || $user->is_admin == true) { - Cache::forget('_api:statuses:recent_9:' . $status->profile_id); - Cache::forget('profile:status_count:' . $status->profile_id); - Cache::forget('profile:embed:' . $status->profile_id); - StatusService::del($status->id, true); - Cache::forget('profile:status_count:'.$status->profile_id); - $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); - } - - if($request->wantsJson()) { - return response()->json(['Status successfully deleted.']); - } else { - return redirect($user->url()); - } - } - - public function storeShare(Request $request) - { - $this->authCheck(); - - $this->validate($request, [ - 'item' => 'required|integer|min:1', - ]); - - $user = Auth::user(); - $profile = $user->profile; - $status = Status::whereScope('public') - ->findOrFail($request->input('item')); - - $count = $status->reblogs_count; - - $exists = Status::whereProfileId(Auth::user()->profile->id) - ->whereReblogOfId($status->id) - ->exists(); - if ($exists == true) { - $shares = Status::whereProfileId(Auth::user()->profile->id) - ->whereReblogOfId($status->id) - ->get(); - foreach ($shares as $share) { - UndoSharePipeline::dispatch($share); - ReblogService::del($profile->id, $status->id); - $count--; - } - } else { - $share = new Status(); - $share->profile_id = $profile->id; - $share->reblog_of_id = $status->id; - $share->in_reply_to_profile_id = $status->profile_id; - $share->type = 'share'; - $share->save(); - $count++; - SharePipeline::dispatch($share); - ReblogService::add($profile->id, $status->id); - } - - Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id); - StatusService::del($status->id); - - if ($request->ajax()) { - $response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count]; - } else { - $response = redirect($status->url()); - } - - return $response; - } - - public function showActivityPub(Request $request, $status) - { - $object = $status->type == 'poll' ? new Question() : new Note(); - $fractal = new Fractal\Manager(); - $resource = new Fractal\Resource\Item($status, $object); - $res = $fractal->createData($resource)->toArray(); - - return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - - public function edit(Request $request, $username, $id) - { - $this->authCheck(); - $user = Auth::user()->profile; - $status = Status::whereProfileId($user->id) - ->with(['media']) - ->findOrFail($id); - $licenses = License::get(); - return view('status.edit', compact('user', 'status', 'licenses')); - } - - public function editStore(Request $request, $username, $id) - { - $this->authCheck(); - $user = Auth::user()->profile; - $status = Status::whereProfileId($user->id) - ->with(['media']) - ->findOrFail($id); - - $this->validate($request, [ - 'license' => 'nullable|integer|min:1|max:16', - ]); - - $licenseId = $request->input('license'); - - $status->media->each(function($media) use($licenseId) { - $media->license = $licenseId; - $media->save(); - Cache::forget('status:transformer:media:attachments:'.$media->status_id); - }); - - return redirect($status->url()); - } - - protected function authCheck() - { - if (Auth::check() == false) { - abort(403); - } - } - - protected function validateVisibility($visibility) - { - $allowed = ['public', 'unlisted', 'private']; - return in_array($visibility, $allowed) ? $visibility : 'public'; - } - - public static function mimeTypeCheck($mimes) - { - $allowed = explode(',', config_cache('pixelfed.media_types')); - $count = count($mimes); - $photos = 0; - $videos = 0; - foreach($mimes as $mime) { - if(in_array($mime, $allowed) == false && $mime !== 'video/mp4') { - continue; - } - if(str_contains($mime, 'image/')) { - $photos++; - } - if(str_contains($mime, 'video/')) { - $videos++; - } - } - if($photos == 1 && $videos == 0) { - return 'photo'; - } - if($videos == 1 && $photos == 0) { - return 'video'; - } - if($photos > 1 && $videos == 0) { - return 'photo:album'; - } - if($videos > 1 && $photos == 0) { - return 'video:album'; - } - if($photos >= 1 && $videos >= 1) { - return 'photo:video:album'; - } - - return 'text'; - } - - public function toggleVisibility(Request $request) { - $this->authCheck(); - $this->validate($request, [ - 'item' => 'required|string|min:1|max:20', - 'disableComments' => 'required|boolean' - ]); - - $user = Auth::user(); - $id = $request->input('item'); - $state = $request->input('disableComments'); - - $status = Status::findOrFail($id); - - if($status->profile_id != $user->profile->id && $user->is_admin == false) { - abort(403); - } - - $status->comments_disabled = $status->comments_disabled == true ? false : true; - $status->save(); - - return response()->json([200]); - } - - public function storeView(Request $request) - { - abort_if(!$request->user(), 403); - - $views = $request->input('_v'); - $uid = $request->user()->profile_id; - - if(empty($views) || !is_array($views)) { - return response()->json(0); - } - - Cache::forget('profile:home-timeline-cursor:' . $request->user()->id); - - foreach($views as $view) { - if(!isset($view['sid']) || !isset($view['pid'])) { - continue; - } - DB::transaction(function () use($view, $uid) { - StatusView::firstOrCreate([ - 'status_id' => $view['sid'], - 'status_profile_id' => $view['pid'], - 'profile_id' => $uid - ]); - }); - } - - return response()->json(1); - } + public function show(Request $request, $username, $id) + { + // redirect authed users to Metro 2.0 + if ($request->user()) { + // unless they force static view + if (! $request->has('fs') || $request->input('fs') != '1') { + return redirect('/i/web/post/'.$id); + } + } + + $status = StatusService::get($id, false); + + abort_if( + ! $status || + ! isset($status['account'], $status['account']['username']) || + $status['account']['username'] != $username || + isset($status['reblog']), 404); + + abort_if(! in_array($status['visibility'], ['public', 'unlisted']) && ! $request->user(), 403, 'Invalid permission'); + + if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) { + return $this->showActivityPub($request, $status); + } + + $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); + if ($user->status != null) { + return ProfileController::accountCheck($user); + } + + $status = Status::whereProfileId($user->id) + ->whereNull('reblog_of_id') + ->whereIn('scope', ['public', 'unlisted', 'private']) + ->findOrFail($id); + + if ($status->uri || $status->url) { + $url = $status->uri ?? $status->url; + if (ends_with($url, '/activity')) { + $url = str_replace('/activity', '', $url); + } + + return redirect($url); + } + + if ($status->visibility == 'private' || $user->is_private) { + if (! Auth::check()) { + abort(404); + } + $pid = Auth::user()->profile; + if ($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) { + abort(404); + } + } + + if ($status->type == 'archived') { + if (Auth::user()->profile_id !== $status->profile_id) { + abort(404); + } + } + + $template = $status->in_reply_to_id ? 'status.reply' : 'status.show'; + + return view($template, compact('user', 'status')); + } + + public function shortcodeRedirect(Request $request, $id) + { + $hid = HashidService::decode($id); + abort_if(! $hid, 404); + + return redirect('/i/web/post/'.$hid); + } + + public function showId(int $id) + { + abort(404); + $status = Status::whereNull('reblog_of_id') + ->whereIn('scope', ['public', 'unlisted']) + ->findOrFail($id); + + return redirect($status->url()); + } + + public function showEmbed(Request $request, $username, int $id) + { + if (! (bool) config_cache('instance.embed.post')) { + $res = view('status.embed-removed'); + + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + $status = StatusService::get($id); + + if ( + ! $status || + ! isset($status['account'], $status['account']['id'], $status['local']) || + ! $status['local'] || + strtolower($status['account']['username']) !== strtolower($username) || + isset($status['account']['moved'], $status['account']['moved']['id']) + ) { + $content = view('status.embed-removed'); + + return response($content, 404)->header('X-Frame-Options', 'ALLOWALL'); + } + + $profile = AccountService::get($status['account']['id'], true); + + if (! $profile || $profile['locked'] || ! $profile['local']) { + $content = view('status.embed-removed'); + + return response($content)->header('X-Frame-Options', 'ALLOWALL'); + } + + $embedCheck = AccountService::canEmbed($profile['id']); + + if (! $embedCheck) { + $content = view('status.embed-removed'); + + return response($content)->header('X-Frame-Options', 'ALLOWALL'); + } + + $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 3600, function () use ($profile) { + $user = Profile::find($profile['id']); + if (! $user) { + return true; + } + $exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count(); + if ($exists) { + return true; + } + + return false; + }); + + if ($aiCheck) { + $res = view('status.embed-removed'); + + return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + $status = StatusService::get($id); + + if ( + ! $status || + ! isset($status['account'], $status['account']['id']) || + intval($status['account']['id']) !== intval($profile['id']) || + $status['sensitive'] || + $status['visibility'] !== 'public' || + ! in_array($status['pf_type'], ['photo', 'photo:album']) + ) { + $content = view('status.embed-removed'); + + return response($content)->header('X-Frame-Options', 'ALLOWALL'); + } + + $showLikes = $request->filled('likes') && $request->likes == true; + $showCaption = $request->filled('caption') && $request->caption !== false; + $layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full'; + $content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout')); + + return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); + } + + public function showObject(Request $request, $username, int $id) + { + $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); + + if ($user->status != null) { + return ProfileController::accountCheck($user); + } + + $status = Status::whereProfileId($user->id) + ->whereNotIn('visibility', ['draft', 'direct']) + ->findOrFail($id); + + abort_if($status->uri, 404); + + if ($status->visibility == 'private' || $user->is_private) { + if (! Auth::check()) { + abort(403); + } + $pid = Auth::user()->profile; + if ($user->followedBy($pid) == false && $user->id !== $pid->id) { + abort(403); + } + } + + return $this->showActivityPub($request, $status); + } + + public function compose() + { + $this->authCheck(); + + return view('status.compose'); + } + + public function store(Request $request) {} + + public function delete(Request $request) + { + $this->authCheck(); + + $this->validate($request, [ + 'item' => 'required|integer|min:1', + ]); + + $status = Status::findOrFail($request->input('item')); + + $user = Auth::user(); + + if ($status->profile_id != $user->profile->id && + $user->is_admin == true && + $status->uri == null + ) { + $media = $status->media; + + $ai = new AccountInterstitial; + $ai->user_id = $status->profile->user_id; + $ai->type = 'post.removed'; + $ai->view = 'account.moderation.post.removed'; + $ai->item_type = 'App\Status'; + $ai->item_id = $status->id; + $ai->has_media = (bool) $media->count(); + $ai->blurhash = $media->count() ? $media->first()->blurhash : null; + $ai->meta = json_encode([ + 'caption' => $status->caption, + 'created_at' => $status->created_at, + 'type' => $status->type, + 'url' => $status->url(), + 'is_nsfw' => $status->is_nsfw, + 'scope' => $status->scope, + 'reblog' => $status->reblog_of_id, + 'likes_count' => $status->likes_count, + 'reblogs_count' => $status->reblogs_count, + ]); + $ai->save(); + + $u = $status->profile->user; + $u->has_interstitial = true; + $u->save(); + } + + if ($status->in_reply_to_id) { + $parent = Status::find($status->in_reply_to_id); + if ($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) { + Cache::forget('_api:statuses:recent_9:'.$status->profile_id); + Cache::forget('profile:status_count:'.$status->profile_id); + Cache::forget('profile:embed:'.$status->profile_id); + StatusService::del($status->id, true); + Cache::forget('profile:status_count:'.$status->profile_id); + $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); + } + } elseif ($status->profile_id == $user->profile_id || $user->is_admin == true) { + Cache::forget('_api:statuses:recent_9:'.$status->profile_id); + Cache::forget('profile:status_count:'.$status->profile_id); + Cache::forget('profile:embed:'.$status->profile_id); + StatusService::del($status->id, true); + Cache::forget('profile:status_count:'.$status->profile_id); + $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); + } + + if ($request->wantsJson()) { + return response()->json(['Status successfully deleted.']); + } else { + return redirect($user->url()); + } + } + + public function storeShare(Request $request) + { + $this->authCheck(); + + $this->validate($request, [ + 'item' => 'required|integer|min:1', + ]); + + $user = Auth::user(); + $profile = $user->profile; + $status = Status::whereScope('public') + ->findOrFail($request->input('item')); + $statusAccount = AccountService::get($status->profile_id); + abort_if(! $statusAccount || isset($statusAccount['moved'], $statusAccount['moved']['id']), 422, 'Account moved'); + + $count = $status->reblogs_count; + $defaultCaption = config_cache('database.default') === 'mysql' ? null : ""; + $exists = Status::whereProfileId(Auth::user()->profile->id) + ->whereReblogOfId($status->id) + ->exists(); + if ($exists == true) { + $shares = Status::whereProfileId(Auth::user()->profile->id) + ->whereReblogOfId($status->id) + ->get(); + foreach ($shares as $share) { + UndoSharePipeline::dispatch($share); + ReblogService::del($profile->id, $status->id); + $count--; + } + } else { + $share = new Status; + $share->caption = $defaultCaption; + $share->rendered = $defaultCaption; + $share->profile_id = $profile->id; + $share->reblog_of_id = $status->id; + $share->in_reply_to_profile_id = $status->profile_id; + $share->type = 'share'; + $share->save(); + $count++; + SharePipeline::dispatch($share); + ReblogService::add($profile->id, $status->id); + } + + Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id); + StatusService::del($status->id); + + if ($request->ajax()) { + $response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count]; + } else { + $response = redirect($status->url()); + } + + return $response; + } + + public function showActivityPub(Request $request, $status) + { + $key = 'pf:status:ap:v1:sid:'.$status['id']; + + return Cache::remember($key, 3600, function () use ($status) { + $status = Status::findOrFail($status['id']); + $object = $status->type == 'poll' ? new Question : new Note; + $fractal = new Fractal\Manager; + $resource = new Fractal\Resource\Item($status, $object); + $res = $fractal->createData($resource)->toArray(); + + return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + }); + } + + public function edit(Request $request, $username, $id) + { + $this->authCheck(); + $user = Auth::user()->profile; + $status = Status::whereProfileId($user->id) + ->with(['media']) + ->findOrFail($id); + $licenses = License::get(); + + return view('status.edit', compact('user', 'status', 'licenses')); + } + + public function editStore(Request $request, $username, $id) + { + $this->authCheck(); + $user = Auth::user()->profile; + $status = Status::whereProfileId($user->id) + ->with(['media']) + ->findOrFail($id); + + $this->validate($request, [ + 'license' => 'nullable|integer|min:1|max:16', + ]); + + $licenseId = $request->input('license'); + + $status->media->each(function ($media) use ($licenseId) { + $media->license = $licenseId; + $media->save(); + Cache::forget('status:transformer:media:attachments:'.$media->status_id); + }); + + return redirect($status->url()); + } + + protected function authCheck() + { + if (Auth::check() == false) { + abort(403); + } + } + + protected function validateVisibility($visibility) + { + $allowed = ['public', 'unlisted', 'private']; + + return in_array($visibility, $allowed) ? $visibility : 'public'; + } + + public static function mimeTypeCheck($mimes) + { + $allowed = explode(',', config_cache('pixelfed.media_types')); + $count = count($mimes); + $photos = 0; + $videos = 0; + foreach ($mimes as $mime) { + if (in_array($mime, $allowed) == false && $mime !== 'video/mp4') { + continue; + } + if (str_contains($mime, 'image/')) { + $photos++; + } + if (str_contains($mime, 'video/')) { + $videos++; + } + } + if ($photos == 1 && $videos == 0) { + return 'photo'; + } + if ($videos == 1 && $photos == 0) { + return 'video'; + } + if ($photos > 1 && $videos == 0) { + return 'photo:album'; + } + if ($videos > 1 && $photos == 0) { + return 'video:album'; + } + if ($photos >= 1 && $videos >= 1) { + return 'photo:video:album'; + } + + return 'text'; + } + + public function toggleVisibility(Request $request) + { + $this->authCheck(); + $this->validate($request, [ + 'item' => 'required|string|min:1|max:20', + 'disableComments' => 'required|boolean', + ]); + + $user = Auth::user(); + $id = $request->input('item'); + $state = $request->input('disableComments'); + + $status = Status::findOrFail($id); + + if ($status->profile_id != $user->profile->id && $user->is_admin == false) { + abort(403); + } + + $status->comments_disabled = $status->comments_disabled == true ? false : true; + $status->save(); + + return response()->json([200]); + } + + public function storeView(Request $request) + { + abort_if(! $request->user(), 403); + + $views = $request->input('_v'); + $uid = $request->user()->profile_id; + + if (empty($views) || ! is_array($views)) { + return response()->json(0); + } + + Cache::forget('profile:home-timeline-cursor:'.$request->user()->id); + + foreach ($views as $view) { + if (! isset($view['sid']) || ! isset($view['pid'])) { + continue; + } + DB::transaction(function () use ($view, $uid) { + StatusView::firstOrCreate([ + 'status_id' => $view['sid'], + 'status_profile_id' => $view['pid'], + 'profile_id' => $uid, + ]); + }); + } + + return response()->json(1); + } } diff --git a/app/Http/Controllers/Stories/StoryApiV1Controller.php b/app/Http/Controllers/Stories/StoryApiV1Controller.php index db2b1f533..48599fc3a 100644 --- a/app/Http/Controllers/Stories/StoryApiV1Controller.php +++ b/app/Http/Controllers/Stories/StoryApiV1Controller.php @@ -2,380 +2,513 @@ namespace App\Http\Controllers\Stories; -use App\Http\Controllers\Controller; -use Illuminate\Http\Request; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Storage; -use App\Models\Conversation; use App\DirectMessage; -use App\Notification; -use App\Story; -use App\Status; -use App\StoryView; +use App\Http\Controllers\Controller; +use App\Http\Resources\StoryView as StoryViewResource; use App\Jobs\StoryPipeline\StoryDelete; use App\Jobs\StoryPipeline\StoryFanout; use App\Jobs\StoryPipeline\StoryReplyDeliver; use App\Jobs\StoryPipeline\StoryViewDeliver; +use App\Models\Conversation; +use App\Notification; use App\Services\AccountService; use App\Services\MediaPathService; use App\Services\StoryService; -use App\Http\Resources\StoryView as StoryViewResource; +use App\Status; +use App\Story; +use App\StoryView; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; class StoryApiV1Controller extends Controller { - public function carousel(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $pid = $request->user()->profile_id; + const RECENT_KEY = 'pf:stories:recent-by-id:'; - if(config('database.default') == 'pgsql') { - $s = Story::select('stories.*', 'followers.following_id') - ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') - ->where('followers.profile_id', $pid) - ->where('stories.active', true) - ->get(); - } else { - $s = Story::select('stories.*', 'followers.following_id') - ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') - ->where('followers.profile_id', $pid) - ->where('stories.active', true) - ->orderBy('id') - ->get(); - } + const RECENT_TTL = 300; - $nodes = $s->map(function($s) use($pid) { - $profile = AccountService::get($s->profile_id, true); - if(!$profile || !isset($profile['id'])) { - return false; - } + public function carousel(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + $pid = $request->user()->profile_id; - return [ - 'id' => (string) $s->id, - 'pid' => (string) $s->profile_id, - 'type' => $s->type, - 'src' => url(Storage::url($s->path)), - 'duration' => $s->duration ?? 3, - 'seen' => StoryService::hasSeen($pid, $s->id), - 'created_at' => $s->created_at->format('c') - ]; - }) - ->filter() - ->groupBy('pid') - ->map(function($item) use($pid) { - $profile = AccountService::get($item[0]['pid'], true); - $url = $profile['local'] ? url("/stories/{$profile['username']}") : - url("/i/rs/{$profile['id']}"); - return [ - 'id' => 'pfs:' . $profile['id'], - 'user' => [ - 'id' => (string) $profile['id'], - 'username' => $profile['username'], - 'username_acct' => $profile['acct'], - 'avatar' => $profile['avatar'], - 'local' => $profile['local'], - 'is_author' => $profile['id'] == $pid - ], - 'nodes' => $item, - 'url' => $url, - 'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])), - ]; - }) - ->sortBy('seen') - ->values(); + if (config('database.default') == 'pgsql') { + $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->map(function ($s) { + $r = new \StdClass; + $r->id = $s->id; + $r->profile_id = $s->profile_id; + $r->type = $s->type; + $r->path = $s->path; - $res = [ - 'self' => [], - 'nodes' => $nodes, - ]; + return $r; + }) + ->unique('profile_id'); + }); + } else { + $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->orderBy('id') + ->get(); + }); + } - if(Story::whereProfileId($pid)->whereActive(true)->exists()) { - $selfStories = Story::whereProfileId($pid) - ->whereActive(true) - ->get() - ->map(function($s) use($pid) { - return [ - 'id' => (string) $s->id, - 'type' => $s->type, - 'src' => url(Storage::url($s->path)), - 'duration' => $s->duration, - 'seen' => true, - 'created_at' => $s->created_at->format('c') - ]; - }) - ->sortBy('id') - ->values(); - $selfProfile = AccountService::get($pid, true); - $res['self'] = [ - 'user' => [ - 'id' => (string) $selfProfile['id'], - 'username' => $selfProfile['acct'], - 'avatar' => $selfProfile['avatar'], - 'local' => $selfProfile['local'], - 'is_author' => true - ], + $nodes = $s->map(function ($s) use ($pid) { + $profile = AccountService::get($s->profile_id, true); + if (! $profile || ! isset($profile['id'])) { + return false; + } - 'nodes' => $selfStories, - ]; - } - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + return [ + 'id' => (string) $s->id, + 'pid' => (string) $s->profile_id, + 'type' => $s->type, + 'src' => url(Storage::url($s->path)), + 'duration' => $s->duration ?? 3, + 'seen' => StoryService::hasSeen($pid, $s->id), + 'created_at' => $s->created_at->format('c'), + ]; + }) + ->filter() + ->groupBy('pid') + ->map(function ($item) use ($pid) { + $profile = AccountService::get($item[0]['pid'], true); + $url = $profile['local'] ? url("/stories/{$profile['username']}") : + url("/i/rs/{$profile['id']}"); - public function add(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + return [ + 'id' => 'pfs:'.$profile['id'], + 'user' => [ + 'id' => (string) $profile['id'], + 'username' => $profile['username'], + 'username_acct' => $profile['acct'], + 'avatar' => $profile['avatar'], + 'local' => $profile['local'], + 'is_author' => $profile['id'] == $pid, + ], + 'nodes' => $item, + 'url' => $url, + 'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])), + ]; + }) + ->sortBy('seen') + ->values(); - $this->validate($request, [ - 'file' => function() { - return [ - 'required', - 'mimetypes:image/jpeg,image/png,video/mp4', - 'max:' . config_cache('pixelfed.max_photo_size'), - ]; - }, - 'duration' => 'sometimes|integer|min:0|max:30' - ]); + $res = [ + 'self' => [], + 'nodes' => $nodes, + ]; - $user = $request->user(); + if (Story::whereProfileId($pid)->whereActive(true)->exists()) { + $selfStories = Story::whereProfileId($pid) + ->whereActive(true) + ->get() + ->map(function ($s) { + return [ + 'id' => (string) $s->id, + 'type' => $s->type, + 'src' => url(Storage::url($s->path)), + 'duration' => $s->duration, + 'seen' => true, + 'created_at' => $s->created_at->format('c'), + ]; + }) + ->sortBy('id') + ->values(); + $selfProfile = AccountService::get($pid, true); + $res['self'] = [ + 'user' => [ + 'id' => (string) $selfProfile['id'], + 'username' => $selfProfile['acct'], + 'avatar' => $selfProfile['avatar'], + 'local' => $selfProfile['local'], + 'is_author' => true, + ], - $count = Story::whereProfileId($user->profile_id) - ->whereActive(true) - ->where('expires_at', '>', now()) - ->count(); + 'nodes' => $selfStories, + ]; + } - if($count >= Story::MAX_PER_DAY) { - abort(418, 'You have reached your limit for new Stories today.'); - } + return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } - $photo = $request->file('file'); - $path = $this->storeMedia($photo, $user); + public function selfCarousel(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + $pid = $request->user()->profile_id; - $story = new Story(); - $story->duration = $request->input('duration', 3); - $story->profile_id = $user->profile_id; - $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; - $story->mime = $photo->getMimeType(); - $story->path = $path; - $story->local = true; - $story->size = $photo->getSize(); - $story->bearcap_token = str_random(64); - $story->expires_at = now()->addMinutes(1440); - $story->save(); + if (config('database.default') == 'pgsql') { + $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->map(function ($s) { + $r = new \StdClass; + $r->id = $s->id; + $r->profile_id = $s->profile_id; + $r->type = $s->type; + $r->path = $s->path; - $url = $story->path; + return $r; + }) + ->unique('profile_id'); + }); + } else { + $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->orderBy('id') + ->get(); + }); + } - $res = [ - 'code' => 200, - 'msg' => 'Successfully added', - 'media_id' => (string) $story->id, - 'media_url' => url(Storage::url($url)) . '?v=' . time(), - 'media_type' => $story->type - ]; + $nodes = $s->map(function ($s) use ($pid) { + $profile = AccountService::get($s->profile_id, true); + if (! $profile || ! isset($profile['id'])) { + return false; + } - return $res; - } + return [ + 'id' => (string) $s->id, + 'pid' => (string) $s->profile_id, + 'type' => $s->type, + 'src' => url(Storage::url($s->path)), + 'duration' => $s->duration ?? 3, + 'seen' => StoryService::hasSeen($pid, $s->id), + 'created_at' => $s->created_at->format('c'), + ]; + }) + ->filter() + ->groupBy('pid') + ->map(function ($item) use ($pid) { + $profile = AccountService::get($item[0]['pid'], true); + $url = $profile['local'] ? url("/stories/{$profile['username']}") : + url("/i/rs/{$profile['id']}"); - public function publish(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + return [ + 'id' => 'pfs:'.$profile['id'], + 'user' => [ + 'id' => (string) $profile['id'], + 'username' => $profile['username'], + 'username_acct' => $profile['acct'], + 'avatar' => $profile['avatar'], + 'local' => $profile['local'], + 'is_author' => $profile['id'] == $pid, + ], + 'nodes' => $item, + 'url' => $url, + 'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])), + ]; + }) + ->sortBy('seen') + ->values(); - $this->validate($request, [ - 'media_id' => 'required', - 'duration' => 'required|integer|min:0|max:30', - 'can_reply' => 'required|boolean', - 'can_react' => 'required|boolean' - ]); + $selfProfile = AccountService::get($pid, true); + $res = [ + 'self' => [ + 'user' => [ + 'id' => (string) $selfProfile['id'], + 'username' => $selfProfile['acct'], + 'avatar' => $selfProfile['avatar'], + 'local' => $selfProfile['local'], + 'is_author' => true, + ], - $id = $request->input('media_id'); - $user = $request->user(); - $story = Story::whereProfileId($user->profile_id) - ->findOrFail($id); + 'nodes' => [], + ], + 'nodes' => $nodes, + ]; - $story->active = true; - $story->duration = $request->input('duration', 10); - $story->can_reply = $request->input('can_reply'); - $story->can_react = $request->input('can_react'); - $story->save(); + if (Story::whereProfileId($pid)->whereActive(true)->exists()) { + $selfStories = Story::whereProfileId($pid) + ->whereActive(true) + ->get() + ->map(function ($s) { + return [ + 'id' => (string) $s->id, + 'type' => $s->type, + 'src' => url(Storage::url($s->path)), + 'duration' => $s->duration, + 'seen' => true, + 'created_at' => $s->created_at->format('c'), + ]; + }) + ->sortBy('id') + ->values(); + $res['self']['nodes'] = $selfStories; + } - StoryService::delLatest($story->profile_id); - StoryFanout::dispatch($story)->onQueue('story'); - StoryService::addRotateQueue($story->id); + return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } - return [ - 'code' => 200, - 'msg' => 'Successfully published', - ]; - } + public function add(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); - public function delete(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'file' => function () { + return [ + 'required', + 'mimetypes:image/jpeg,image/png,video/mp4', + 'max:'.config_cache('pixelfed.max_photo_size'), + ]; + }, + 'duration' => 'sometimes|integer|min:0|max:30', + ]); - $user = $request->user(); + $user = $request->user(); - $story = Story::whereProfileId($user->profile_id) - ->findOrFail($id); - $story->active = false; - $story->save(); + $count = Story::whereProfileId($user->profile_id) + ->whereActive(true) + ->where('expires_at', '>', now()) + ->count(); - StoryDelete::dispatch($story)->onQueue('story'); + if ($count >= Story::MAX_PER_DAY) { + abort(418, 'You have reached your limit for new Stories today.'); + } - return [ - 'code' => 200, - 'msg' => 'Successfully deleted' - ]; - } + $photo = $request->file('file'); + $path = $this->storeMedia($photo, $user); - public function viewed(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $story = new Story; + $story->duration = $request->input('duration', 3); + $story->profile_id = $user->profile_id; + $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo'; + $story->mime = $photo->getMimeType(); + $story->path = $path; + $story->local = true; + $story->size = $photo->getSize(); + $story->bearcap_token = str_random(64); + $story->expires_at = now()->addMinutes(1440); + $story->save(); - $this->validate($request, [ - 'id' => 'required|min:1', - ]); - $id = $request->input('id'); + $url = $story->path; - $authed = $request->user()->profile; + $res = [ + 'code' => 200, + 'msg' => 'Successfully added', + 'media_id' => (string) $story->id, + 'media_url' => url(Storage::url($url)).'?v='.time(), + 'media_type' => $story->type, + ]; - $story = Story::with('profile') - ->findOrFail($id); - $exp = $story->expires_at; + return $res; + } - $profile = $story->profile; + public function publish(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); - if($story->profile_id == $authed->id) { - return []; - } + $this->validate($request, [ + 'media_id' => 'required', + 'duration' => 'required|integer|min:0|max:30', + 'can_reply' => 'required|boolean', + 'can_react' => 'required|boolean', + ]); - $publicOnly = (bool) $profile->followedBy($authed); - abort_if(!$publicOnly, 403); + $id = $request->input('media_id'); + $user = $request->user(); + $story = Story::whereProfileId($user->profile_id) + ->findOrFail($id); - $v = StoryView::firstOrCreate([ - 'story_id' => $id, - 'profile_id' => $authed->id - ]); + $story->active = true; + $story->duration = $request->input('duration', 10); + $story->can_reply = $request->input('can_reply'); + $story->can_react = $request->input('can_react'); + $story->save(); - if($v->wasRecentlyCreated) { - Story::findOrFail($story->id)->increment('view_count'); + StoryService::delLatest($story->profile_id); + StoryFanout::dispatch($story)->onQueue('story'); + StoryService::addRotateQueue($story->id); - if($story->local == false) { - StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); - } - } + return [ + 'code' => 200, + 'msg' => 'Successfully published', + ]; + } - Cache::forget('stories:recent:by_id:' . $authed->id); - StoryService::addSeen($authed->id, $story->id); - return ['code' => 200]; - } + public function delete(Request $request, $id) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); - public function comment(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'sid' => 'required', - 'caption' => 'required|string' - ]); - $pid = $request->user()->profile_id; - $text = $request->input('caption'); + $user = $request->user(); - $story = Story::findOrFail($request->input('sid')); + $story = Story::whereProfileId($user->profile_id) + ->findOrFail($id); + $story->active = false; + $story->save(); - abort_if(!$story->can_reply, 422); + StoryDelete::dispatch($story)->onQueue('story'); - $status = new Status; - $status->type = 'story:reply'; - $status->profile_id = $pid; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id - ]); - $status->save(); + return [ + 'code' => 200, + 'msg' => 'Successfully deleted', + ]; + } - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $pid; - $dm->type = 'story:comment'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $story->profile->username, - 'story_actor_username' => $request->user()->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'caption' => $text - ]); - $dm->save(); + public function viewed(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $pid - ], - [ - 'type' => 'story:comment', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); + $this->validate($request, [ + 'id' => 'required|min:1', + ]); + $id = $request->input('id'); - if($story->local) { - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:comment'; - $n->save(); - } else { - StoryReplyDeliver::dispatch($story, $status)->onQueue('story'); - } + $authed = $request->user()->profile; - return [ - 'code' => 200, - 'msg' => 'Sent!' - ]; - } + $story = Story::with('profile') + ->findOrFail($id); + $exp = $story->expires_at; - protected function storeMedia($photo, $user) - { - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), [ - 'image/jpeg', - 'image/png', - 'video/mp4' - ]) == false) { - abort(400, 'Invalid media type'); - return; - } + $profile = $story->profile; - $storagePath = MediaPathService::story($user->profile); - $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension()); - return $path; - } + if ($story->profile_id == $authed->id) { + return []; + } - public function viewers(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $publicOnly = (bool) $profile->followedBy($authed); + abort_if(! $publicOnly, 403); - $this->validate($request, [ - 'sid' => 'required|string|min:1|max:50' - ]); + $v = StoryView::firstOrCreate([ + 'story_id' => $id, + 'profile_id' => $authed->id, + ]); - $pid = $request->user()->profile_id; - $sid = $request->input('sid'); + if ($v->wasRecentlyCreated) { + Story::findOrFail($story->id)->increment('view_count'); - $story = Story::whereProfileId($pid) - ->whereActive(true) - ->findOrFail($sid); + if ($story->local == false) { + StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); + } + } - $viewers = StoryView::whereStoryId($story->id) + Cache::forget('stories:recent:by_id:'.$authed->id); + StoryService::addSeen($authed->id, $story->id); + + return ['code' => 200]; + } + + public function comment(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + $this->validate($request, [ + 'sid' => 'required', + 'caption' => 'required|string', + ]); + $pid = $request->user()->profile_id; + $text = $request->input('caption'); + + $story = Story::findOrFail($request->input('sid')); + + abort_if(! $story->can_reply, 422); + + $status = new Status; + $status->type = 'story:reply'; + $status->profile_id = $pid; + $status->caption = $text; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id, + ]); + $status->save(); + + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $pid; + $dm->type = 'story:comment'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $story->profile->username, + 'story_actor_username' => $request->user()->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'caption' => $text, + ]); + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $pid, + ], + [ + 'type' => 'story:comment', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false, + ] + ); + + if ($story->local) { + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:comment'; + $n->save(); + } else { + StoryReplyDeliver::dispatch($story, $status)->onQueue('story'); + } + + return [ + 'code' => 200, + 'msg' => 'Sent!', + ]; + } + + protected function storeMedia($photo, $user) + { + $mimes = explode(',', config_cache('pixelfed.media_types')); + if (in_array($photo->getMimeType(), [ + 'image/jpeg', + 'image/png', + 'video/mp4', + ]) == false) { + abort(400, 'Invalid media type'); + + return; + } + + $storagePath = MediaPathService::story($user->profile); + $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)).'_'.Str::random(random_int(32, 35)).'_'.Str::random(random_int(1, 14)).'.'.$photo->extension()); + + return $path; + } + + public function viewers(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + + $this->validate($request, [ + 'sid' => 'required|string|min:1|max:50', + ]); + + $pid = $request->user()->profile_id; + $sid = $request->input('sid'); + + $story = Story::whereProfileId($pid) + ->whereActive(true) + ->findOrFail($sid); + + $viewers = StoryView::whereStoryId($story->id) ->orderByDesc('id') - ->cursorPaginate(10); + ->cursorPaginate(10); - return StoryViewResource::collection($viewers); - } + return StoryViewResource::collection($viewers); + } } diff --git a/app/Http/Controllers/StoryComposeController.php b/app/Http/Controllers/StoryComposeController.php index 8f9358b74..e02e2d219 100644 --- a/app/Http/Controllers/StoryComposeController.php +++ b/app/Http/Controllers/StoryComposeController.php @@ -2,333 +2,338 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use Illuminate\Support\Str; -use App\Media; -use App\Profile; -use App\Report; use App\DirectMessage; -use App\Notification; -use App\Status; -use App\Story; -use App\StoryView; -use App\Models\Poll; -use App\Models\PollVote; -use App\Services\ProfileService; -use App\Services\StoryService; -use Cache, Storage; -use Image as Intervention; -use App\Services\FollowerService; -use App\Services\MediaPathService; -use FFMpeg; -use FFMpeg\Coordinate\Dimension; -use FFMpeg\Format\Video\X264; +use App\Jobs\StoryPipeline\StoryDelete; +use App\Jobs\StoryPipeline\StoryFanout; use App\Jobs\StoryPipeline\StoryReactionDeliver; use App\Jobs\StoryPipeline\StoryReplyDeliver; -use App\Jobs\StoryPipeline\StoryFanout; -use App\Jobs\StoryPipeline\StoryDelete; -use ImageOptimizer; use App\Models\Conversation; +use App\Models\Poll; +use App\Models\PollVote; +use App\Notification; +use App\Report; +use App\Services\FollowerService; +use App\Services\MediaPathService; +use App\Services\StoryService; +use App\Services\UserRoleService; +use App\Status; +use App\Story; +use FFMpeg; +use Illuminate\Http\Request; +use Illuminate\Support\Str; +use Image as Intervention; +use Storage; class StoryComposeController extends Controller { public function apiV1Add(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); - $this->validate($request, [ - 'file' => function() { - return [ - 'required', - 'mimetypes:image/jpeg,image/png,video/mp4', - 'max:' . config_cache('pixelfed.max_photo_size'), - ]; - }, - ]); - - $user = $request->user(); - - $count = Story::whereProfileId($user->profile_id) - ->whereActive(true) - ->where('expires_at', '>', now()) - ->count(); - - if($count >= Story::MAX_PER_DAY) { - abort(418, 'You have reached your limit for new Stories today.'); - } - - $photo = $request->file('file'); - $path = $this->storePhoto($photo, $user); - - $story = new Story(); - $story->duration = 3; - $story->profile_id = $user->profile_id; - $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; - $story->mime = $photo->getMimeType(); - $story->path = $path; - $story->local = true; - $story->size = $photo->getSize(); - $story->bearcap_token = str_random(64); - $story->expires_at = now()->addMinutes(1440); - $story->save(); - - $url = $story->path; - - $res = [ - 'code' => 200, - 'msg' => 'Successfully added', - 'media_id' => (string) $story->id, - 'media_url' => url(Storage::url($url)) . '?v=' . time(), - 'media_type' => $story->type - ]; - - if($story->type === 'video') { - $video = FFMpeg::open($path); - $duration = $video->getDurationInSeconds(); - $res['media_duration'] = $duration; - if($duration > 500) { - Storage::delete($story->path); - $story->delete(); - return response()->json([ - 'message' => 'Video duration cannot exceed 60 seconds' - ], 422); - } - } - - return $res; - } - - protected function storePhoto($photo, $user) - { - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), [ - 'image/jpeg', - 'image/png', - 'video/mp4' - ]) == false) { - abort(400, 'Invalid media type'); - return; - } - - $storagePath = MediaPathService::story($user->profile); - $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension()); - if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) { - $fpath = storage_path('app/' . $path); - $img = Intervention::make($fpath); - $img->orientate(); - $img->save($fpath, config_cache('pixelfed.image_quality')); - $img->destroy(); - } - return $path; - } - - public function cropPhoto(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - - $this->validate($request, [ - 'media_id' => 'required|integer|min:1', - 'width' => 'required', - 'height' => 'required', - 'x' => 'required', - 'y' => 'required' - ]); - - $user = $request->user(); - $id = $request->input('media_id'); - $width = round($request->input('width')); - $height = round($request->input('height')); - $x = round($request->input('x')); - $y = round($request->input('y')); - - $story = Story::whereProfileId($user->profile_id)->findOrFail($id); - - $path = storage_path('app/' . $story->path); - - if(!is_file($path)) { - abort(400, 'Invalid or missing media.'); - } - - if($story->type === 'photo') { - $img = Intervention::make($path); - $img->crop($width, $height, $x, $y); - $img->resize(1080, 1920, function ($constraint) { - $constraint->aspectRatio(); - }); - $img->save($path, config_cache('pixelfed.image_quality')); - } - - return [ - 'code' => 200, - 'msg' => 'Successfully cropped', - ]; - } - - public function publishStory(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - - $this->validate($request, [ - 'media_id' => 'required', - 'duration' => 'required|integer|min:3|max:120', - 'can_reply' => 'required|boolean', - 'can_react' => 'required|boolean' - ]); - - $id = $request->input('media_id'); - $user = $request->user(); - $story = Story::whereProfileId($user->profile_id) - ->findOrFail($id); - - $story->active = true; - $story->duration = $request->input('duration', 10); - $story->can_reply = $request->input('can_reply'); - $story->can_react = $request->input('can_react'); - $story->save(); - - StoryService::delLatest($story->profile_id); - StoryFanout::dispatch($story)->onQueue('story'); - StoryService::addRotateQueue($story->id); - - return [ - 'code' => 200, - 'msg' => 'Successfully published', - ]; - } - - public function apiV1Delete(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - - $user = $request->user(); - - $story = Story::whereProfileId($user->profile_id) - ->findOrFail($id); - $story->active = false; - $story->save(); - - StoryDelete::dispatch($story)->onQueue('story'); - - return [ - 'code' => 200, - 'msg' => 'Successfully deleted' - ]; - } - - public function compose(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - - return view('stories.compose'); - } - - public function createPoll(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - abort_if(!config_cache('instance.polls.enabled'), 404); - - return $request->all(); - } - - public function publishStoryPoll(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - - $this->validate($request, [ - 'question' => 'required|string|min:6|max:140', - 'options' => 'required|array|min:2|max:4', - 'can_reply' => 'required|boolean', - 'can_react' => 'required|boolean' - ]); - - $pid = $request->user()->profile_id; - - $count = Story::whereProfileId($pid) - ->whereActive(true) - ->where('expires_at', '>', now()) - ->count(); - - if($count >= Story::MAX_PER_DAY) { - abort(418, 'You have reached your limit for new Stories today.'); - } - - $story = new Story; - $story->type = 'poll'; - $story->story = json_encode([ - 'question' => $request->input('question'), - 'options' => $request->input('options') - ]); - $story->public = false; - $story->local = true; - $story->profile_id = $pid; - $story->expires_at = now()->addMinutes(1440); - $story->duration = 30; - $story->can_reply = false; - $story->can_react = false; - $story->save(); - - $poll = new Poll; - $poll->story_id = $story->id; - $poll->profile_id = $pid; - $poll->poll_options = $request->input('options'); - $poll->expires_at = $story->expires_at; - $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { - return 0; - })->toArray(); - $poll->save(); - - $story->active = true; - $story->save(); - - StoryService::delLatest($story->profile_id); - - return [ - 'code' => 200, - 'msg' => 'Successfully published', - ]; - } - - public function storyPollVote(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - - $this->validate($request, [ - 'sid' => 'required', - 'ci' => 'required|integer|min:0|max:3' - ]); - - $pid = $request->user()->profile_id; - $ci = $request->input('ci'); - $story = Story::findOrFail($request->input('sid')); - abort_if(!FollowerService::follows($pid, $story->profile_id), 403); - $poll = Poll::whereStoryId($story->id)->firstOrFail(); - - $vote = new PollVote; - $vote->profile_id = $pid; - $vote->poll_id = $poll->id; - $vote->story_id = $story->id; - $vote->status_id = null; - $vote->choice = $ci; - $vote->save(); - - $poll->votes_count = $poll->votes_count + 1; - $poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) { - return $ci == $key ? $tally + 1 : $tally; - })->toArray(); - $poll->save(); - - return 200; - } - - public function storeReport(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - - $this->validate($request, [ - 'type' => 'required|alpha_dash', - 'id' => 'required|integer|min:1', + $this->validate($request, [ + 'file' => function () { + return [ + 'required', + 'mimetypes:image/jpeg,image/png,video/mp4', + 'max:'.config_cache('pixelfed.max_photo_size'), + ]; + }, ]); + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $count = Story::whereProfileId($user->profile_id) + ->whereActive(true) + ->where('expires_at', '>', now()) + ->count(); + + if ($count >= Story::MAX_PER_DAY) { + abort(418, 'You have reached your limit for new Stories today.'); + } + + $photo = $request->file('file'); + $path = $this->storePhoto($photo, $user); + + $story = new Story; + $story->duration = 3; + $story->profile_id = $user->profile_id; + $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo'; + $story->mime = $photo->getMimeType(); + $story->path = $path; + $story->local = true; + $story->size = $photo->getSize(); + $story->bearcap_token = str_random(64); + $story->expires_at = now()->addMinutes(1440); + $story->save(); + + $url = $story->path; + + $res = [ + 'code' => 200, + 'msg' => 'Successfully added', + 'media_id' => (string) $story->id, + 'media_url' => url(Storage::url($url)).'?v='.time(), + 'media_type' => $story->type, + ]; + + if ($story->type === 'video') { + $video = FFMpeg::open($path); + $duration = $video->getDurationInSeconds(); + $res['media_duration'] = $duration; + if ($duration > 500) { + Storage::delete($story->path); + $story->delete(); + + return response()->json([ + 'message' => 'Video duration cannot exceed 60 seconds', + ], 422); + } + } + + return $res; + } + + protected function storePhoto($photo, $user) + { + $mimes = explode(',', config_cache('pixelfed.media_types')); + if (in_array($photo->getMimeType(), [ + 'image/jpeg', + 'image/png', + 'video/mp4', + ]) == false) { + abort(400, 'Invalid media type'); + + return; + } + + $storagePath = MediaPathService::story($user->profile); + $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)).'_'.Str::random(random_int(32, 35)).'_'.Str::random(random_int(1, 14)).'.'.$photo->extension()); + if (in_array($photo->getMimeType(), ['image/jpeg', 'image/png'])) { + $fpath = storage_path('app/'.$path); + $img = Intervention::make($fpath); + $img->orientate(); + $img->save($fpath, config_cache('pixelfed.image_quality')); + $img->destroy(); + } + + return $path; + } + + public function cropPhoto(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + + $this->validate($request, [ + 'media_id' => 'required|integer|min:1', + 'width' => 'required', + 'height' => 'required', + 'x' => 'required', + 'y' => 'required', + ]); + + $user = $request->user(); + $id = $request->input('media_id'); + $width = round($request->input('width')); + $height = round($request->input('height')); + $x = round($request->input('x')); + $y = round($request->input('y')); + + $story = Story::whereProfileId($user->profile_id)->findOrFail($id); + + $path = storage_path('app/'.$story->path); + + if (! is_file($path)) { + abort(400, 'Invalid or missing media.'); + } + + if ($story->type === 'photo') { + $img = Intervention::make($path); + $img->crop($width, $height, $x, $y); + $img->resize(1080, 1920, function ($constraint) { + $constraint->aspectRatio(); + }); + $img->save($path, config_cache('pixelfed.image_quality')); + } + + return [ + 'code' => 200, + 'msg' => 'Successfully cropped', + ]; + } + + public function publishStory(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + + $this->validate($request, [ + 'media_id' => 'required', + 'duration' => 'required|integer|min:3|max:120', + 'can_reply' => 'required|boolean', + 'can_react' => 'required|boolean', + ]); + + $id = $request->input('media_id'); + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $story = Story::whereProfileId($user->profile_id) + ->findOrFail($id); + + $story->active = true; + $story->duration = $request->input('duration', 10); + $story->can_reply = $request->input('can_reply'); + $story->can_react = $request->input('can_react'); + $story->save(); + + StoryService::delLatest($story->profile_id); + StoryFanout::dispatch($story)->onQueue('story'); + StoryService::addRotateQueue($story->id); + + return [ + 'code' => 200, + 'msg' => 'Successfully published', + ]; + } + + public function apiV1Delete(Request $request, $id) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + + $user = $request->user(); + + $story = Story::whereProfileId($user->profile_id) + ->findOrFail($id); + $story->active = false; + $story->save(); + + StoryDelete::dispatch($story)->onQueue('story'); + + return [ + 'code' => 200, + 'msg' => 'Successfully deleted', + ]; + } + + public function compose(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + + return view('stories.compose'); + } + + public function createPoll(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + abort_if(! config_cache('instance.polls.enabled'), 404); + + return $request->all(); + } + + public function publishStoryPoll(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + + $this->validate($request, [ + 'question' => 'required|string|min:6|max:140', + 'options' => 'required|array|min:2|max:4', + 'can_reply' => 'required|boolean', + 'can_react' => 'required|boolean', + ]); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $pid = $request->user()->profile_id; + + $count = Story::whereProfileId($pid) + ->whereActive(true) + ->where('expires_at', '>', now()) + ->count(); + + if ($count >= Story::MAX_PER_DAY) { + abort(418, 'You have reached your limit for new Stories today.'); + } + + $story = new Story; + $story->type = 'poll'; + $story->story = json_encode([ + 'question' => $request->input('question'), + 'options' => $request->input('options'), + ]); + $story->public = false; + $story->local = true; + $story->profile_id = $pid; + $story->expires_at = now()->addMinutes(1440); + $story->duration = 30; + $story->can_reply = false; + $story->can_react = false; + $story->save(); + + $poll = new Poll; + $poll->story_id = $story->id; + $poll->profile_id = $pid; + $poll->poll_options = $request->input('options'); + $poll->expires_at = $story->expires_at; + $poll->cached_tallies = collect($poll->poll_options)->map(function ($o) { + return 0; + })->toArray(); + $poll->save(); + + $story->active = true; + $story->save(); + + StoryService::delLatest($story->profile_id); + + return [ + 'code' => 200, + 'msg' => 'Successfully published', + ]; + } + + public function storyPollVote(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + + $this->validate($request, [ + 'sid' => 'required', + 'ci' => 'required|integer|min:0|max:3', + ]); + + $pid = $request->user()->profile_id; + $ci = $request->input('ci'); + $story = Story::findOrFail($request->input('sid')); + abort_if(! FollowerService::follows($pid, $story->profile_id), 403); + $poll = Poll::whereStoryId($story->id)->firstOrFail(); + + $vote = new PollVote; + $vote->profile_id = $pid; + $vote->poll_id = $poll->id; + $vote->story_id = $story->id; + $vote->status_id = null; + $vote->choice = $ci; + $vote->save(); + + $poll->votes_count = $poll->votes_count + 1; + $poll->cached_tallies = collect($poll->getTallies())->map(function ($tally, $key) use ($ci) { + return $ci == $key ? $tally + 1 : $tally; + })->toArray(); + $poll->save(); + + return 200; + } + + public function storeReport(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + + $this->validate($request, [ + 'type' => 'required|alpha_dash', + 'id' => 'required|integer|min:1', + ]); + + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $pid = $request->user()->profile_id; $sid = $request->input('id'); $type = $request->input('type'); @@ -344,28 +349,28 @@ class StoryComposeController extends Controller 'copyright', 'impersonation', 'scam', - 'terrorism' + 'terrorism', ]; - abort_if(!in_array($type, $types), 422, 'Invalid story report type'); + abort_if(! in_array($type, $types), 422, 'Invalid story report type'); $story = Story::findOrFail($sid); abort_if($story->profile_id == $pid, 422, 'Cannot report your own story'); - abort_if(!FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow'); + abort_if(! FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow'); - if( Report::whereProfileId($pid) - ->whereObjectType('App\Story') - ->whereObjectId($story->id) - ->exists() + if (Report::whereProfileId($pid) + ->whereObjectType('App\Story') + ->whereObjectId($story->id) + ->exists() ) { - return response()->json(['error' => [ - 'code' => 409, - 'message' => 'Cannot report the same story again' - ]], 409); + return response()->json(['error' => [ + 'code' => 409, + 'message' => 'Cannot report the same story again', + ]], 409); } - $report = new Report; + $report = new Report; $report->profile_id = $pid; $report->user_id = $request->user()->id; $report->object_id = $story->id; @@ -376,149 +381,149 @@ class StoryComposeController extends Controller $report->save(); return [200]; - } + } - public function react(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'sid' => 'required', - 'reaction' => 'required|string' - ]); - $pid = $request->user()->profile_id; - $text = $request->input('reaction'); + public function react(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + $this->validate($request, [ + 'sid' => 'required', + 'reaction' => 'required|string', + ]); + $pid = $request->user()->profile_id; + $text = $request->input('reaction'); + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $story = Story::findOrFail($request->input('sid')); - $story = Story::findOrFail($request->input('sid')); + abort_if(! $story->can_react, 422); + abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story'); - abort_if(!$story->can_react, 422); - abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story'); + $status = new Status; + $status->profile_id = $pid; + $status->type = 'story:reaction'; + $status->caption = $text; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id, + 'reaction' => $text, + ]); + $status->save(); - $status = new Status; - $status->profile_id = $pid; - $status->type = 'story:reaction'; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id, - 'reaction' => $text - ]); - $status->save(); + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $pid; + $dm->type = 'story:react'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $story->profile->username, + 'story_actor_username' => $request->user()->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'reaction' => $text, + ]); + $dm->save(); - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $pid; - $dm->type = 'story:react'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $story->profile->username, - 'story_actor_username' => $request->user()->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'reaction' => $text - ]); - $dm->save(); + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $pid, + ], + [ + 'type' => 'story:react', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false, + ] + ); - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $pid - ], - [ - 'type' => 'story:react', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); + if ($story->local) { + // generate notification + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:react'; + $n->save(); + } else { + StoryReactionDeliver::dispatch($story, $status)->onQueue('story'); + } - if($story->local) { - // generate notification - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:react'; - $n->save(); - } else { - StoryReactionDeliver::dispatch($story, $status)->onQueue('story'); - } + StoryService::reactIncrement($story->id, $pid); - StoryService::reactIncrement($story->id, $pid); + return 200; + } - return 200; - } + public function comment(Request $request) + { + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); + $this->validate($request, [ + 'sid' => 'required', + 'caption' => 'required|string', + ]); + $pid = $request->user()->profile_id; + $text = $request->input('caption'); + $user = $request->user(); + abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $story = Story::findOrFail($request->input('sid')); - public function comment(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'sid' => 'required', - 'caption' => 'required|string' - ]); - $pid = $request->user()->profile_id; - $text = $request->input('caption'); + abort_if(! $story->can_reply, 422); - $story = Story::findOrFail($request->input('sid')); + $status = new Status; + $status->type = 'story:reply'; + $status->profile_id = $pid; + $status->caption = $text; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id, + ]); + $status->save(); - abort_if(!$story->can_reply, 422); + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $pid; + $dm->type = 'story:comment'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $story->profile->username, + 'story_actor_username' => $request->user()->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'caption' => $text, + ]); + $dm->save(); - $status = new Status; - $status->type = 'story:reply'; - $status->profile_id = $pid; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id - ]); - $status->save(); + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $pid, + ], + [ + 'type' => 'story:comment', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false, + ] + ); - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $pid; - $dm->type = 'story:comment'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $story->profile->username, - 'story_actor_username' => $request->user()->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'caption' => $text - ]); - $dm->save(); + if ($story->local) { + // generate notification + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:comment'; + $n->save(); + } else { + StoryReplyDeliver::dispatch($story, $status)->onQueue('story'); + } - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $pid - ], - [ - 'type' => 'story:comment', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); - - if($story->local) { - // generate notification - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:comment'; - $n->save(); - } else { - StoryReplyDeliver::dispatch($story, $status)->onQueue('story'); - } - - return 200; - } + return 200; + } } diff --git a/app/Http/Controllers/StoryController.php b/app/Http/Controllers/StoryController.php index 5a9fb5530..fede7c6d9 100644 --- a/app/Http/Controllers/StoryController.php +++ b/app/Http/Controllers/StoryController.php @@ -28,288 +28,308 @@ use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Resource\Item; use App\Transformer\ActivityPub\Verb\StoryVerb; use App\Jobs\StoryPipeline\StoryViewDeliver; +use App\Services\UserRoleService; class StoryController extends StoryComposeController { - public function recent(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $pid = $request->user()->profile_id; + public function recent(Request $request) + { + abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return []; + } + $pid = $user->profile_id; - if(config('database.default') == 'pgsql') { - $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { - return Story::select('stories.*', 'followers.following_id') - ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') - ->where('followers.profile_id', $pid) - ->where('stories.active', true) - ->get() - ->map(function($s) { - $r = new \StdClass; - $r->id = $s->id; - $r->profile_id = $s->profile_id; - $r->type = $s->type; - $r->path = $s->path; - return $r; - }) - ->unique('profile_id'); - }); + if(config('database.default') == 'pgsql') { + $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->get() + ->map(function($s) { + $r = new \StdClass; + $r->id = $s->id; + $r->profile_id = $s->profile_id; + $r->type = $s->type; + $r->path = $s->path; + return $r; + }) + ->unique('profile_id'); + }); - } else { - $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { - return Story::select('stories.*', 'followers.following_id') - ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') - ->where('followers.profile_id', $pid) - ->where('stories.active', true) - ->groupBy('followers.following_id') - ->orderByDesc('id') - ->get(); - }); - } + } else { + $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->groupBy('followers.following_id') + ->orderByDesc('id') + ->get(); + }); + } - $self = Cache::remember('pf:stories:recent-self:' . $pid, 21600, function() use($pid) { - return Story::whereProfileId($pid) - ->whereActive(true) - ->orderByDesc('id') - ->limit(1) - ->get() - ->map(function($s) use($pid) { - $r = new \StdClass; - $r->id = $s->id; - $r->profile_id = $pid; - $r->type = $s->type; - $r->path = $s->path; - return $r; - }); - }); + $self = Cache::remember('pf:stories:recent-self:' . $pid, 21600, function() use($pid) { + return Story::whereProfileId($pid) + ->whereActive(true) + ->orderByDesc('id') + ->limit(1) + ->get() + ->map(function($s) use($pid) { + $r = new \StdClass; + $r->id = $s->id; + $r->profile_id = $pid; + $r->type = $s->type; + $r->path = $s->path; + return $r; + }); + }); - if($self->count()) { - $s->prepend($self->first()); - } + if($self->count()) { + $s->prepend($self->first()); + } - $res = $s->map(function($s) use($pid) { - $profile = AccountService::get($s->profile_id); - $url = $profile['local'] ? url("/stories/{$profile['username']}") : - url("/i/rs/{$profile['id']}"); - return [ - 'pid' => $profile['id'], - 'avatar' => $profile['avatar'], - 'local' => $profile['local'], - 'username' => $profile['acct'], - 'latest' => [ - 'id' => $s->id, - 'type' => $s->type, - 'preview_url' => url(Storage::url($s->path)) - ], - 'url' => $url, - 'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)), - 'sid' => $s->id - ]; - }) - ->sortBy('seen') - ->values(); - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + $res = $s->map(function($s) use($pid) { + $profile = AccountService::get($s->profile_id); + $url = $profile['local'] ? url("/stories/{$profile['username']}") : + url("/i/rs/{$profile['id']}"); + return [ + 'pid' => $profile['id'], + 'avatar' => $profile['avatar'], + 'local' => $profile['local'], + 'username' => $profile['acct'], + 'latest' => [ + 'id' => $s->id, + 'type' => $s->type, + 'preview_url' => url(Storage::url($s->path)) + ], + 'url' => $url, + 'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)), + 'sid' => $s->id + ]; + }) + ->sortBy('seen') + ->values(); + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - public function profile(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function profile(Request $request, $id) + { + abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404); - $authed = $request->user()->profile_id; - $profile = Profile::findOrFail($id); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return []; + } + $authed = $user->profile_id; + $profile = Profile::findOrFail($id); - if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) { - return abort([], 403); - } + if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) { + return abort([], 403); + } - $stories = Story::whereProfileId($profile->id) - ->whereActive(true) - ->orderBy('expires_at') - ->get() - ->map(function($s, $k) use($authed) { - $seen = StoryService::hasSeen($authed, $s->id); - $res = [ - 'id' => (string) $s->id, - 'type' => $s->type, - 'duration' => $s->duration, - 'src' => url(Storage::url($s->path)), - 'created_at' => $s->created_at->toAtomString(), - 'expires_at' => $s->expires_at->toAtomString(), - 'view_count' => ($authed == $s->profile_id) ? ($s->view_count ?? 0) : null, - 'seen' => $seen, - 'progress' => $seen ? 100 : 0, - 'can_reply' => (bool) $s->can_reply, - 'can_react' => (bool) $s->can_react - ]; + $stories = Story::whereProfileId($profile->id) + ->whereActive(true) + ->orderBy('expires_at') + ->get() + ->map(function($s, $k) use($authed) { + $seen = StoryService::hasSeen($authed, $s->id); + $res = [ + 'id' => (string) $s->id, + 'type' => $s->type, + 'duration' => $s->duration, + 'src' => url(Storage::url($s->path)), + 'created_at' => $s->created_at->toAtomString(), + 'expires_at' => $s->expires_at->toAtomString(), + 'view_count' => ($authed == $s->profile_id) ? ($s->view_count ?? 0) : null, + 'seen' => $seen, + 'progress' => $seen ? 100 : 0, + 'can_reply' => (bool) $s->can_reply, + 'can_react' => (bool) $s->can_react + ]; - if($s->type == 'poll') { - $res['question'] = json_decode($s->story, true)['question']; - $res['options'] = json_decode($s->story, true)['options']; - $res['voted'] = PollService::votedStory($s->id, $authed); - if($res['voted']) { - $res['voted_index'] = PollService::storyChoice($s->id, $authed); - } - } + if($s->type == 'poll') { + $res['question'] = json_decode($s->story, true)['question']; + $res['options'] = json_decode($s->story, true)['options']; + $res['voted'] = PollService::votedStory($s->id, $authed); + if($res['voted']) { + $res['voted_index'] = PollService::storyChoice($s->id, $authed); + } + } - return $res; - })->toArray(); - if(count($stories) == 0) { - return []; - } - $cursor = count($stories) - 1; - $stories = [[ - 'id' => (string) $stories[$cursor]['id'], - 'nodes' => $stories, - 'account' => AccountService::get($profile->id), - 'pid' => (string) $profile->id - ]]; - return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + return $res; + })->toArray(); + if(count($stories) == 0) { + return []; + } + $cursor = count($stories) - 1; + $stories = [[ + 'id' => (string) $stories[$cursor]['id'], + 'nodes' => $stories, + 'account' => AccountService::get($profile->id), + 'pid' => (string) $profile->id + ]]; + return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - public function viewed(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function viewed(Request $request) + { + abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'id' => 'required|min:1', - ]); - $id = $request->input('id'); + $this->validate($request, [ + 'id' => 'required|min:1', + ]); + $id = $request->input('id'); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return []; + } + $authed = $user->profile; - $authed = $request->user()->profile; + $story = Story::with('profile') + ->findOrFail($id); + $exp = $story->expires_at; - $story = Story::with('profile') - ->findOrFail($id); - $exp = $story->expires_at; + $profile = $story->profile; - $profile = $story->profile; + if($story->profile_id == $authed->id) { + return []; + } - if($story->profile_id == $authed->id) { - return []; - } + $publicOnly = (bool) $profile->followedBy($authed); + abort_if(!$publicOnly, 403); - $publicOnly = (bool) $profile->followedBy($authed); - abort_if(!$publicOnly, 403); + $v = StoryView::firstOrCreate([ + 'story_id' => $id, + 'profile_id' => $authed->id + ]); - $v = StoryView::firstOrCreate([ - 'story_id' => $id, - 'profile_id' => $authed->id - ]); + if($v->wasRecentlyCreated) { + Story::findOrFail($story->id)->increment('view_count'); - if($v->wasRecentlyCreated) { - Story::findOrFail($story->id)->increment('view_count'); + if($story->local == false) { + StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); + } + } - if($story->local == false) { - StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); - } - } + Cache::forget('stories:recent:by_id:' . $authed->id); + StoryService::addSeen($authed->id, $story->id); + return ['code' => 200]; + } - Cache::forget('stories:recent:by_id:' . $authed->id); - StoryService::addSeen($authed->id, $story->id); - return ['code' => 200]; - } + public function exists(Request $request, $id) + { + abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return response()->json(false); + } + return response()->json(Story::whereProfileId($id) + ->whereActive(true) + ->exists()); + } - public function exists(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function iRedirect(Request $request) + { + abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404); - return response()->json(Story::whereProfileId($id) - ->whereActive(true) - ->exists()); - } + $user = $request->user(); + abort_if(!$user, 404); + $username = $user->username; + return redirect("/stories/{$username}"); + } - public function iRedirect(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function viewers(Request $request) + { + abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404); - $user = $request->user(); - abort_if(!$user, 404); - $username = $user->username; - return redirect("/stories/{$username}"); - } + $this->validate($request, [ + 'sid' => 'required|string' + ]); - public function viewers(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return response()->json([]); + } - $this->validate($request, [ - 'sid' => 'required|string' - ]); + $pid = $request->user()->profile_id; + $sid = $request->input('sid'); - $pid = $request->user()->profile_id; - $sid = $request->input('sid'); + $story = Story::whereProfileId($pid) + ->whereActive(true) + ->findOrFail($sid); - $story = Story::whereProfileId($pid) - ->whereActive(true) - ->findOrFail($sid); + $viewers = StoryView::whereStoryId($story->id) + ->latest() + ->simplePaginate(10) + ->map(function($view) { + return AccountService::get($view->profile_id); + }) + ->values(); - $viewers = StoryView::whereStoryId($story->id) - ->latest() - ->simplePaginate(10) - ->map(function($view) { - return AccountService::get($view->profile_id); - }) - ->values(); + return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + public function remoteStory(Request $request, $id) + { + abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404); - public function remoteStory(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $profile = Profile::findOrFail($id); + if($profile->user_id != null || $profile->domain == null) { + return redirect('/stories/' . $profile->username); + } + $pid = $profile->id; + return view('stories.show_remote', compact('pid')); + } - $profile = Profile::findOrFail($id); - if($profile->user_id != null || $profile->domain == null) { - return redirect('/stories/' . $profile->username); - } - $pid = $profile->id; - return view('stories.show_remote', compact('pid')); - } + public function pollResults(Request $request) + { + abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404); - public function pollResults(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'sid' => 'required|string' + ]); - $this->validate($request, [ - 'sid' => 'required|string' - ]); + $pid = $request->user()->profile_id; + $sid = $request->input('sid'); - $pid = $request->user()->profile_id; - $sid = $request->input('sid'); + $story = Story::whereProfileId($pid) + ->whereActive(true) + ->findOrFail($sid); - $story = Story::whereProfileId($pid) - ->whereActive(true) - ->findOrFail($sid); + return PollService::storyResults($sid); + } - return PollService::storyResults($sid); - } + public function getActivityObject(Request $request, $username, $id) + { + abort_if(!(bool) config_cache('instance.stories.enabled'), 404); - public function getActivityObject(Request $request, $username, $id) - { - abort_if(!config_cache('instance.stories.enabled'), 404); + if(!$request->wantsJson()) { + return redirect('/stories/' . $username); + } - if(!$request->wantsJson()) { - return redirect('/stories/' . $username); - } + abort_if(!$request->hasHeader('Authorization'), 404); - abort_if(!$request->hasHeader('Authorization'), 404); + $profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail(); + $story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id); - $profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail(); - $story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id); + abort_if($story->bearcap_token == null, 404); + abort_if(now()->gt($story->expires_at), 404); + $token = substr($request->header('Authorization'), 7); + abort_if(hash_equals($story->bearcap_token, $token) === false, 404); + abort_if($story->created_at->lt(now()->subMinutes(20)), 404); - abort_if($story->bearcap_token == null, 404); - abort_if(now()->gt($story->expires_at), 404); - $token = substr($request->header('Authorization'), 7); - abort_if(hash_equals($story->bearcap_token, $token) === false, 404); - abort_if($story->created_at->lt(now()->subMinutes(20)), 404); + $fractal = new Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Item($story, new StoryVerb()); + $res = $fractal->createData($resource)->toArray(); + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - $fractal = new Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Item($story, new StoryVerb()); - $res = $fractal->createData($resource)->toArray(); - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - - public function showSystemStory() - { - // return view('stories.system'); - } + public function showSystemStory() + { + // return view('stories.system'); + } } diff --git a/app/Http/Controllers/UserEmailForgotController.php b/app/Http/Controllers/UserEmailForgotController.php new file mode 100644 index 000000000..3889b9802 --- /dev/null +++ b/app/Http/Controllers/UserEmailForgotController.php @@ -0,0 +1,131 @@ +middleware('guest'); + abort_unless(config('security.forgot-email.enabled'), 404); + } + + public function index(Request $request) + { + abort_if($request->user(), 404); + return view('auth.email.forgot'); + } + + public function store(Request $request) + { + $rules = [ + 'username' => 'required|min:2|max:15|exists:users' + ]; + + $messages = [ + 'username.exists' => 'This username is no longer active or does not exist!' + ]; + + if((bool) config_cache('captcha.enabled')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'You need to complete the captcha!'; + } + + $randomDelay = random_int(500000, 2000000); + usleep($randomDelay); + + $this->validate($request, $rules, $messages); + $check = self::checkLimits(); + + if(!$check) { + return redirect()->back()->withErrors([ + 'username' => 'Please try again later, we\'ve reached our quota and cannot process any more requests at this time.' + ]); + } + + $user = User::whereUsername($request->input('username')) + ->whereNotNull('email_verified_at') + ->whereNull('status') + ->whereIsAdmin(false) + ->first(); + + if(!$user) { + return redirect()->back()->withErrors([ + 'username' => 'Invalid username or account. It may not exist, or does not have a verified email, is an admin account or is disabled.' + ]); + } + + $exists = UserEmailForgot::whereUserId($user->id) + ->where('email_sent_at', '>', now()->subHours(24)) + ->count(); + + if($exists) { + return redirect()->back()->withErrors([ + 'username' => 'An email reminder was recently sent to this account, please try again after 24 hours!' + ]); + } + + return $this->storeHandle($request, $user); + } + + protected function storeHandle($request, $user) + { + UserEmailForgot::create([ + 'user_id' => $user->id, + 'ip_address' => $request->ip(), + 'user_agent' => $request->userAgent(), + 'email_sent_at' => now() + ]); + + Mail::to($user->email)->send(new UserEmailForgotReminder($user)); + self::getLimits(true); + return redirect()->back()->with(['status' => 'Successfully sent an email reminder!']); + } + + public static function checkLimits() + { + $limits = self::getLimits(); + + if( + $limits['current']['hourly'] >= $limits['max']['hourly'] || + $limits['current']['daily'] >= $limits['max']['daily'] || + $limits['current']['weekly'] >= $limits['max']['weekly'] || + $limits['current']['monthly'] >= $limits['max']['monthly'] + ) { + return false; + } + + return true; + } + + public static function getLimits($forget = false) + { + return [ + 'max' => config('security.forgot-email.limits.max'), + 'current' => [ + 'hourly' => self::activeCount(60, $forget), + 'daily' => self::activeCount(1440, $forget), + 'weekly' => self::activeCount(10080, $forget), + 'monthly' => self::activeCount(43800, $forget) + ] + ]; + } + + public static function activeCount($mins, $forget = false) + { + if($forget) { + Cache::forget('pf:auth:forgot-email:active-count:dur-' . $mins); + } + return Cache::remember('pf:auth:forgot-email:active-count:dur-' . $mins, 14200, function() use($mins) { + return UserEmailForgot::where('email_sent_at', '>', now()->subMinutes($mins))->count(); + }); + } +} diff --git a/app/Http/Controllers/UserRolesController.php b/app/Http/Controllers/UserRolesController.php new file mode 100644 index 000000000..65a71d19d --- /dev/null +++ b/app/Http/Controllers/UserRolesController.php @@ -0,0 +1,23 @@ +middleware('auth'); + } + + public function getRoles(Request $request) + { + $this->validate($request, [ + 'id' => 'required' + ]); + + return UserRoleService::getRoles($request->user()->id); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 5cc99014b..4ec8832e8 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -14,12 +14,12 @@ class Kernel extends HttpKernel * @var array */ protected $middleware = [ + \Illuminate\Http\Middleware\HandleCors::class, \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, + \App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, - \App\Http\Middleware\TrustProxies::class, - \Illuminate\Http\Middleware\HandleCors::class, ]; /** @@ -54,6 +54,7 @@ class Kernel extends HttpKernel * @var array */ protected $routeMiddleware = [ + 'api.admin' => \App\Http\Middleware\Api\Admin::class, 'admin' => \App\Http\Middleware\Admin::class, 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, @@ -68,6 +69,8 @@ class Kernel extends HttpKernel 'twofactor' => \App\Http\Middleware\TwoFactorAuth::class, 'validemail' => \App\Http\Middleware\EmailVerificationCheck::class, 'interstitial' => \App\Http\Middleware\AccountInterstitial::class, + 'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class, + 'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class, // 'restricted' => \App\Http\Middleware\RestrictedAccess::class, ]; } diff --git a/app/Http/Middleware/Api/Admin.php b/app/Http/Middleware/Api/Admin.php new file mode 100644 index 000000000..65d24758d --- /dev/null +++ b/app/Http/Middleware/Api/Admin.php @@ -0,0 +1,26 @@ +is_admin == false) { + return abort(403, "You must be an administrator to do that"); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/ProfileMigrationStoreRequest.php b/app/Http/Requests/ProfileMigrationStoreRequest.php new file mode 100644 index 000000000..9b071511a --- /dev/null +++ b/app/Http/Requests/ProfileMigrationStoreRequest.php @@ -0,0 +1,80 @@ +user() || $this->user()->status) { + return false; + } + + return true; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'acct' => 'required|email', + 'password' => 'required|current_password', + ]; + } + + public function after(): array + { + return [ + function (Validator $validator) { + $err = $this->validateNewAccount(); + if ($err !== 'noerr') { + $validator->errors()->add( + 'acct', + $err + ); + } + }, + ]; + } + + protected function validateNewAccount() + { + if (ProfileMigration::whereProfileId($this->user()->profile_id)->where('created_at', '>', now()->subDays(30))->exists()) { + return 'Error - You have migrated your account in the past 30 days, you can only perform a migration once per 30 days.'; + } + $acct = WebfingerService::rawGet($this->acct); + if (! $acct) { + return 'The new account you provided is not responding to our requests.'; + } + $pr = FetchCacheService::getJson($acct); + if (! $pr || ! isset($pr['alsoKnownAs'])) { + return 'Invalid account lookup response.'; + } + if (! count($pr['alsoKnownAs']) || ! is_array($pr['alsoKnownAs'])) { + return 'The new account does not contain an alias to your current account.'; + } + $curAcctUrl = $this->user()->profile->permalink(); + if (! in_array($curAcctUrl, $pr['alsoKnownAs'])) { + return 'The new account does not contain an alias to your current account.'; + } + + return 'noerr'; + } +} diff --git a/app/Http/Requests/Status/StoreStatusEditRequest.php b/app/Http/Requests/Status/StoreStatusEditRequest.php index aa9364ca6..e8e2d22f5 100644 --- a/app/Http/Requests/Status/StoreStatusEditRequest.php +++ b/app/Http/Requests/Status/StoreStatusEditRequest.php @@ -2,10 +2,10 @@ namespace App\Http\Requests\Status; -use Illuminate\Foundation\Http\FormRequest; use App\Media; use App\Status; use Closure; +use Illuminate\Foundation\Http\FormRequest; class StoreStatusEditRequest extends FormRequest { @@ -14,24 +14,25 @@ class StoreStatusEditRequest extends FormRequest */ public function authorize(): bool { - $profile = $this->user()->profile; - if($profile->status != null) { - return false; - } - if($profile->unlisted == true && $profile->cw == true) { - return false; - } - $types = [ - "photo", - "photo:album", - "photo:video:album", - "reply", - "text", - "video", - "video:album" - ]; - $scopes = ['public', 'unlisted', 'private']; - $status = Status::whereNull('reblog_of_id')->whereIn('type', $types)->whereIn('scope', $scopes)->find($this->route('id')); + $profile = $this->user()->profile; + if ($profile->status != null) { + return false; + } + if ($profile->unlisted == true && $profile->cw == true) { + return false; + } + $types = [ + 'photo', + 'photo:album', + 'photo:video:album', + 'reply', + 'text', + 'video', + 'video:album', + ]; + $scopes = ['public', 'unlisted', 'private']; + $status = Status::whereNull('reblog_of_id')->whereIn('type', $types)->whereIn('scope', $scopes)->find($this->route('id')); + return $status && $this->user()->profile_id === $status->profile_id; } @@ -47,18 +48,18 @@ class StoreStatusEditRequest extends FormRequest 'spoiler_text' => 'nullable|string|max:140', 'sensitive' => 'sometimes|boolean', 'media_ids' => [ - 'nullable', - 'required_without:status', - 'array', - 'max:' . config('pixelfed.max_album_length'), - function (string $attribute, mixed $value, Closure $fail) { - Media::whereProfileId($this->user()->profile_id) - ->where(function($query) { - return $query->whereNull('status_id') - ->orWhere('status_id', '=', $this->route('id')); - }) - ->findOrFail($value); - }, + 'nullable', + 'required_without:status', + 'array', + 'max:'.(int) config_cache('pixelfed.max_album_length'), + function (string $attribute, mixed $value, Closure $fail) { + Media::whereProfileId($this->user()->profile_id) + ->where(function ($query) { + return $query->whereNull('status_id') + ->orWhere('status_id', '=', $this->route('id')); + }) + ->findOrFail($value); + }, ], 'location' => 'sometimes|nullable', 'location.id' => 'sometimes|integer|min:1|max:128769', diff --git a/app/Http/Resources/Admin/AdminModeratedProfileResource.php b/app/Http/Resources/Admin/AdminModeratedProfileResource.php new file mode 100644 index 000000000..d0ce5a617 --- /dev/null +++ b/app/Http/Resources/Admin/AdminModeratedProfileResource.php @@ -0,0 +1,45 @@ + + */ + public function toArray(Request $request): array + { + $profileObj = []; + $profile = Profile::withTrashed()->find($this->profile_id); + if ($profile) { + $profileObj = [ + 'name' => $profile->name, + 'username' => $profile->username, + 'username_str' => explode('@', $profile->username)[1], + 'remote_url' => $profile->remote_url, + ]; + } + + return [ + 'id' => $this->id, + 'domain' => $this->domain, + 'profile' => $profileObj, + 'profile_id' => $this->profile_id, + 'profile_url' => $this->profile_url, + 'note' => $this->note, + 'is_banned' => (bool) $this->is_banned, + 'is_nsfw' => (bool) $this->is_nsfw, + 'is_unlisted' => (bool) $this->is_unlisted, + 'is_noautolink' => (bool) $this->is_noautolink, + 'is_nodms' => (bool) $this->is_nodms, + 'is_notrending' => (bool) $this->is_notrending, + 'created_at' => now()->parse($this->created_at)->format('c'), + ]; + } +} diff --git a/app/Http/Resources/AdminRemoteReport.php b/app/Http/Resources/AdminRemoteReport.php new file mode 100644 index 000000000..a726e25cc --- /dev/null +++ b/app/Http/Resources/AdminRemoteReport.php @@ -0,0 +1,49 @@ + + */ + public function toArray(Request $request): array + { + $instance = parse_url($this->uri, PHP_URL_HOST); + $statuses = []; + if($this->status_ids && count($this->status_ids)) { + foreach($this->status_ids as $sid) { + $s = StatusService::get($sid, false); + if($s && $s['in_reply_to_id'] != null) { + $parent = StatusService::get($s['in_reply_to_id'], false); + if($parent) { + $s['parent'] = $parent; + } + } + if($s) { + $statuses[] = $s; + } + } + } + $res = [ + 'id' => $this->id, + 'instance' => $instance, + 'reported' => AccountService::get($this->account_id, true), + 'status_ids' => $this->status_ids, + 'statuses' => $statuses, + 'message' => $this->comment, + 'report_meta' => $this->report_meta, + 'created_at' => optional($this->created_at)->format('c'), + 'action_taken_at' => optional($this->action_taken_at)->format('c'), + ]; + return $res; + } +} diff --git a/app/Http/Resources/AdminReport.php b/app/Http/Resources/AdminReport.php index c541e58cd..9b9681bcc 100644 --- a/app/Http/Resources/AdminReport.php +++ b/app/Http/Resources/AdminReport.php @@ -2,10 +2,11 @@ namespace App\Http\Resources; -use Illuminate\Http\Request; -use Illuminate\Http\Resources\Json\JsonResource; use App\Services\AccountService; use App\Services\StatusService; +use App\Story; +use Illuminate\Http\Request; +use Illuminate\Http\Resources\Json\JsonResource; class AdminReport extends JsonResource { @@ -16,22 +17,29 @@ class AdminReport extends JsonResource */ public function toArray(Request $request): array { - $res = [ - 'id' => $this->id, - 'reporter' => AccountService::get($this->profile_id, true), - 'type' => $this->type, - 'object_id' => (string) $this->object_id, - 'object_type' => $this->object_type, - 'reported' => AccountService::get($this->reported_profile_id, true), - 'status' => null, - 'reporter_message' => $this->message, - 'admin_seen_at' => $this->admin_seen, - 'created_at' => $this->created_at, - ]; + $res = [ + 'id' => $this->id, + 'reporter' => AccountService::get($this->profile_id, true), + 'type' => $this->type, + 'object_id' => (string) $this->object_id, + 'object_type' => $this->object_type, + 'reported' => AccountService::get($this->reported_profile_id, true), + 'status' => null, + 'reporter_message' => $this->message, + 'admin_seen_at' => $this->admin_seen, + 'created_at' => $this->created_at, + ]; - if($this->object_id && $this->object_type === 'App\Status') { - $res['status'] = StatusService::get($this->object_id, false); - } + if ($this->object_id && $this->object_type === 'App\Status') { + $res['status'] = StatusService::get($this->object_id, false); + } + + if ($this->object_id && $this->object_type === 'App\Story') { + $story = Story::find($this->object_id); + if ($story) { + $res['story'] = $story->toAdminEntity(); + } + } return $res; } diff --git a/app/Http/Resources/AdminUser.php b/app/Http/Resources/AdminUser.php index 75bac9f62..390c5c00e 100644 --- a/app/Http/Resources/AdminUser.php +++ b/app/Http/Resources/AdminUser.php @@ -2,8 +2,8 @@ namespace App\Http\Resources; -use Illuminate\Http\Resources\Json\JsonResource; use App\Services\AccountService; +use Illuminate\Http\Resources\Json\JsonResource; class AdminUser extends JsonResource { @@ -18,8 +18,8 @@ class AdminUser extends JsonResource $account = AccountService::get($this->profile_id, true); $res = [ - 'id' => $this->id, - 'profile_id' => $this->profile_id, + 'id' => (string) $this->id, + 'profile_id' => (string) $this->profile_id, 'name' => $this->name, 'username' => $this->username, 'is_admin' => (bool) $this->is_admin, @@ -28,17 +28,18 @@ class AdminUser extends JsonResource 'two_factor_enabled' => (bool) $this->{'2fa_enabled'}, 'register_source' => $this->register_source, 'app_register_ip' => $this->app_register_ip, + 'has_interstitial' => (bool) $this->has_interstitial, 'last_active_at' => $this->last_active_at, 'created_at' => $this->created_at, ]; - if($account) { + if ($account) { $res['avatar'] = $account['avatar']; $res['bio'] = $account['note_text']; - $res['statuses_count'] = $account['statuses_count']; - $res['following_count'] = $account['following_count']; - $res['followers_count'] = $account['followers_count']; - $res['is_private'] = $account['locked']; + $res['statuses_count'] = (int) $account['statuses_count']; + $res['following_count'] = (int) $account['following_count']; + $res['followers_count'] = (int) $account['followers_count']; + $res['is_private'] = (bool) $account['locked']; } return $res; diff --git a/app/Http/Resources/MastoApi/Admin/DomainBlockResource.php b/app/Http/Resources/MastoApi/Admin/DomainBlockResource.php new file mode 100644 index 000000000..eeb3ddc09 --- /dev/null +++ b/app/Http/Resources/MastoApi/Admin/DomainBlockResource.php @@ -0,0 +1,42 @@ + + */ + public function toArray(Request $request): array + { + $severity = 'noop'; + if ($this->banned) { + $severity = 'suspend'; + } else if ($this->unlisted) { + $severity = 'silence'; + } + + return [ + 'id' => $this->id, + 'domain' => $this->domain, + // This property is coming in Mastodon 4.3, although it'll only be + // useful if Pixelfed supports obfuscating domains: + 'digest' => hash('sha256', $this->domain), + 'severity' => $severity, + // Using the updated_at value as this is going to be the closest to + // when the domain was banned + 'created_at' => $this->updated_at, + // We don't have data for these fields + 'reject_media' => false, + 'reject_reports' => false, + 'private_comment' => $this->notes ? join('; ', $this->notes) : null, + 'public_comment' => $this->limit_reason, + 'obfuscate' => false + ]; + } +} diff --git a/app/Http/Resources/StatusStateless.php b/app/Http/Resources/StatusStateless.php index df451cc53..0a7bbe8d4 100644 --- a/app/Http/Resources/StatusStateless.php +++ b/app/Http/Resources/StatusStateless.php @@ -2,18 +2,17 @@ namespace App\Http\Resources; -use Illuminate\Http\Resources\Json\JsonResource; -use Cache; +use App\Models\CustomEmoji; use App\Services\AccountService; use App\Services\HashidService; use App\Services\LikeService; use App\Services\MediaService; use App\Services\MediaTagService; -use App\Services\StatusHashtagService; -use App\Services\StatusLabelService; -use App\Services\StatusMentionService; use App\Services\PollService; -use App\Models\CustomEmoji; +use App\Services\StatusHashtagService; +use App\Services\StatusMentionService; +use App\Util\Lexer\Autolink; +use Illuminate\Http\Resources\Json\JsonResource; class StatusStateless extends JsonResource { @@ -28,49 +27,50 @@ class StatusStateless extends JsonResource $status = $this; $taggedPeople = MediaTagService::get($status->id); $poll = $status->type === 'poll' ? PollService::get($status->id) : null; + $autoLink = $status->caption ? Autolink::create()->autolink($status->caption) : null; return [ - '_v' => 1, - 'id' => (string) $status->id, + '_v' => 1, + 'id' => (string) $status->id, //'gid' => $status->group_id ? (string) $status->group_id : null, - 'shortcode' => HashidService::encode($status->id), - 'uri' => $status->url(), - 'url' => $status->url(), - 'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null, - 'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null, - 'reblog' => null, - 'content' => $status->rendered ?? $status->caption, - 'content_text' => $status->caption, - 'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)), - 'emojis' => CustomEmoji::scan($status->caption), - 'reblogs_count' => $status->reblogs_count ?? 0, - 'favourites_count' => $status->likes_count ?? 0, - 'reblogged' => null, - 'favourited' => null, - 'muted' => null, - 'sensitive' => (bool) $status->is_nsfw, - 'spoiler_text' => $status->cw_summary ?? '', - 'visibility' => $status->scope ?? $status->visibility, - 'application' => [ - 'name' => 'web', - 'website' => null - ], - 'language' => null, - 'mentions' => StatusMentionService::get($status->id), - 'pf_type' => $status->type ?? $status->setType(), - 'reply_count' => (int) $status->reply_count, - 'comments_disabled' => (bool) $status->comments_disabled, - 'thread' => false, - 'replies' => [], - 'parent' => [], - 'place' => $status->place, - 'local' => (bool) $status->local, - 'taggedPeople' => $taggedPeople, - 'liked_by' => LikeService::likedBy($status), - 'media_attachments' => MediaService::get($status->id), - 'account' => AccountService::get($status->profile_id, true), - 'tags' => StatusHashtagService::statusTags($status->id), - 'poll' => $poll + 'shortcode' => HashidService::encode($status->id), + 'uri' => $status->url(), + 'url' => $status->url(), + 'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null, + 'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null, + 'reblog' => null, + 'content' => $autoLink, + 'content_text' => $status->caption, + 'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)), + 'emojis' => CustomEmoji::scan($status->caption), + 'reblogs_count' => $status->reblogs_count ?? 0, + 'favourites_count' => $status->likes_count ?? 0, + 'reblogged' => null, + 'favourited' => null, + 'muted' => null, + 'sensitive' => (bool) $status->is_nsfw, + 'spoiler_text' => $status->cw_summary ?? '', + 'visibility' => $status->scope ?? $status->visibility, + 'application' => [ + 'name' => 'web', + 'website' => null, + ], + 'language' => null, + 'mentions' => StatusMentionService::get($status->id), + 'pf_type' => $status->type ?? $status->setType(), + 'reply_count' => (int) $status->reply_count, + 'comments_disabled' => (bool) $status->comments_disabled, + 'thread' => false, + 'replies' => [], + 'parent' => [], + 'place' => $status->place, + 'local' => (bool) $status->local, + 'taggedPeople' => $taggedPeople, + 'liked_by' => LikeService::likedBy($status), + 'media_attachments' => MediaService::get($status->id), + 'account' => AccountService::get($status->profile_id, true), + 'tags' => StatusHashtagService::statusTags($status->id), + 'poll' => $poll, ]; } } diff --git a/app/Instance.php b/app/Instance.php index 6a7b8e6f2..a93d9e95e 100644 --- a/app/Instance.php +++ b/app/Instance.php @@ -6,63 +6,84 @@ use Illuminate\Database\Eloquent\Model; class Instance extends Model { - protected $fillable = ['domain', 'banned', 'auto_cw', 'unlisted', 'notes']; + protected $casts = [ + 'last_crawled_at' => 'datetime', + 'actors_last_synced_at' => 'datetime', + 'notes' => 'array', + 'nodeinfo_last_fetched' => 'datetime', + 'delivery_next_after' => 'datetime', + ]; - public function profiles() - { - return $this->hasMany(Profile::class, 'domain', 'domain'); - } + protected $fillable = [ + 'domain', + 'banned', + 'auto_cw', + 'unlisted', + 'notes' + ]; - public function statuses() - { - return $this->hasManyThrough( - Status::class, - Profile::class, - 'domain', - 'profile_id', - 'domain', - 'id' - ); - } + // To get all moderated instances, we need to search where (banned OR unlisted) + public function scopeModerated($query): void { + $query->where(function ($query) { + $query->where('banned', true)->orWhere('unlisted', true); + }); + } - public function reported() - { - return $this->hasManyThrough( - Report::class, - Profile::class, - 'domain', - 'reported_profile_id', - 'domain', - 'id' - ); - } + public function profiles() + { + return $this->hasMany(Profile::class, 'domain', 'domain'); + } - public function reports() - { - return $this->hasManyThrough( - Report::class, - Profile::class, - 'domain', - 'profile_id', - 'domain', - 'id' - ); - } + public function statuses() + { + return $this->hasManyThrough( + Status::class, + Profile::class, + 'domain', + 'profile_id', + 'domain', + 'id' + ); + } - public function media() - { - return $this->hasManyThrough( - Media::class, - Profile::class, - 'domain', - 'profile_id', - 'domain', - 'id' - ); - } + public function reported() + { + return $this->hasManyThrough( + Report::class, + Profile::class, + 'domain', + 'reported_profile_id', + 'domain', + 'id' + ); + } - public function getUrl() - { - return url("/i/admin/instances/show/{$this->id}"); - } + public function reports() + { + return $this->hasManyThrough( + Report::class, + Profile::class, + 'domain', + 'profile_id', + 'domain', + 'id' + ); + } + + public function media() + { + return $this->hasManyThrough( + Media::class, + Profile::class, + 'domain', + 'profile_id', + 'domain', + 'id' + ); + } + + public function getUrl() + { + return url("/i/admin/instances/show/{$this->id}"); + } } diff --git a/app/Jobs/AvatarPipeline/AvatarOptimize.php b/app/Jobs/AvatarPipeline/AvatarOptimize.php index 4464dff4e..8b50d8330 100644 --- a/app/Jobs/AvatarPipeline/AvatarOptimize.php +++ b/app/Jobs/AvatarPipeline/AvatarOptimize.php @@ -2,9 +2,9 @@ namespace App\Jobs\AvatarPipeline; -use Cache; use App\Avatar; use App\Profile; +use Cache; use Carbon\Carbon; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -17,88 +17,88 @@ use Storage; class AvatarOptimize implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $profile; - protected $current; + protected $profile; - /** - * Delete the job if its models no longer exist. - * - * @var bool - */ - public $deleteWhenMissingModels = true; + protected $current; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct(Profile $profile, $current) - { - $this->profile = $profile; - $this->current = $current; - } + /** + * Delete the job if its models no longer exist. + * + * @var bool + */ + public $deleteWhenMissingModels = true; - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $avatar = $this->profile->avatar; - $file = storage_path("app/$avatar->media_path"); + /** + * Create a new job instance. + * + * @return void + */ + public function __construct(Profile $profile, $current) + { + $this->profile = $profile; + $this->current = $current; + } - try { - $img = Intervention::make($file)->orientate(); - $img->fit(200, 200, function ($constraint) { - $constraint->upsize(); - }); - $quality = config_cache('pixelfed.image_quality'); - $img->save($file, $quality); + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $avatar = $this->profile->avatar; + $file = storage_path("app/$avatar->media_path"); - $avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail(); - $avatar->change_count = ++$avatar->change_count; - $avatar->last_processed_at = Carbon::now(); - $avatar->save(); - Cache::forget('avatar:' . $avatar->profile_id); - $this->deleteOldAvatar($avatar->media_path, $this->current); + try { + $img = Intervention::make($file)->orientate(); + $img->fit(200, 200, function ($constraint) { + $constraint->upsize(); + }); + $quality = config_cache('pixelfed.image_quality'); + $img->save($file, $quality); - if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) { - $this->uploadToCloud($avatar); - } else { - $avatar->cdn_url = null; - $avatar->save(); - } - } catch (Exception $e) { - } - } + $avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail(); + $avatar->change_count = ++$avatar->change_count; + $avatar->last_processed_at = Carbon::now(); + $avatar->save(); + Cache::forget('avatar:'.$avatar->profile_id); + $this->deleteOldAvatar($avatar->media_path, $this->current); - protected function deleteOldAvatar($new, $current) - { - if ( storage_path('app/'.$new) == $current || - Str::endsWith($current, 'avatars/default.png') || - Str::endsWith($current, 'avatars/default.jpg')) - { - return; - } - if (is_file($current)) { - @unlink($current); - } - } + if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('instance.avatar.local_to_cloud')) { + $this->uploadToCloud($avatar); + } else { + $avatar->cdn_url = null; + $avatar->save(); + } + } catch (Exception $e) { + } + } - protected function uploadToCloud($avatar) - { - $base = 'cache/avatars/' . $avatar->profile_id; - $disk = Storage::disk(config('filesystems.cloud')); - $disk->deleteDirectory($base); - $path = $base . '/' . 'avatar_' . strtolower(Str::random(random_int(3,6))) . $avatar->change_count . '.' . pathinfo($avatar->media_path, PATHINFO_EXTENSION); - $url = $disk->put($path, Storage::get($avatar->media_path)); - $avatar->media_path = $path; - $avatar->cdn_url = $disk->url($path); - $avatar->save(); - Storage::delete($avatar->media_path); - Cache::forget('avatar:' . $avatar->profile_id); - } + protected function deleteOldAvatar($new, $current) + { + if (storage_path('app/'.$new) == $current || + Str::endsWith($current, 'avatars/default.png') || + Str::endsWith($current, 'avatars/default.jpg')) { + return; + } + if (is_file($current)) { + @unlink($current); + } + } + + protected function uploadToCloud($avatar) + { + $base = 'cache/avatars/'.$avatar->profile_id; + $disk = Storage::disk(config('filesystems.cloud')); + $disk->deleteDirectory($base); + $path = $base.'/'.'avatar_'.strtolower(Str::random(random_int(3, 6))).$avatar->change_count.'.'.pathinfo($avatar->media_path, PATHINFO_EXTENSION); + $url = $disk->put($path, Storage::get($avatar->media_path)); + $avatar->media_path = $path; + $avatar->cdn_url = $disk->url($path); + $avatar->save(); + Storage::delete($avatar->media_path); + Cache::forget('avatar:'.$avatar->profile_id); + } } diff --git a/app/Jobs/AvatarPipeline/RemoteAvatarFetch.php b/app/Jobs/AvatarPipeline/RemoteAvatarFetch.php index 4e4a1b2ec..c2a2b2a16 100644 --- a/app/Jobs/AvatarPipeline/RemoteAvatarFetch.php +++ b/app/Jobs/AvatarPipeline/RemoteAvatarFetch.php @@ -4,112 +4,107 @@ namespace App\Jobs\AvatarPipeline; use App\Avatar; use App\Profile; +use App\Services\MediaStorageService; +use App\Util\ActivityPub\Helpers; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use App\Util\ActivityPub\Helpers; -use Illuminate\Support\Str; -use Zttp\Zttp; -use App\Http\Controllers\AvatarController; -use Storage; -use Log; -use Illuminate\Http\File; -use App\Services\MediaStorageService; -use App\Services\ActivityPubFetchService; class RemoteAvatarFetch implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $profile; + protected $profile; - /** - * Delete the job if its models no longer exist. - * - * @var bool - */ - public $deleteWhenMissingModels = true; + /** + * Delete the job if its models no longer exist. + * + * @var bool + */ + public $deleteWhenMissingModels = true; - /** - * The number of times the job may be attempted. - * - * @var int - */ - public $tries = 1; - public $timeout = 300; - public $maxExceptions = 1; + /** + * The number of times the job may be attempted. + * + * @var int + */ + public $tries = 1; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct(Profile $profile) - { - $this->profile = $profile; - } + public $timeout = 300; - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $profile = $this->profile; + public $maxExceptions = 1; - if(boolval(config_cache('pixelfed.cloud_storage')) == false && boolval(config_cache('federation.avatars.store_local')) == false) { - return 1; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct(Profile $profile) + { + $this->profile = $profile; + } - if($profile->domain == null || $profile->private_key) { - return 1; - } + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $profile = $this->profile; - $avatar = Avatar::whereProfileId($profile->id)->first(); + if ((bool) config_cache('pixelfed.cloud_storage') == false && (bool) config_cache('federation.avatars.store_local') == false) { + return 1; + } - if(!$avatar) { - $avatar = new Avatar; - $avatar->profile_id = $profile->id; - $avatar->save(); - } + if ($profile->domain == null || $profile->private_key) { + return 1; + } - if($avatar->media_path == null && $avatar->remote_url == null) { - $avatar->media_path = 'public/avatars/default.jpg'; - $avatar->is_remote = true; - $avatar->save(); - } + $avatar = Avatar::whereProfileId($profile->id)->first(); - $person = Helpers::fetchFromUrl($profile->remote_url); + if (! $avatar) { + $avatar = new Avatar; + $avatar->profile_id = $profile->id; + $avatar->save(); + } - if(!$person || !isset($person['@context'])) { - return 1; - } + if ($avatar->media_path == null && $avatar->remote_url == null) { + $avatar->media_path = 'public/avatars/default.jpg'; + $avatar->is_remote = true; + $avatar->save(); + } - if( !isset($person['icon']) || - !isset($person['icon']['type']) || - !isset($person['icon']['url']) - ) { - return 1; - } + $person = Helpers::fetchFromUrl($profile->remote_url); - if($person['icon']['type'] !== 'Image') { - return 1; - } + if (! $person || ! isset($person['@context'])) { + return 1; + } - if(!Helpers::validateUrl($person['icon']['url'])) { - return 1; - } + if (! isset($person['icon']) || + ! isset($person['icon']['type']) || + ! isset($person['icon']['url']) + ) { + return 1; + } - $icon = $person['icon']; + if ($person['icon']['type'] !== 'Image') { + return 1; + } - $avatar->remote_url = $icon['url']; - $avatar->save(); + if (! Helpers::validateUrl($person['icon']['url'])) { + return 1; + } - MediaStorageService::avatar($avatar, boolval(config_cache('pixelfed.cloud_storage')) == false, true); + $icon = $person['icon']; - return 1; - } + $avatar->remote_url = $icon['url']; + $avatar->save(); + + MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false, true); + + return 1; + } } diff --git a/app/Jobs/AvatarPipeline/RemoteAvatarFetchFromUrl.php b/app/Jobs/AvatarPipeline/RemoteAvatarFetchFromUrl.php index c8c6820e4..f8a63c5bd 100644 --- a/app/Jobs/AvatarPipeline/RemoteAvatarFetchFromUrl.php +++ b/app/Jobs/AvatarPipeline/RemoteAvatarFetchFromUrl.php @@ -4,93 +4,88 @@ namespace App\Jobs\AvatarPipeline; use App\Avatar; use App\Profile; +use App\Services\AccountService; +use App\Services\MediaStorageService; +use Cache; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use App\Util\ActivityPub\Helpers; -use Illuminate\Support\Str; -use Zttp\Zttp; -use App\Http\Controllers\AvatarController; -use Cache; -use Storage; -use Log; -use Illuminate\Http\File; -use App\Services\AccountService; -use App\Services\MediaStorageService; -use App\Services\ActivityPubFetchService; class RemoteAvatarFetchFromUrl implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $profile; - protected $url; + protected $profile; - /** - * Delete the job if its models no longer exist. - * - * @var bool - */ - public $deleteWhenMissingModels = true; + protected $url; - /** - * The number of times the job may be attempted. - * - * @var int - */ - public $tries = 1; - public $timeout = 300; - public $maxExceptions = 1; + /** + * Delete the job if its models no longer exist. + * + * @var bool + */ + public $deleteWhenMissingModels = true; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct(Profile $profile, $url) - { - $this->profile = $profile; - $this->url = $url; - } + /** + * The number of times the job may be attempted. + * + * @var int + */ + public $tries = 1; - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $profile = $this->profile; + public $timeout = 300; - Cache::forget('avatar:' . $profile->id); - AccountService::del($profile->id); + public $maxExceptions = 1; - if(boolval(config_cache('pixelfed.cloud_storage')) == false && boolval(config_cache('federation.avatars.store_local')) == false) { - return 1; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct(Profile $profile, $url) + { + $this->profile = $profile; + $this->url = $url; + } - if($profile->domain == null || $profile->private_key) { - return 1; - } + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $profile = $this->profile; - $avatar = Avatar::whereProfileId($profile->id)->first(); + Cache::forget('avatar:'.$profile->id); + AccountService::del($profile->id); - if(!$avatar) { - $avatar = new Avatar; - $avatar->profile_id = $profile->id; - $avatar->is_remote = true; - $avatar->remote_url = $this->url; - $avatar->save(); - } else { - $avatar->remote_url = $this->url; - $avatar->is_remote = true; - $avatar->save(); - } + if ((bool) config_cache('pixelfed.cloud_storage') == false && (bool) config_cache('federation.avatars.store_local') == false) { + return 1; + } - MediaStorageService::avatar($avatar, boolval(config_cache('pixelfed.cloud_storage')) == false, true); + if ($profile->domain == null || $profile->private_key) { + return 1; + } - return 1; - } + $avatar = Avatar::whereProfileId($profile->id)->first(); + + if (! $avatar) { + $avatar = new Avatar; + $avatar->profile_id = $profile->id; + $avatar->is_remote = true; + $avatar->remote_url = $this->url; + $avatar->save(); + } else { + $avatar->remote_url = $this->url; + $avatar->is_remote = true; + $avatar->save(); + } + + MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false, true); + + return 1; + } } diff --git a/app/Jobs/CommentPipeline/CommentPipeline.php b/app/Jobs/CommentPipeline/CommentPipeline.php index 3b2d896af..1917ecea5 100644 --- a/app/Jobs/CommentPipeline/CommentPipeline.php +++ b/app/Jobs/CommentPipeline/CommentPipeline.php @@ -91,19 +91,21 @@ class CommentPipeline implements ShouldQueue return; } - DB::transaction(function() use($target, $actor, $comment) { - $notification = new Notification(); - $notification->profile_id = $target->id; - $notification->actor_id = $actor->id; - $notification->action = 'comment'; - $notification->item_id = $comment->id; - $notification->item_type = "App\Status"; - $notification->save(); + if($target->user_id && $target->domain === null) { + DB::transaction(function() use($target, $actor, $comment) { + $notification = new Notification(); + $notification->profile_id = $target->id; + $notification->actor_id = $actor->id; + $notification->action = 'comment'; + $notification->item_id = $comment->id; + $notification->item_type = "App\Status"; + $notification->save(); - NotificationService::setNotification($notification); - NotificationService::set($notification->profile_id, $notification->id); - StatusService::del($comment->id); - }); + NotificationService::setNotification($notification); + NotificationService::set($notification->profile_id, $notification->id); + StatusService::del($comment->id); + }); + } if($exists = Cache::get('status:replies:all:' . $status->id)) { if($exists && $exists->count() == 3) { diff --git a/app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php b/app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php new file mode 100644 index 000000000..a1b5f279a --- /dev/null +++ b/app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php @@ -0,0 +1,65 @@ +cr = $cr; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if(!config('instance.curated_registration.notify.admin.on_verify_email.enabled')) { + return; + } + + config('instance.curated_registration.notify.admin.on_verify_email.bundle') ? + $this->handleBundled() : + $this->handleUnbundled(); + } + + protected function handleBundled() + { + $cr = $this->cr; + Storage::append('conanap.json', json_encode([ + 'id' => $cr->id, + 'email' => $cr->email, + 'created_at' => $cr->created_at, + 'updated_at' => $cr->updated_at, + ])); + } + + protected function handleUnbundled() + { + $cr = $this->cr; + if($aid = config_cache('instance.admin.pid')) { + $admin = User::whereProfileId($aid)->first(); + if($admin && $admin->email) { + Mail::to($admin->email)->send(new CuratedRegisterNotifyAdmin($cr)); + } + } + } +} diff --git a/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php b/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php index 4969fca2f..77cd5286f 100644 --- a/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php +++ b/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php @@ -22,9 +22,9 @@ use App\Notification; use App\Services\AccountService; use App\Services\NetworkTimelineService; use App\Services\StatusService; -use App\Jobs\ProfilePipeline\DecrementPostCount; use App\Jobs\MediaPipeline\MediaDeletePipeline; use Cache; +use App\Services\Account\AccountStatService; class DeleteRemoteStatusPipeline implements ShouldQueue { @@ -56,10 +56,7 @@ class DeleteRemoteStatusPipeline implements ShouldQueue { $status = $this->status; - if(AccountService::get($status->profile_id, true)) { - DecrementPostCount::dispatch($status->profile_id)->onQueue('low'); - } - + AccountStatService::decrementPostCount($status->profile_id); NetworkTimelineService::del($status->id); StatusService::del($status->id, true); Bookmark::whereStatusId($status->id)->delete(); diff --git a/app/Jobs/DirectPipeline/DirectDeletePipeline.php b/app/Jobs/DirectPipeline/DirectDeletePipeline.php new file mode 100644 index 000000000..947806422 --- /dev/null +++ b/app/Jobs/DirectPipeline/DirectDeletePipeline.php @@ -0,0 +1,42 @@ +profile = $profile; + $this->url = $url; + $this->payload = $payload; + } + + /** + * Execute the job. + */ + public function handle(): void + { + Helpers::sendSignedObject($this->profile, $this->url, $this->payload); + } +} diff --git a/app/Jobs/DirectPipeline/DirectDeliverPipeline.php b/app/Jobs/DirectPipeline/DirectDeliverPipeline.php new file mode 100644 index 000000000..7d20a406e --- /dev/null +++ b/app/Jobs/DirectPipeline/DirectDeliverPipeline.php @@ -0,0 +1,42 @@ +profile = $profile; + $this->url = $url; + $this->payload = $payload; + } + + /** + * Execute the job. + */ + public function handle(): void + { + Helpers::sendSignedObject($this->profile, $this->url, $this->payload); + } +} diff --git a/app/Jobs/FollowPipeline/FollowPipeline.php b/app/Jobs/FollowPipeline/FollowPipeline.php index 225334304..0df16206a 100644 --- a/app/Jobs/FollowPipeline/FollowPipeline.php +++ b/app/Jobs/FollowPipeline/FollowPipeline.php @@ -3,7 +3,13 @@ namespace App\Jobs\FollowPipeline; use App\Follower; +use App\Jobs\PushNotificationPipeline\FollowPushNotifyPipeline; use App\Notification; +use App\Services\AccountService; +use App\Services\FollowerService; +use App\Services\NotificationAppGatewayService; +use App\Services\PushNotificationService; +use App\User; use Cache; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -11,9 +17,6 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Log; -use Illuminate\Support\Facades\Redis; -use App\Services\AccountService; -use App\Services\FollowerService; class FollowPipeline implements ShouldQueue { @@ -49,16 +52,16 @@ class FollowPipeline implements ShouldQueue $actor = $follower->actor; $target = $follower->target; - if(!$actor || !$target) { + if (! $actor || ! $target) { return; } - if($target->domain || !$target->private_key) { + if ($target->domain || ! $target->private_key) { return; } - Cache::forget('profile:following:' . $actor->id); - Cache::forget('profile:following:' . $target->id); + Cache::forget('profile:following:'.$actor->id); + Cache::forget('profile:following:'.$target->id); FollowerService::add($actor->id, $target->id); @@ -72,16 +75,27 @@ class FollowPipeline implements ShouldQueue $target->save(); AccountService::del($target->id); - try { - $notification = new Notification(); - $notification->profile_id = $target->id; - $notification->actor_id = $actor->id; - $notification->action = 'follow'; - $notification->item_id = $target->id; - $notification->item_type = "App\Profile"; - $notification->save(); - } catch (Exception $e) { - Log::error($e); + if ($target->user_id && $target->domain === null) { + try { + $notification = new Notification; + $notification->profile_id = $target->id; + $notification->actor_id = $actor->id; + $notification->action = 'follow'; + $notification->item_id = $target->id; + $notification->item_type = "App\Profile"; + $notification->save(); + } catch (Exception $e) { + Log::error($e); + } + + if (NotificationAppGatewayService::enabled()) { + if (PushNotificationService::check('follow', $target->id)) { + $user = User::whereProfileId($target->id)->first(); + if ($user && $user->expo_token && $user->notify_enabled) { + FollowPushNotifyPipeline::dispatch($user->expo_token, $actor->username)->onQueue('pushnotify'); + } + } + } } } } diff --git a/app/Jobs/FollowPipeline/FollowServiceWarmCache.php b/app/Jobs/FollowPipeline/FollowServiceWarmCache.php index 990236f69..0d3cca7ac 100644 --- a/app/Jobs/FollowPipeline/FollowServiceWarmCache.php +++ b/app/Jobs/FollowPipeline/FollowServiceWarmCache.php @@ -73,7 +73,7 @@ class FollowServiceWarmCache implements ShouldQueue if(Follower::whereProfileId($id)->orWhere('following_id', $id)->count()) { $following = []; $followers = []; - foreach(Follower::lazy() as $follow) { + foreach(Follower::where('following_id', $id)->orWhere('profile_id', $id)->lazyById(500) as $follow) { if($follow->following_id != $id && $follow->profile_id != $id) { continue; } diff --git a/app/Jobs/FollowPipeline/UnfollowPipeline.php b/app/Jobs/FollowPipeline/UnfollowPipeline.php index c00246e2f..9417e535e 100644 --- a/app/Jobs/FollowPipeline/UnfollowPipeline.php +++ b/app/Jobs/FollowPipeline/UnfollowPipeline.php @@ -4,111 +4,115 @@ namespace App\Jobs\FollowPipeline; use App\Follower; use App\FollowRequest; +use App\Jobs\HomeFeedPipeline\FeedUnfollowPipeline; use App\Notification; use App\Profile; +use App\Services\AccountService; +use App\Services\FollowerService; +use App\Services\NotificationService; use Cache; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Log; -use Illuminate\Support\Facades\Redis; -use App\Services\AccountService; -use App\Services\FollowerService; -use App\Services\NotificationService; class UnfollowPipeline implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $actor; - protected $target; + protected $actor; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct($actor, $target) - { - $this->actor = $actor; - $this->target = $target; - } + protected $target; - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $actor = $this->actor; - $target = $this->target; + /** + * Create a new job instance. + * + * @return void + */ + public function __construct($actor, $target) + { + $this->actor = $actor; + $this->target = $target; + } - $actorProfile = Profile::find($actor); - if(!$actorProfile) { - return; - } - $targetProfile = Profile::find($target); - if(!$targetProfile) { - return; - } + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $actor = $this->actor; + $target = $this->target; - FollowerService::remove($actor, $target); + $actorProfile = Profile::find($actor); + if (! $actorProfile) { + return; + } + $targetProfile = Profile::find($target); + if (! $targetProfile) { + return; + } - $actorProfileSync = Cache::get(FollowerService::FOLLOWING_SYNC_KEY . $actor); - if(!$actorProfileSync) { - FollowServiceWarmCache::dispatch($actor)->onQueue('low'); - } else { - if($actorProfile->following_count) { - $actorProfile->decrement('following_count'); - } else { - $count = Follower::whereProfileId($actor)->count(); - $actorProfile->following_count = $count; - $actorProfile->save(); - } - Cache::put(FollowerService::FOLLOWING_SYNC_KEY . $actor, 1, 604800); - AccountService::del($actor); - } + FeedUnfollowPipeline::dispatch($actor, $target)->onQueue('follow'); - $targetProfileSync = Cache::get(FollowerService::FOLLOWERS_SYNC_KEY . $target); - if(!$targetProfileSync) { - FollowServiceWarmCache::dispatch($target)->onQueue('low'); - } else { - if($targetProfile->followers_count) { - $targetProfile->decrement('followers_count'); - } else { - $count = Follower::whereFollowingId($target)->count(); - $targetProfile->followers_count = $count; - $targetProfile->save(); - } - Cache::put(FollowerService::FOLLOWERS_SYNC_KEY . $target, 1, 604800); - AccountService::del($target); - } + FollowerService::remove($actor, $target); - if($targetProfile->domain == null) { - Notification::withTrashed() - ->whereProfileId($target) - ->whereAction('follow') - ->whereActorId($actor) - ->whereItemId($target) - ->whereItemType('App\Profile') - ->get() - ->each(function($n) { - NotificationService::del($n->profile_id, $n->id); - $n->forceDelete(); - }); - } + $actorProfileSync = Cache::get(FollowerService::FOLLOWING_SYNC_KEY.$actor); + if (! $actorProfileSync) { + FollowServiceWarmCache::dispatch($actor)->onQueue('low'); + } else { + if ($actorProfile->following_count) { + $actorProfile->decrement('following_count'); + } else { + $count = Follower::whereProfileId($actor)->count(); + $actorProfile->following_count = $count; + $actorProfile->save(); + } + Cache::put(FollowerService::FOLLOWING_SYNC_KEY.$actor, 1, 604800); + AccountService::del($actor); + } - if($actorProfile->domain == null && config('instance.timeline.home.cached')) { - Cache::forget('pf:timelines:home:' . $actor); - } + $targetProfileSync = Cache::get(FollowerService::FOLLOWERS_SYNC_KEY.$target); + if (! $targetProfileSync) { + FollowServiceWarmCache::dispatch($target)->onQueue('low'); + } else { + if ($targetProfile->followers_count) { + $targetProfile->decrement('followers_count'); + } else { + $count = Follower::whereFollowingId($target)->count(); + $targetProfile->followers_count = $count; + $targetProfile->save(); + } + Cache::put(FollowerService::FOLLOWERS_SYNC_KEY.$target, 1, 604800); + AccountService::del($target); + } - FollowRequest::whereFollowingId($target) - ->whereFollowerId($actor) - ->delete(); + if ($targetProfile->domain == null) { + Notification::withTrashed() + ->whereProfileId($target) + ->whereAction('follow') + ->whereActorId($actor) + ->whereItemId($target) + ->whereItemType('App\Profile') + ->get() + ->each(function ($n) { + NotificationService::del($n->profile_id, $n->id); + $n->forceDelete(); + }); + } - return; - } + if ($actorProfile->domain == null && config('instance.timeline.home.cached')) { + Cache::forget('pf:timelines:home:'.$actor); + } + + FollowRequest::whereFollowingId($target) + ->whereFollowerId($actor) + ->delete(); + + AccountService::del($target); + AccountService::del($actor); + + } } diff --git a/app/Jobs/GroupPipeline/GroupCommentPipeline.php b/app/Jobs/GroupPipeline/GroupCommentPipeline.php new file mode 100644 index 000000000..cdae65d10 --- /dev/null +++ b/app/Jobs/GroupPipeline/GroupCommentPipeline.php @@ -0,0 +1,99 @@ +status = $status; + $this->comment = $comment; + $this->groupPost = $groupPost; + } + + public function handle() + { + if($this->status->group_id == null || $this->comment->group_id == null) { + return; + } + + $this->updateParentReplyCount(); + $this->generateNotification(); + + if($this->groupPost) { + $this->updateChildReplyCount(); + } + } + + protected function updateParentReplyCount() + { + $parent = $this->status; + $parent->reply_count = Status::whereInReplyToId($parent->id)->count(); + $parent->save(); + StatusService::del($parent->id); + } + + protected function updateChildReplyCount() + { + $gp = $this->groupPost; + if($gp->reply_child_id) { + $parent = GroupPost::whereStatusId($gp->reply_child_id)->first(); + if($parent) { + $parent->reply_count++; + $parent->save(); + } + } + } + + protected function generateNotification() + { + $status = $this->status; + $comment = $this->comment; + + $target = $status->profile; + $actor = $comment->profile; + + if ($actor->id == $target->id || $status->comments_disabled == true) { + return; + } + + $notification = DB::transaction(function() use($target, $actor, $comment) { + $actorName = $actor->username; + $actorUrl = $actor->url(); + $text = "{$actorName} commented on your group post."; + $html = "{$actorName} commented on your group post."; + $notification = new Notification(); + $notification->profile_id = $target->id; + $notification->actor_id = $actor->id; + $notification->action = 'group:comment'; + $notification->item_id = $comment->id; + $notification->item_type = "App\Status"; + $notification->save(); + return $notification; + }); + + NotificationService::setNotification($notification); + NotificationService::set($notification->profile_id, $notification->id); + } +} diff --git a/app/Jobs/GroupPipeline/GroupMediaPipeline.php b/app/Jobs/GroupPipeline/GroupMediaPipeline.php new file mode 100644 index 000000000..1155e5465 --- /dev/null +++ b/app/Jobs/GroupPipeline/GroupMediaPipeline.php @@ -0,0 +1,57 @@ +media = $media; + } + + public function handle() + { + MediaStorageService::store($this->media); + } + + protected function localToCloud($media) + { + $path = storage_path('app/'.$media->media_path); + $thumb = storage_path('app/'.$media->thumbnail_path); + + $p = explode('/', $media->media_path); + $name = array_pop($p); + $pt = explode('/', $media->thumbnail_path); + $thumbname = array_pop($pt); + $storagePath = implode('/', $p); + + $disk = Storage::disk(config('filesystems.cloud')); + $file = $disk->putFileAs($storagePath, new File($path), $name, 'public'); + $url = $disk->url($file); + $thumbFile = $disk->putFileAs($storagePath, new File($thumb), $thumbname, 'public'); + $thumbUrl = $disk->url($thumbFile); + $media->thumbnail_url = $thumbUrl; + $media->cdn_url = $url; + $media->optimized_url = $url; + $media->replicated_at = now(); + $media->save(); + if($media->status_id) { + Cache::forget('status:transformer:media:attachments:' . $media->status_id); + } + } + +} diff --git a/app/Jobs/GroupPipeline/GroupMemberInvite.php b/app/Jobs/GroupPipeline/GroupMemberInvite.php new file mode 100644 index 000000000..d2c2bf8ef --- /dev/null +++ b/app/Jobs/GroupPipeline/GroupMemberInvite.php @@ -0,0 +1,54 @@ +invite = $invite; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $invite = $this->invite; + $actor = Profile::find($invite->from_profile_id); + $target = Profile::find($invite->to_profile_id); + + if(!$actor || !$target) { + return; + } + + $notification = new Notification; + $notification->profile_id = $target->id; + $notification->actor_id = $actor->id; + $notification->action = 'group:invite'; + $notification->item_id = $invite->group_id; + $notification->item_type = 'App\Models\Group'; + $notification->save(); + } +} diff --git a/app/Jobs/GroupPipeline/JoinApproved.php b/app/Jobs/GroupPipeline/JoinApproved.php new file mode 100644 index 000000000..f41c8f698 --- /dev/null +++ b/app/Jobs/GroupPipeline/JoinApproved.php @@ -0,0 +1,54 @@ +member = $member; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $member = $this->member; + $member->approved_at = now(); + $member->join_request = false; + $member->role = 'member'; + $member->save(); + + $n = new Notification; + $n->profile_id = $member->profile_id; + $n->actor_id = $member->profile_id; + $n->item_id = $member->group_id; + $n->item_type = 'App\Models\Group'; + $n->save(); + + GroupService::del($member->group_id); + GroupService::delSelf($member->group_id, $member->profile_id); + } +} diff --git a/app/Jobs/GroupPipeline/JoinRejected.php b/app/Jobs/GroupPipeline/JoinRejected.php new file mode 100644 index 000000000..71e1e30c8 --- /dev/null +++ b/app/Jobs/GroupPipeline/JoinRejected.php @@ -0,0 +1,50 @@ +member = $member; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $member = $this->member; + $member->rejected_at = now(); + $member->save(); + + $n = new Notification; + $n->profile_id = $member->profile_id; + $n->actor_id = $member->profile_id; + $n->item_id = $member->group_id; + $n->item_type = 'App\Models\Group'; + $n->action = 'group.join.rejected'; + $n->save(); + } +} diff --git a/app/Jobs/GroupPipeline/LikePipeline.php b/app/Jobs/GroupPipeline/LikePipeline.php new file mode 100644 index 000000000..bd3e668f7 --- /dev/null +++ b/app/Jobs/GroupPipeline/LikePipeline.php @@ -0,0 +1,107 @@ +like = $like; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $like = $this->like; + + $status = $this->like->status; + $actor = $this->like->actor; + + if (!$status) { + // Ignore notifications to deleted statuses + return; + } + + StatusService::refresh($status->id); + + if($status->url && $actor->domain == null) { + return $this->remoteLikeDeliver(); + } + + $exists = Notification::whereProfileId($status->profile_id) + ->whereActorId($actor->id) + ->whereAction('group:like') + ->whereItemId($status->id) + ->whereItemType('App\Status') + ->count(); + + if ($actor->id === $status->profile_id || $exists !== 0) { + return true; + } + + try { + $notification = new Notification(); + $notification->profile_id = $status->profile_id; + $notification->actor_id = $actor->id; + $notification->action = 'group:like'; + $notification->item_id = $status->id; + $notification->item_type = "App\Status"; + $notification->save(); + + } catch (Exception $e) { + } + } + + public function remoteLikeDeliver() + { + $like = $this->like; + $status = $this->like->status; + $actor = $this->like->actor; + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($like, new LikeTransformer()); + $activity = $fractal->createData($resource)->toArray(); + + $url = $status->profile->sharedInbox ?? $status->profile->inbox_url; + + Helpers::sendSignedObject($actor, $url, $activity); + } +} diff --git a/app/Jobs/GroupPipeline/NewStatusPipeline.php b/app/Jobs/GroupPipeline/NewStatusPipeline.php new file mode 100644 index 000000000..d791d81a4 --- /dev/null +++ b/app/Jobs/GroupPipeline/NewStatusPipeline.php @@ -0,0 +1,123 @@ +status = $status; + $this->gp = $gp; + } + + public function handle() + { + $status = $this->status; + + $autolink = Autolink::create() + ->setAutolinkActiveUsersOnly(true) + ->setBaseHashPath("/groups/{$status->group_id}/topics/") + ->setBaseUserPath("/groups/{$status->group_id}/username/") + ->autolink($status->caption); + + $entities = Extractor::create()->extract($status->caption); + $status->entities = null; + $status->save(); + + $this->tags = array_unique($entities['hashtags']); + $this->mentions = array_unique($entities['mentions']); + + if (count($this->tags)) { + $this->storeHashtags(); + } + + if (count($this->mentions)) { + $this->storeMentions($this->mentions); + } + } + + protected function storeHashtags() + { + $tags = $this->tags; + $status = $this->status; + $gp = $this->gp; + + foreach ($tags as $tag) { + if (mb_strlen($tag) > 124) { + continue; + } + + DB::transaction(function () use ($status, $tag, $gp) { + $slug = str_slug($tag, '-', false); + $hashtag = Hashtag::firstOrCreate( + ['name' => $tag, 'slug' => $slug] + ); + GroupPostHashtag::firstOrCreate( + [ + 'group_id' => $status->group_id, + 'group_post_id' => $gp->id, + 'status_id' => $status->id, + 'hashtag_id' => $hashtag->id, + 'profile_id' => $status->profile_id, + ] + ); + + }); + } + + if (count($this->mentions)) { + $this->storeMentions(); + } + StatusService::del($status->id); + } + + protected function storeMentions() + { + $mentions = $this->mentions; + $status = $this->status; + + foreach ($mentions as $mention) { + $mentioned = Profile::whereUsername($mention)->first(); + + if (empty($mentioned) || ! isset($mentioned->id)) { + continue; + } + + DB::transaction(function () use ($status, $mentioned) { + $m = new Mention; + $m->status_id = $status->id; + $m->profile_id = $mentioned->id; + $m->save(); + + MentionPipeline::dispatch($status, $m); + }); + } + StatusService::del($status->id); + } +} diff --git a/app/Jobs/GroupPipeline/UnlikePipeline.php b/app/Jobs/GroupPipeline/UnlikePipeline.php new file mode 100644 index 000000000..b322d6853 --- /dev/null +++ b/app/Jobs/GroupPipeline/UnlikePipeline.php @@ -0,0 +1,109 @@ +like = $like; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $like = $this->like; + + $status = $this->like->status; + $actor = $this->like->actor; + + if (!$status) { + // Ignore notifications to deleted statuses + return; + } + + $count = $status->likes_count > 1 ? $status->likes_count : $status->likes()->count(); + $status->likes_count = $count - 1; + $status->save(); + + StatusService::del($status->id); + + if($actor->id !== $status->profile_id && $status->url && $actor->domain == null) { + $this->remoteLikeDeliver(); + } + + $exists = Notification::whereProfileId($status->profile_id) + ->whereActorId($actor->id) + ->whereAction('group:like') + ->whereItemId($status->id) + ->whereItemType('App\Status') + ->first(); + + if($exists) { + $exists->delete(); + } + + $like = Like::whereProfileId($actor->id)->whereStatusId($status->id)->first(); + + if(!$like) { + return; + } + + $like->forceDelete(); + + return; + } + + public function remoteLikeDeliver() + { + $like = $this->like; + $status = $this->like->status; + $actor = $this->like->actor; + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($like, new LikeTransformer()); + $activity = $fractal->createData($resource)->toArray(); + + $url = $status->profile->sharedInbox ?? $status->profile->inbox_url; + + Helpers::sendSignedObject($actor, $url, $activity); + } +} diff --git a/app/Jobs/GroupsPipeline/DeleteCommentPipeline.php b/app/Jobs/GroupsPipeline/DeleteCommentPipeline.php new file mode 100644 index 000000000..e1d94c5de --- /dev/null +++ b/app/Jobs/GroupsPipeline/DeleteCommentPipeline.php @@ -0,0 +1,58 @@ +parent = $parent; + $this->status = $status; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $parent = $this->parent; + $parent->reply_count = GroupComment::whereStatusId($parent->id)->count(); + $parent->save(); + + return; + } +} diff --git a/app/Jobs/GroupsPipeline/ImageResizePipeline.php b/app/Jobs/GroupsPipeline/ImageResizePipeline.php new file mode 100644 index 000000000..fa649efea --- /dev/null +++ b/app/Jobs/GroupsPipeline/ImageResizePipeline.php @@ -0,0 +1,89 @@ +media = $media; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $media = $this->media; + + if(!$media) { + return; + } + + if (!Storage::exists($media->media_path) || $media->skip_optimize) { + return; + } + + $path = $media->media_path; + $file = storage_path('app/' . $path); + $quality = config_cache('pixelfed.image_quality'); + + $orientations = [ + 'square' => [ + 'width' => 1080, + 'height' => 1080, + ], + 'landscape' => [ + 'width' => 1920, + 'height' => 1080, + ], + 'portrait' => [ + 'width' => 1080, + 'height' => 1350, + ], + ]; + + try { + $img = Intervention::make($file); + $img->orientate(); + $width = $img->width(); + $height = $img->height(); + $aspect = $width / $height; + $orientation = $aspect === 1 ? 'square' : ($aspect > 1 ? 'landscape' : 'portrait'); + $ratio = $orientations[$orientation]; + $img->resize($ratio['width'], $ratio['height']); + $img->save($file, $quality); + } catch (Exception $e) { + Log::error($e); + } + } +} diff --git a/app/Jobs/GroupsPipeline/ImageS3DeletePipeline.php b/app/Jobs/GroupsPipeline/ImageS3DeletePipeline.php new file mode 100644 index 000000000..d59c6d086 --- /dev/null +++ b/app/Jobs/GroupsPipeline/ImageS3DeletePipeline.php @@ -0,0 +1,67 @@ +media = $media; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $media = $this->media; + + if(!$media || (bool) config_cache('pixelfed.cloud_storage') === false) { + return; + } + + $fs = Storage::disk(config('filesystems.cloud')); + + if(!$fs) { + return; + } + + if($fs->exists($media->media_path)) { + $fs->delete($media->media_path); + } + } +} diff --git a/app/Jobs/GroupsPipeline/ImageS3UploadPipeline.php b/app/Jobs/GroupsPipeline/ImageS3UploadPipeline.php new file mode 100644 index 000000000..169c11073 --- /dev/null +++ b/app/Jobs/GroupsPipeline/ImageS3UploadPipeline.php @@ -0,0 +1,107 @@ +media = $media; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $media = $this->media; + + if(!$media || (bool) config_cache('pixelfed.cloud_storage') === false) { + return; + } + + $path = storage_path('app/' . $media->media_path); + + $p = explode('/', $media->media_path); + $name = array_pop($p); + $storagePath = implode('/', $p); + + $url = (bool) config_cache('pixelfed.cloud_storage') && (bool) config('media.storage.remote.resilient_mode') ? + self::handleResilientStore($storagePath, $path, $name) : + self::handleStore($storagePath, $path, $name); + + if($url && strlen($url) && str_starts_with($url, 'https://')) { + $media->cdn_url = $url; + $media->processed_at = now(); + $media->version = 11; + $media->save(); + Storage::disk('local')->delete($media->media_path); + } + } + + protected function handleStore($storagePath, $path, $name) + { + return retry(3, function() use($storagePath, $path, $name) { + $baseDisk = (bool) config_cache('pixelfed.cloud_storage') ? config('filesystems.cloud') : 'local'; + $disk = Storage::disk($baseDisk); + $file = $disk->putFileAs($storagePath, new File($path), $name, 'public'); + return $disk->url($file); + }, random_int(100, 500)); + } + + protected function handleResilientStore($storagePath, $path, $name) + { + $attempts = 0; + return retry(4, function() use($storagePath, $path, $name, $attempts) { + self::$attempts++; + usleep(100000); + $baseDisk = self::$attempts > 1 ? $this->getAltDriver() : config('filesystems.cloud'); + try { + $disk = Storage::disk($baseDisk); + $file = $disk->putFileAs($storagePath, new File($path), $name, 'public'); + } catch (S3Exception | ClientException | ConnectException | UnableToWriteFile | Exception $e) {} + return $disk->url($file); + }, function (int $attempt, Exception $exception) { + return $attempt * 200; + }); + } + + protected function getAltDriver() + { + return config('filesystems.cloud'); + } +} diff --git a/app/Jobs/GroupsPipeline/MemberJoinApprovedPipeline.php b/app/Jobs/GroupsPipeline/MemberJoinApprovedPipeline.php new file mode 100644 index 000000000..a3ec21982 --- /dev/null +++ b/app/Jobs/GroupsPipeline/MemberJoinApprovedPipeline.php @@ -0,0 +1,47 @@ +member = $member; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $member = $this->member; + $member->approved_at = now(); + $member->join_request = false; + $member->role = 'member'; + $member->save(); + + GroupService::del($member->group_id); + GroupService::delSelf($member->group_id, $member->profile_id); + } +} diff --git a/app/Jobs/GroupsPipeline/MemberJoinRejectedPipeline.php b/app/Jobs/GroupsPipeline/MemberJoinRejectedPipeline.php new file mode 100644 index 000000000..5e8226de0 --- /dev/null +++ b/app/Jobs/GroupsPipeline/MemberJoinRejectedPipeline.php @@ -0,0 +1,42 @@ +member = $member; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $member = $this->member; + $member->rejected_at = now(); + $member->save(); + } +} diff --git a/app/Jobs/GroupsPipeline/NewCommentPipeline.php b/app/Jobs/GroupsPipeline/NewCommentPipeline.php new file mode 100644 index 000000000..fb618a14d --- /dev/null +++ b/app/Jobs/GroupsPipeline/NewCommentPipeline.php @@ -0,0 +1,115 @@ +parent = $parent; + $this->status = $status; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $profile = $this->status->profile; + $status = $this->status; + + $parent = $this->parent; + $parent->reply_count = GroupComment::whereStatusId($parent->id)->count(); + $parent->save(); + + if ($profile->no_autolink == false) { + $this->parseEntities(); + } + } + + public function parseEntities() + { + $this->extractEntities(); + } + + public function extractEntities() + { + $this->entities = Extractor::create()->extract($this->status->caption); + $this->autolinkStatus(); + } + + public function autolinkStatus() + { + $this->autolink = Autolink::create()->autolink($this->status->caption); + $this->storeHashtags(); + } + + public function storeHashtags() + { + $tags = array_unique($this->entities['hashtags']); + $status = $this->status; + + foreach ($tags as $tag) { + if (mb_strlen($tag) > 124) { + continue; + } + DB::transaction(function () use ($status, $tag) { + $hashtag = GroupHashtag::firstOrCreate([ + 'name' => $tag, + ]); + + GroupPostHashtag::firstOrCreate( + [ + 'status_id' => $status->id, + 'group_id' => $status->group_id, + 'hashtag_id' => $hashtag->id, + 'profile_id' => $status->profile_id, + 'status_visibility' => $status->visibility, + ] + ); + }); + } + $this->storeMentions(); + } + + public function storeMentions() + { + // todo + } +} diff --git a/app/Jobs/GroupsPipeline/NewPostPipeline.php b/app/Jobs/GroupsPipeline/NewPostPipeline.php new file mode 100644 index 000000000..1302a0233 --- /dev/null +++ b/app/Jobs/GroupsPipeline/NewPostPipeline.php @@ -0,0 +1,108 @@ +status = $status; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $profile = $this->status->profile; + $status = $this->status; + + if ($profile->no_autolink == false) { + $this->parseEntities(); + } + } + + public function parseEntities() + { + $this->extractEntities(); + } + + public function extractEntities() + { + $this->entities = Extractor::create()->extract($this->status->caption); + $this->autolinkStatus(); + } + + public function autolinkStatus() + { + $this->autolink = Autolink::create()->autolink($this->status->caption); + $this->storeHashtags(); + } + + public function storeHashtags() + { + $tags = array_unique($this->entities['hashtags']); + $status = $this->status; + + foreach ($tags as $tag) { + if (mb_strlen($tag) > 124) { + continue; + } + DB::transaction(function () use ($status, $tag) { + $hashtag = GroupHashtag::firstOrCreate([ + 'name' => $tag, + ]); + + GroupPostHashtag::firstOrCreate( + [ + 'status_id' => $status->id, + 'group_id' => $status->group_id, + 'hashtag_id' => $hashtag->id, + 'profile_id' => $status->profile_id, + 'status_visibility' => $status->visibility, + ] + ); + }); + } + $this->storeMentions(); + } + + public function storeMentions() + { + // todo + } +} diff --git a/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php b/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php new file mode 100644 index 000000000..e386329ca --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php @@ -0,0 +1,87 @@ +actorId . ':fid:' . $this->followingId; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:insert:follows:aid:{$this->actorId}:fid:{$this->followingId}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($actorId, $followingId) + { + $this->actorId = $actorId; + $this->followingId = $followingId; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $actorId = $this->actorId; + $followingId = $this->followingId; + + $minId = SnowflakeService::byDate(now()->subWeeks(6)); + + $ids = Status::where('id', '>', $minId) + ->where('profile_id', $followingId) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereIn('visibility',['public', 'unlisted', 'private']) + ->orderByDesc('id') + ->limit(HomeTimelineService::FOLLOWER_FEED_POST_LIMIT) + ->pluck('id'); + + foreach($ids as $id) { + HomeTimelineService::add($actorId, $id); + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php new file mode 100644 index 000000000..4237a7b1a --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php @@ -0,0 +1,114 @@ +sid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:insert:sid:{$this->sid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($sid, $pid) + { + $this->sid = $sid; + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $sid = $this->sid; + $status = StatusService::get($sid, false); + + if(!$status || !isset($status['account']) || !isset($status['account']['id'], $status['url'])) { + return; + } + + if(!in_array($status['pf_type'], ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + return; + } + + HomeTimelineService::add($this->pid, $this->sid); + + $ids = FollowerService::localFollowerIds($this->pid); + + if(!$ids || !count($ids)) { + return; + } + + $domain = strtolower(parse_url($status['url'], PHP_URL_HOST)); + $skipIds = []; + + if(strtolower(config('pixelfed.domain.app')) !== $domain) { + $skipIds = UserDomainBlock::where('domain', $domain)->pluck('profile_id')->toArray(); + } + + $filters = UserFilter::whereFilterableType('App\Profile') + ->whereFilterableId($status['account']['id']) + ->whereIn('filter_type', ['mute', 'block']) + ->pluck('user_id') + ->toArray(); + + if($filters && count($filters)) { + $skipIds = array_merge($skipIds, $filters); + } + + $skipIds = array_unique(array_values($skipIds)); + + foreach($ids as $id) { + if(!in_array($id, $skipIds)) { + HomeTimelineService::add($id, $this->sid); + } + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php new file mode 100644 index 000000000..6c4ce0c35 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php @@ -0,0 +1,112 @@ +sid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:insert:remote:sid:{$this->sid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($sid, $pid) + { + $this->sid = $sid; + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $sid = $this->sid; + $status = StatusService::get($sid, false); + + if(!$status || !isset($status['account']) || !isset($status['account']['id'], $status['url'])) { + return; + } + + if(!in_array($status['pf_type'], ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + return; + } + + $ids = FollowerService::localFollowerIds($this->pid); + + if(!$ids || !count($ids)) { + return; + } + + $domain = strtolower(parse_url($status['url'], PHP_URL_HOST)); + $skipIds = []; + + if(strtolower(config('pixelfed.domain.app')) !== $domain) { + $skipIds = UserDomainBlock::where('domain', $domain)->pluck('profile_id')->toArray(); + } + + $filters = UserFilter::whereFilterableType('App\Profile') + ->whereFilterableId($status['account']['id']) + ->whereIn('filter_type', ['mute', 'block']) + ->pluck('user_id') + ->toArray(); + + if($filters && count($filters)) { + $skipIds = array_merge($skipIds, $filters); + } + + $skipIds = array_unique(array_values($skipIds)); + + foreach($ids as $id) { + if(!in_array($id, $skipIds)) { + HomeTimelineService::add($id, $this->sid); + } + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php b/app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php new file mode 100644 index 000000000..018ea3794 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php @@ -0,0 +1,98 @@ +pid . ':d-' . $this->domain; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:remove:domain:{$this->pid}:d-{$this->domain}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($pid, $domain) + { + $this->pid = $pid; + $this->domain = $domain; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if(!config('exp.cached_home_timeline')) { + return; + } + + if ($this->batch()->cancelled()) { + return; + } + + if(!$this->pid || !$this->domain) { + return; + } + $domain = strtolower($this->domain); + $pid = $this->pid; + $posts = HomeTimelineService::get($pid, '0', '-1'); + + foreach($posts as $post) { + $status = StatusService::get($post, false); + if(!$status || !isset($status['url'])) { + HomeTimelineService::rem($pid, $post); + continue; + } + $host = strtolower(parse_url($status['url'], PHP_URL_HOST)); + if($host === strtolower(config('pixelfed.domain.app')) || !$host) { + continue; + } + if($host === $domain) { + HomeTimelineService::rem($pid, $status['id']); + } + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php b/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php new file mode 100644 index 000000000..5c09d749a --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php @@ -0,0 +1,76 @@ +sid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:remove:sid:{$this->sid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($sid, $pid) + { + $this->sid = $sid; + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $ids = FollowerService::localFollowerIds($this->pid); + + HomeTimelineService::rem($this->pid, $this->sid); + + foreach($ids as $id) { + HomeTimelineService::rem($id, $this->sid); + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/FeedRemoveRemotePipeline.php b/app/Jobs/HomeFeedPipeline/FeedRemoveRemotePipeline.php new file mode 100644 index 000000000..d9ee716ba --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedRemoveRemotePipeline.php @@ -0,0 +1,74 @@ +sid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:remove:remote:sid:{$this->sid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($sid, $pid) + { + $this->sid = $sid; + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $ids = FollowerService::localFollowerIds($this->pid); + + foreach($ids as $id) { + HomeTimelineService::rem($id, $this->sid); + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/FeedUnfollowPipeline.php b/app/Jobs/HomeFeedPipeline/FeedUnfollowPipeline.php new file mode 100644 index 000000000..996e74c10 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedUnfollowPipeline.php @@ -0,0 +1,81 @@ +actorId . ':fid:' . $this->followingId; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:remove:follows:aid:{$this->actorId}:fid:{$this->followingId}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($actorId, $followingId) + { + $this->actorId = $actorId; + $this->followingId = $followingId; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $actorId = $this->actorId; + $followingId = $this->followingId; + + $ids = HomeTimelineService::get($actorId, 0, -1); + foreach($ids as $id) { + $status = StatusService::get($id, false); + if($status && isset($status['account'], $status['account']['id'])) { + if($status['account']['id'] == $followingId) { + HomeTimelineService::rem($actorId, $id); + } + } + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/FeedWarmCachePipeline.php b/app/Jobs/HomeFeedPipeline/FeedWarmCachePipeline.php new file mode 100644 index 000000000..00cdbda65 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedWarmCachePipeline.php @@ -0,0 +1,67 @@ +pid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hfp:warm-cache:pid:{$this->pid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($pid) + { + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $pid = $this->pid; + HomeTimelineService::warmCache($pid, true, 400, true); + } +} diff --git a/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php new file mode 100644 index 000000000..eca598e49 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php @@ -0,0 +1,116 @@ +hashtag->id; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hfp:hashtag:fanout:insert:{$this->hashtag->id}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct(StatusHashtag $hashtag) + { + $this->hashtag = $hashtag; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $hashtag = $this->hashtag; + $sid = $hashtag->status_id; + $status = StatusService::get($sid, false); + + if(!$status || !isset($status['account']) || !isset($status['account']['id'], $status['url'])) { + return; + } + + if(!in_array($status['pf_type'], ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + return; + } + + $domain = strtolower(parse_url($status['url'], PHP_URL_HOST)); + $skipIds = []; + + if(strtolower(config('pixelfed.domain.app')) !== $domain) { + $skipIds = UserDomainBlock::where('domain', $domain)->pluck('profile_id')->toArray(); + } + + $filters = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray(); + + if($filters && count($filters)) { + $skipIds = array_merge($skipIds, $filters); + } + + $skipIds = array_unique(array_values($skipIds)); + + $ids = HashtagFollowService::getPidByHid($hashtag->hashtag_id); + + if(!$ids || !count($ids)) { + return; + } + + foreach($ids as $id) { + if(!in_array($id, $skipIds)) { + HomeTimelineService::add($id, $hashtag->status_id); + } + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php new file mode 100644 index 000000000..f1968e120 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php @@ -0,0 +1,92 @@ +hid . ':' . $this->sid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hfp:hashtag:fanout:remove:{$this->hid}:{$this->sid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($sid, $hid) + { + $this->sid = $sid; + $this->hid = $hid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $sid = $this->sid; + $hid = $this->hid; + $status = StatusService::get($sid, false); + + if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { + return; + } + + if(!in_array($status['pf_type'], ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + return; + } + + $ids = HashtagFollowService::getPidByHid($hid); + + if(!$ids || !count($ids)) { + return; + } + + foreach($ids as $id) { + HomeTimelineService::rem($id, $sid); + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php new file mode 100644 index 000000000..232179ec3 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php @@ -0,0 +1,80 @@ +hid = $hid; + $this->pid = $pid; + $this->slug = $slug; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $hid = $this->hid; + $pid = $this->pid; + $slug = strtolower($this->slug); + + $statusIds = HomeTimelineService::get($pid, 0, -1); + + $followingIds = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { + $following = Follower::whereProfileId($pid)->pluck('following_id'); + return $following->push($pid)->toArray(); + }); + + foreach($statusIds as $id) { + $status = StatusService::get($id, false); + if(!$status || empty($status['tags'])) { + HomeTimelineService::rem($pid, $id); + continue; + } + $following = in_array((int) $status['account']['id'], $followingIds); + if($following === true) { + continue; + } + + $tags = collect($status['tags'])->map(function($tag) { + return strtolower($tag['name']); + })->filter()->values()->toArray(); + + if(in_array($slug, $tags)) { + HomeTimelineService::rem($pid, $id); + } + } + } +} diff --git a/app/Jobs/ImageOptimizePipeline/ImageOptimize.php b/app/Jobs/ImageOptimizePipeline/ImageOptimize.php index 0448ade6a..e2d558143 100644 --- a/app/Jobs/ImageOptimizePipeline/ImageOptimize.php +++ b/app/Jobs/ImageOptimizePipeline/ImageOptimize.php @@ -45,7 +45,7 @@ class ImageOptimize implements ShouldQueue return; } - if(config('pixelfed.optimize_image') == false) { + if((bool) config_cache('pixelfed.optimize_image') == false) { ImageThumbnail::dispatch($media)->onQueue('mmo'); return; } else { diff --git a/app/Jobs/ImageOptimizePipeline/ImageResize.php b/app/Jobs/ImageOptimizePipeline/ImageResize.php index 9bb896a40..2aa51a532 100644 --- a/app/Jobs/ImageOptimizePipeline/ImageResize.php +++ b/app/Jobs/ImageOptimizePipeline/ImageResize.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Log; class ImageResize implements ShouldQueue { @@ -46,10 +47,11 @@ class ImageResize implements ShouldQueue } $path = storage_path('app/'.$media->media_path); if (!is_file($path) || $media->skip_optimize) { + Log::info('Tried to optimize media that does not exist or is not readable. ' . $path); return; } - if(!config('pixelfed.optimize_image')) { + if((bool) config_cache('pixelfed.optimize_image') === false) { ImageThumbnail::dispatch($media)->onQueue('mmo'); return; } @@ -57,6 +59,7 @@ class ImageResize implements ShouldQueue $img = new Image(); $img->resizeImage($media); } catch (Exception $e) { + Log::error($e); } ImageThumbnail::dispatch($media)->onQueue('mmo'); diff --git a/app/Jobs/ImageOptimizePipeline/ImageUpdate.php b/app/Jobs/ImageOptimizePipeline/ImageUpdate.php index 550448699..9012529f2 100644 --- a/app/Jobs/ImageOptimizePipeline/ImageUpdate.php +++ b/app/Jobs/ImageOptimizePipeline/ImageUpdate.php @@ -61,7 +61,7 @@ class ImageUpdate implements ShouldQueue return; } - if(config('pixelfed.optimize_image')) { + if((bool) config_cache('pixelfed.optimize_image')) { if (in_array($media->mime, $this->protectedMimes) == true) { ImageOptimizer::optimize($thumb); if(!$media->skip_optimize) { diff --git a/app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php b/app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php new file mode 100644 index 000000000..cdf91e376 --- /dev/null +++ b/app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php @@ -0,0 +1,129 @@ +importPost->id; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("import-media-to-cloud-pipeline:ip-id:{$this->importPost->id}"))->shared()->dontRelease()]; + } + + /** + * Delete the job if its models no longer exist. + * + * @var bool + */ + public $deleteWhenMissingModels = true; + + /** + * Create a new job instance. + */ + public function __construct(ImportPost $importPost) + { + $this->importPost = $importPost; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $ip = $this->importPost; + + if( + $ip->status_id === null || + $ip->uploaded_to_s3 === true || + (bool) config_cache('pixelfed.cloud_storage') === false) { + return; + } + + $media = Media::whereStatusId($ip->status_id)->get(); + + if(!$media || !$media->count()) { + $importPost = ImportPost::find($ip->id); + $importPost->uploaded_to_s3 = true; + $importPost->save(); + return; + } + + foreach($media as $mediaPart) { + $this->handleMedia($mediaPart); + } + } + + protected function handleMedia($media) + { + $ip = $this->importPost; + + $importPost = ImportPost::find($ip->id); + + if(!$importPost) { + return; + } + + $res = MediaStorageService::move($media); + + $importPost->uploaded_to_s3 = true; + $importPost->save(); + + if(!$res) { + return; + } + + if($res === 'invalid file') { + return; + } + + if($res === 'success') { + if($media->mime === 'video/mp4') { + VideoThumbnailToCloudPipeline::dispatch($media)->onQueue('low'); + } else { + Storage::disk('local')->delete($media->media_path); + } + } + } +} diff --git a/app/Jobs/InboxPipeline/InboxValidator.php b/app/Jobs/InboxPipeline/InboxValidator.php index 4017d3acd..8d0f414c5 100644 --- a/app/Jobs/InboxPipeline/InboxValidator.php +++ b/app/Jobs/InboxPipeline/InboxValidator.php @@ -193,7 +193,7 @@ class InboxValidator implements ShouldQueue } try { - $res = Http::timeout(20)->withHeaders([ + $res = Http::withOptions(['allow_redirects' => false])->timeout(20)->withHeaders([ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', ])->get($actor->remote_url); diff --git a/app/Jobs/InboxPipeline/InboxWorker.php b/app/Jobs/InboxPipeline/InboxWorker.php index c8508c0fc..1bc88507d 100644 --- a/app/Jobs/InboxPipeline/InboxWorker.php +++ b/app/Jobs/InboxPipeline/InboxWorker.php @@ -173,7 +173,7 @@ class InboxWorker implements ShouldQueue } try { - $res = Http::timeout(20)->withHeaders([ + $res = Http::withOptions(['allow_redirects' => false])->timeout(20)->withHeaders([ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', ])->get($actor->remote_url); diff --git a/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php b/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php index b8c79d67f..38127b2aa 100644 --- a/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php +++ b/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php @@ -4,6 +4,7 @@ namespace App\Jobs\InstancePipeline; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; +use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; @@ -12,45 +13,73 @@ use Illuminate\Support\Facades\Http; use App\Instance; use App\Profile; use App\Services\NodeinfoService; +use Illuminate\Contracts\Cache\Repository; +use Illuminate\Support\Facades\Cache; -class FetchNodeinfoPipeline implements ShouldQueue +class FetchNodeinfoPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $instance; + protected $instance; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct(Instance $instance) - { - $this->instance = $instance; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct(Instance $instance) + { + $this->instance = $instance; + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $instance = $this->instance; + /** + * The number of seconds after which the job's unique lock will be released. + * + * @var int + */ + public $uniqueFor = 14400; - $ni = NodeinfoService::get($instance->domain); - if($ni) { - if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) { - $software = $ni['software']['name']; - $instance->software = strtolower(strip_tags($software)); - $instance->last_crawled_at = now(); - $instance->user_count = Profile::whereDomain($instance->domain)->count(); - $instance->save(); - } - } else { - $instance->user_count = Profile::whereDomain($instance->domain)->count(); - $instance->last_crawled_at = now(); - $instance->save(); - } - } + /** + * Get the unique ID for the job. + */ + public function uniqueId(): string + { + return $this->instance->id; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $instance = $this->instance; + + if( $instance->nodeinfo_last_fetched && + $instance->nodeinfo_last_fetched->gt(now()->subHours(12)) || + $instance->delivery_timeout && + $instance->delivery_next_after->gt(now()) + ) { + return; + } + + $ni = NodeinfoService::get($instance->domain); + $instance->last_crawled_at = now(); + if($ni) { + if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) { + $software = $ni['software']['name']; + $instance->software = strtolower(strip_tags($software)); + $instance->user_count = Profile::whereDomain($instance->domain)->count(); + $instance->nodeinfo_last_fetched = now(); + $instance->last_crawled_at = now(); + $instance->save(); + } + } else { + $instance->delivery_timeout = 1; + $instance->last_crawled_at = now(); + $instance->delivery_next_after = now()->addHours(14); + $instance->save(); + } + } } diff --git a/app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php b/app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php new file mode 100644 index 000000000..79df5aa9a --- /dev/null +++ b/app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php @@ -0,0 +1,76 @@ + + */ + public function middleware(): array + { + return [(new WithoutOverlapping('ip:notification-epoch-update'))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct() + { + // + } + + /** + * Execute the job. + */ + public function handle(): void + { + $pid = Cache::get(NotificationService::EPOCH_CACHE_KEY . '6'); + if($pid && $pid > 1) { + $rec = Notification::where('id', '>', $pid)->whereDate('created_at', now()->subMonths(6)->format('Y-m-d'))->first(); + } else { + $rec = Notification::whereDate('created_at', now()->subMonths(6)->format('Y-m-d'))->first(); + } + $id = 1; + if($rec) { + $id = $rec->id; + } + Cache::put(NotificationService::EPOCH_CACHE_KEY . '6', $id, 1209600); + } +} diff --git a/app/Jobs/LikePipeline/LikePipeline.php b/app/Jobs/LikePipeline/LikePipeline.php index b44c90c8b..7dbd71da2 100644 --- a/app/Jobs/LikePipeline/LikePipeline.php +++ b/app/Jobs/LikePipeline/LikePipeline.php @@ -2,19 +2,22 @@ namespace App\Jobs\LikePipeline; -use Cache, DB, Log; -use Illuminate\Support\Facades\Redis; -use App\{Like, Notification}; +use App\Jobs\PushNotificationPipeline\LikePushNotifyPipeline; +use App\Like; +use App\Notification; +use App\Services\NotificationAppGatewayService; +use App\Services\PushNotificationService; +use App\Services\StatusService; +use App\Transformer\ActivityPub\Verb\Like as LikeTransformer; +use App\User; +use App\Util\ActivityPub\Helpers; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use App\Util\ActivityPub\Helpers; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; -use App\Transformer\ActivityPub\Verb\Like as LikeTransformer; -use App\Services\StatusService; class LikePipeline implements ShouldQueue { @@ -30,6 +33,7 @@ class LikePipeline implements ShouldQueue public $deleteWhenMissingModels = true; public $timeout = 5; + public $tries = 1; /** @@ -54,41 +58,49 @@ class LikePipeline implements ShouldQueue $status = $this->like->status; $actor = $this->like->actor; - if (!$status) { + if (! $status) { // Ignore notifications to deleted statuses return; } - $status->likes_count = DB::table('likes')->whereStatusId($status->id)->count(); - $status->save(); - StatusService::refresh($status->id); - if($status->url && $actor->domain == null) { + if ($status->url && $actor->domain == null) { return $this->remoteLikeDeliver(); } $exists = Notification::whereProfileId($status->profile_id) - ->whereActorId($actor->id) - ->whereAction('like') - ->whereItemId($status->id) - ->whereItemType('App\Status') - ->count(); + ->whereActorId($actor->id) + ->whereAction('like') + ->whereItemId($status->id) + ->whereItemType('App\Status') + ->count(); - if ($actor->id === $status->profile_id || $exists !== 0) { + if ($actor->id === $status->profile_id || $exists) { return true; } - try { - $notification = new Notification(); - $notification->profile_id = $status->profile_id; - $notification->actor_id = $actor->id; - $notification->action = 'like'; - $notification->item_id = $status->id; - $notification->item_type = "App\Status"; - $notification->save(); + if ($status->uri === null && $status->object_url === null && $status->url === null) { + try { + $notification = new Notification; + $notification->profile_id = $status->profile_id; + $notification->actor_id = $actor->id; + $notification->action = 'like'; + $notification->item_id = $status->id; + $notification->item_type = "App\Status"; + $notification->save(); - } catch (Exception $e) { + } catch (Exception $e) { + } + + if (NotificationAppGatewayService::enabled()) { + if (PushNotificationService::check('like', $status->profile_id)) { + $user = User::whereProfileId($status->profile_id)->first(); + if ($user && $user->expo_token && $user->notify_enabled) { + LikePushNotifyPipeline::dispatchSync($user->expo_token, $actor->username); + } + } + } } } @@ -98,9 +110,9 @@ class LikePipeline implements ShouldQueue $status = $this->like->status; $actor = $this->like->actor; - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($like, new LikeTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($like, new LikeTransformer); $activity = $fractal->createData($resource)->toArray(); $url = $status->profile->sharedInbox ?? $status->profile->inbox_url; diff --git a/app/Jobs/MediaPipeline/MediaDeletePipeline.php b/app/Jobs/MediaPipeline/MediaDeletePipeline.php index 55df84948..df16a42d5 100644 --- a/app/Jobs/MediaPipeline/MediaDeletePipeline.php +++ b/app/Jobs/MediaPipeline/MediaDeletePipeline.php @@ -3,27 +3,30 @@ namespace App\Jobs\MediaPipeline; use App\Media; +use App\Services\Media\MediaHlsService; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Redis; -use Illuminate\Support\Facades\Storage; -use App\Services\Media\MediaHlsService; use Illuminate\Queue\Middleware\WithoutOverlapping; -use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Storage; -class MediaDeletePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing +class MediaDeletePipeline implements ShouldBeUniqueUntilProcessing, ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $media; + protected $media; public $timeout = 300; + public $tries = 3; + public $maxExceptions = 1; + public $failOnTimeout = true; + public $deleteWhenMissingModels = true; /** @@ -38,7 +41,7 @@ class MediaDeletePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing */ public function uniqueId(): string { - return 'media:purge-job:id-' . $this->media->id; + return 'media:purge-job:id-'.$this->media->id; } /** @@ -51,58 +54,58 @@ class MediaDeletePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing return [(new WithoutOverlapping("media:purge-job:id-{$this->media->id}"))->shared()->dontRelease()]; } - public function __construct(Media $media) - { - $this->media = $media; - } + public function __construct(Media $media) + { + $this->media = $media; + } - public function handle() - { - $media = $this->media; - $path = $media->media_path; - $thumb = $media->thumbnail_path; + public function handle() + { + $media = $this->media; + $path = $media->media_path; + $thumb = $media->thumbnail_path; - if(!$path) { - return 1; - } + if (! $path) { + return 1; + } - $e = explode('/', $path); - array_pop($e); - $i = implode('/', $e); + $e = explode('/', $path); + array_pop($e); + $i = implode('/', $e); - if(config_cache('pixelfed.cloud_storage') == true) { - $disk = Storage::disk(config('filesystems.cloud')); + if ((bool) config_cache('pixelfed.cloud_storage') == true) { + $disk = Storage::disk(config('filesystems.cloud')); - if($path && $disk->exists($path)) { - $disk->delete($path); - } + if ($path && $disk->exists($path)) { + $disk->delete($path); + } - if($thumb && $disk->exists($thumb)) { - $disk->delete($thumb); - } - } + if ($thumb && $disk->exists($thumb)) { + $disk->delete($thumb); + } + } - $disk = Storage::disk(config('filesystems.local')); + $disk = Storage::disk(config('filesystems.local')); - if($path && $disk->exists($path)) { - $disk->delete($path); - } + if ($path && $disk->exists($path)) { + $disk->delete($path); + } - if($thumb && $disk->exists($thumb)) { - $disk->delete($thumb); - } + if ($thumb && $disk->exists($thumb)) { + $disk->delete($thumb); + } - if($media->hls_path != null) { + if ($media->hls_path != null) { $files = MediaHlsService::allFiles($media); - if($files && count($files)) { - foreach($files as $file) { + if ($files && count($files)) { + foreach ($files as $file) { $disk->delete($file); } } - } + } - $media->delete(); + $media->delete(); - return 1; - } + return 1; + } } diff --git a/app/Jobs/MediaPipeline/MediaFixLocalFilesystemCleanupPipeline.php b/app/Jobs/MediaPipeline/MediaFixLocalFilesystemCleanupPipeline.php index bbd3851b9..a972f1f86 100644 --- a/app/Jobs/MediaPipeline/MediaFixLocalFilesystemCleanupPipeline.php +++ b/app/Jobs/MediaPipeline/MediaFixLocalFilesystemCleanupPipeline.php @@ -8,68 +8,69 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; class MediaFixLocalFilesystemCleanupPipeline implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public $timeout = 1800; - public $tries = 5; - public $maxExceptions = 1; + public $timeout = 1800; - public function handle() - { - if(config_cache('pixelfed.cloud_storage') == false) { - // Only run if cloud storage is enabled - return; - } + public $tries = 5; - $disk = Storage::disk('local'); - $cloud = Storage::disk(config('filesystems.cloud')); + public $maxExceptions = 1; - Media::whereNotNull(['status_id', 'cdn_url', 'replicated_at']) - ->chunk(20, function ($medias) use($disk, $cloud) { - foreach($medias as $media) { - if(!str_starts_with($media->media_path, 'public')) { - continue; - } + public function handle() + { + if ((bool) config_cache('pixelfed.cloud_storage') == false) { + // Only run if cloud storage is enabled + return; + } - if($disk->exists($media->media_path) && $cloud->exists($media->media_path)) { - $disk->delete($media->media_path); - } + $disk = Storage::disk('local'); + $cloud = Storage::disk(config('filesystems.cloud')); - if($media->thumbnail_path) { - if($disk->exists($media->thumbnail_path)) { - $disk->delete($media->thumbnail_path); - } - } + Media::whereNotNull(['status_id', 'cdn_url', 'replicated_at']) + ->chunk(20, function ($medias) use ($disk, $cloud) { + foreach ($medias as $media) { + if (! str_starts_with($media->media_path, 'public')) { + continue; + } - $paths = explode('/', $media->media_path); - if(count($paths) === 7) { - array_pop($paths); - $baseDir = implode('/', $paths); + if ($disk->exists($media->media_path) && $cloud->exists($media->media_path)) { + $disk->delete($media->media_path); + } - if(count($disk->allFiles($baseDir)) === 0) { - $disk->deleteDirectory($baseDir); + if ($media->thumbnail_path) { + if ($disk->exists($media->thumbnail_path)) { + $disk->delete($media->thumbnail_path); + } + } - array_pop($paths); - $baseDir = implode('/', $paths); + $paths = explode('/', $media->media_path); + if (count($paths) === 7) { + array_pop($paths); + $baseDir = implode('/', $paths); - if(count($disk->allFiles($baseDir)) === 0) { - $disk->deleteDirectory($baseDir); + if (count($disk->allFiles($baseDir)) === 0) { + $disk->deleteDirectory($baseDir); - array_pop($paths); - $baseDir = implode('/', $paths); + array_pop($paths); + $baseDir = implode('/', $paths); - if(count($disk->allFiles($baseDir)) === 0) { - $disk->deleteDirectory($baseDir); - } - } - } - } - } - }); - } + if (count($disk->allFiles($baseDir)) === 0) { + $disk->deleteDirectory($baseDir); + + array_pop($paths); + $baseDir = implode('/', $paths); + + if (count($disk->allFiles($baseDir)) === 0) { + $disk->deleteDirectory($baseDir); + } + } + } + } + } + }); + } } diff --git a/app/Jobs/MovePipeline/CleanupLegacyAccountMovePipeline.php b/app/Jobs/MovePipeline/CleanupLegacyAccountMovePipeline.php new file mode 100644 index 000000000..d26ad5624 --- /dev/null +++ b/app/Jobs/MovePipeline/CleanupLegacyAccountMovePipeline.php @@ -0,0 +1,103 @@ +target = $target; + $this->activity = $activity; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('process-move-cleanup-legacy-followers:'.$this->target), + (new ThrottlesExceptions(2, 5 * 60))->backoff(5), + ]; + } + + /** + * Determine the time at which the job should timeout. + */ + public function retryUntil(): DateTime + { + return now()->addMinutes(5); + } + + /** + * Execute the job. + */ + public function handle(): void + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + throw new Exception('Activitypub not enabled'); + } + + $target = $this->target; + $actor = $this->activity; + + $targetAccount = Helpers::profileFetch($target); + $actorAccount = Helpers::profileFetch($actor); + + if (! $targetAccount || ! $actorAccount) { + throw new Exception('Invalid move accounts'); + } + + UserFilter::where('filterable_type', 'App\Profile') + ->where('filterable_id', $actorAccount['id']) + ->update(['filterable_id' => $targetAccount['id']]); + + Follower::whereFollowingId($actorAccount['id'])->delete(); + + $oldProfile = Profile::find($actorAccount['id']); + + if ($oldProfile) { + $oldProfile->moved_to_profile_id = $targetAccount['id']; + $oldProfile->save(); + AccountService::del($oldProfile->id); + AccountService::del($targetAccount['id']); + } + } +} diff --git a/app/Jobs/MovePipeline/MoveMigrateFollowersPipeline.php b/app/Jobs/MovePipeline/MoveMigrateFollowersPipeline.php new file mode 100644 index 000000000..1cde3818e --- /dev/null +++ b/app/Jobs/MovePipeline/MoveMigrateFollowersPipeline.php @@ -0,0 +1,131 @@ +target = $target; + $this->activity = $activity; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('process-move-migrate-followers:'.$this->target), + (new ThrottlesExceptionsWithRedis(5, 2 * 60))->backoff(1), + ]; + } + + /** + * Determine the time at which the job should timeout. + */ + public function retryUntil(): DateTime + { + return now()->addMinutes(15); + } + + /** + * Execute the job. + */ + public function handle(): void + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + throw new Exception('Activitypub not enabled'); + } + + $target = $this->target; + $actor = $this->activity; + + $targetAccount = Helpers::profileFetch($target); + $actorAccount = Helpers::profileFetch($actor); + + if (! $targetAccount || ! $actorAccount) { + throw new Exception('Invalid move accounts'); + } + + $activity = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Follow', + 'actor' => null, + 'object' => $target, + ]; + + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + $addlHeaders = [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]; + $targetInbox = $targetAccount['sharedInbox'] ?? $targetAccount['inbox_url']; + $targetPid = $targetAccount['id']; + + DB::table('followers') + ->join('profiles', 'followers.profile_id', '=', 'profiles.id') + ->where('followers.following_id', $actorAccount['id']) + ->whereNotNull('profiles.user_id') + ->whereNull('profiles.deleted_at') + ->select('profiles.id', 'profiles.user_id', 'profiles.username', 'profiles.private_key', 'profiles.status') + ->chunkById(100, function ($followers) use ($targetInbox, $targetPid, $target) { + foreach ($followers as $follower) { + if (! $follower->private_key || ! $follower->username || ! $follower->user_id || $follower->status === 'delete') { + continue; + } + + Follower::updateOrCreate([ + 'profile_id' => $follower->id, + 'following_id' => $targetPid, + ]); + + MoveSendFollowPipeline::dispatch($follower, $targetInbox, $targetPid, $target)->onQueue('follow'); + } + }, 'id'); + } +} diff --git a/app/Jobs/MovePipeline/MoveSendFollowPipeline.php b/app/Jobs/MovePipeline/MoveSendFollowPipeline.php new file mode 100644 index 000000000..6d1cef5e1 --- /dev/null +++ b/app/Jobs/MovePipeline/MoveSendFollowPipeline.php @@ -0,0 +1,113 @@ + + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('move-send-follow:'.$this->follower->id.':target:'.$this->target), + (new ThrottlesExceptions(2, 5 * 60))->backoff(5), + ]; + } + + /** + * Create a new job instance. + */ + public function __construct($follower, $targetInbox, $targetPid, $target) + { + $this->follower = $follower; + $this->targetInbox = $targetInbox; + $this->targetPid = $targetPid; + $this->target = $target; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $follower = $this->follower; + $targetPid = $this->targetPid; + $targetInbox = $this->targetInbox; + $target = $this->target; + + if (! $follower->username || ! $follower->private_key) { + return; + } + + $permalink = 'https://'.config('pixelfed.domain.app').'/users/'.$follower->username; + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + $addlHeaders = [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]; + + $activity = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Follow', + 'actor' => $permalink, + 'object' => $target, + ]; + + $keyId = $permalink.'#main-key'; + $payload = json_encode($activity); + $headers = HttpSignature::signRaw($follower->private_key, $keyId, $targetInbox, $activity, $addlHeaders); + + $client = new Client([ + 'timeout' => config('federation.activitypub.delivery.timeout'), + ]); + + try { + $client->post($targetInbox, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true, + ], + ]); + } catch (ClientException $e) { + + } + } +} diff --git a/app/Jobs/MovePipeline/MoveSendUndoFollowPipeline.php b/app/Jobs/MovePipeline/MoveSendUndoFollowPipeline.php new file mode 100644 index 000000000..952e8c1fa --- /dev/null +++ b/app/Jobs/MovePipeline/MoveSendUndoFollowPipeline.php @@ -0,0 +1,119 @@ + + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('move-send-unfollow:'.$this->follower->id.':actor:'.$this->actor), + (new ThrottlesExceptions(2, 5 * 60))->backoff(5), + ]; + } + + /** + * Create a new job instance. + */ + public function __construct($follower, $targetInbox, $targetPid, $actor) + { + $this->follower = $follower; + $this->targetInbox = $targetInbox; + $this->targetPid = $targetPid; + $this->actor = $actor; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $follower = $this->follower; + $targetPid = $this->targetPid; + $targetInbox = $this->targetInbox; + $actor = $this->actor; + + if (! $follower->username || ! $follower->private_key) { + return; + } + + $permalink = 'https://'.config('pixelfed.domain.app').'/users/'.$follower->username; + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + $addlHeaders = [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]; + + $activity = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Undo', + 'id' => $permalink.'#follow/'.$targetPid.'/undo', + 'actor' => $permalink, + 'object' => [ + 'type' => 'Follow', + 'id' => $permalink.'#follows/'.$targetPid, + 'object' => $actor, + 'actor' => $permalink, + ], + ]; + + $keyId = $permalink.'#main-key'; + $payload = json_encode($activity); + $headers = HttpSignature::signRaw($follower->private_key, $keyId, $targetInbox, $activity, $addlHeaders); + + $client = new Client([ + 'timeout' => config('federation.activitypub.delivery.timeout'), + ]); + + try { + $client->post($targetInbox, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true, + ], + ]); + } catch (ClientException $e) { + + } + } +} diff --git a/app/Jobs/MovePipeline/ProcessMovePipeline.php b/app/Jobs/MovePipeline/ProcessMovePipeline.php new file mode 100644 index 000000000..1ff95f96c --- /dev/null +++ b/app/Jobs/MovePipeline/ProcessMovePipeline.php @@ -0,0 +1,156 @@ +target = $target; + $this->activity = $activity; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('process-move:'.$this->target), + (new ThrottlesExceptionsWithRedis(5, 2 * 60))->backoff(1), + ]; + } + + /** + * Determine the time at which the job should timeout. + */ + public function retryUntil(): DateTime + { + return now()->addMinutes(10); + } + + /** + * Execute the job. + */ + public function handle(): void + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + throw new Exception('Activitypub not enabled'); + } + + $validTarget = $this->checkTarget(); + if (! $validTarget) { + throw new Exception('Invalid target'); + } + + $validActor = $this->checkActor(); + if (! $validActor) { + throw new Exception('Invalid actor'); + } + + } + + protected function checkTarget() + { + $fetchTargetUrl = $this->target.'?cb='.time(); + $res = ActivityPubFetchService::fetchRequest($fetchTargetUrl, true); + + if (! $res || ! isset($res['alsoKnownAs'])) { + return false; + } + + $targetRes = Helpers::profileFetch($this->target); + if (! $targetRes) { + return false; + } + + if (is_string($res['alsoKnownAs'])) { + return $this->lowerTrim($res['alsoKnownAs']) === $this->lowerTrim($this->activity); + } + + if (is_array($res['alsoKnownAs'])) { + $map = Arr::map($res['alsoKnownAs'], function ($value, $key) { + return trim(strtolower($value)); + }); + + $res = in_array($this->activity, $map); + + return $res; + } + + return false; + } + + protected function checkActor() + { + $fetchActivityUrl = $this->activity.'?cb='.time(); + $res = ActivityPubFetchService::fetchRequest($fetchActivityUrl, true); + + if (! $res || ! isset($res['movedTo']) || empty($res['movedTo'])) { + return false; + } + + $actorRes = Helpers::profileFetch($this->activity); + if (! $actorRes) { + return false; + } + + if (is_string($res['movedTo'])) { + $match = $this->lowerTrim($res['movedTo']) === $this->lowerTrim($this->target); + if (! $match) { + return false; + } + + return $match; + } + + return false; + } + + protected function lowerTrim($str) + { + return trim(strtolower($str)); + } +} diff --git a/app/Jobs/MovePipeline/UnfollowLegacyAccountMovePipeline.php b/app/Jobs/MovePipeline/UnfollowLegacyAccountMovePipeline.php new file mode 100644 index 000000000..47ed2aeb6 --- /dev/null +++ b/app/Jobs/MovePipeline/UnfollowLegacyAccountMovePipeline.php @@ -0,0 +1,111 @@ +target = $target; + $this->activity = $activity; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [ + new WithoutOverlapping('process-move-undo-legacy-followers:'.$this->target), + (new ThrottlesExceptions(2, 5 * 60))->backoff(5), + ]; + } + + /** + * Determine the time at which the job should timeout. + */ + public function retryUntil(): DateTime + { + return now()->addMinutes(5); + } + + /** + * Execute the job. + */ + public function handle(): void + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + throw new Exception('Activitypub not enabled'); + } + + $target = $this->target; + $actor = $this->activity; + + $targetAccount = Helpers::profileFetch($target); + $actorAccount = Helpers::profileFetch($actor); + + if (! $targetAccount || ! $actorAccount) { + throw new Exception('Invalid move accounts'); + } + + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + $addlHeaders = [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]; + $targetInbox = $actorAccount['sharedInbox'] ?? $actorAccount['inbox_url']; + $targetPid = $actorAccount['id']; + + DB::table('followers') + ->join('profiles', 'followers.profile_id', '=', 'profiles.id') + ->where('followers.following_id', $actorAccount['id']) + ->whereNotNull('profiles.user_id') + ->whereNull('profiles.deleted_at') + ->select('profiles.id', 'profiles.user_id', 'profiles.username', 'profiles.private_key', 'profiles.status') + ->chunkById(100, function ($followers) use ($actor, $targetInbox, $targetPid) { + foreach ($followers as $follower) { + if (! $follower->id || ! $follower->private_key || ! $follower->username || ! $follower->user_id || $follower->status === 'delete') { + continue; + } + + MoveSendUndoFollowPipeline::dispatch($follower, $targetInbox, $targetPid, $actor)->onQueue('move'); + } + }, 'id'); + } +} diff --git a/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php b/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php new file mode 100644 index 000000000..a67f4e444 --- /dev/null +++ b/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php @@ -0,0 +1,38 @@ +pc = $pc; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $pc = $this->pc; + + Mail::to($pc->email)->send(new ParentChildInvite($pc)); + } +} diff --git a/app/Jobs/ProfilePipeline/DecrementPostCount.php b/app/Jobs/ProfilePipeline/DecrementPostCount.php index b463f1dda..74d0523b5 100644 --- a/app/Jobs/ProfilePipeline/DecrementPostCount.php +++ b/app/Jobs/ProfilePipeline/DecrementPostCount.php @@ -35,18 +35,7 @@ class DecrementPostCount implements ShouldQueue */ public function handle() { - $id = $this->id; - - $profile = Profile::find($id); - - if(!$profile) { - return 1; - } - - $profile->status_count = $profile->status_count ? $profile->status_count - 1 : 0; - $profile->save(); - AccountService::del($id); - - return 1; + // deprecated + return; } } diff --git a/app/Jobs/ProfilePipeline/IncrementPostCount.php b/app/Jobs/ProfilePipeline/IncrementPostCount.php index fe8d90648..a1f9ceca7 100644 --- a/app/Jobs/ProfilePipeline/IncrementPostCount.php +++ b/app/Jobs/ProfilePipeline/IncrementPostCount.php @@ -8,6 +8,8 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\Middleware\WithoutOverlapping; +use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use App\Profile; use App\Status; use App\Services\AccountService; @@ -35,19 +37,7 @@ class IncrementPostCount implements ShouldQueue */ public function handle() { - $id = $this->id; - - $profile = Profile::find($id); - - if(!$profile) { - return 1; - } - - $profile->status_count = $profile->status_count + 1; - $profile->last_status_at = now(); - $profile->save(); - AccountService::del($id); - - return 1; + // deprecated + return; } } diff --git a/app/Jobs/ProfilePipeline/ProfileMigrationDeliverMoveActivityPipeline.php b/app/Jobs/ProfilePipeline/ProfileMigrationDeliverMoveActivityPipeline.php new file mode 100644 index 000000000..7b8e15c03 --- /dev/null +++ b/app/Jobs/ProfilePipeline/ProfileMigrationDeliverMoveActivityPipeline.php @@ -0,0 +1,140 @@ +migration->id; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping('profile:migration:deliver-move-followers:id:'.$this->migration->id))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($migration, $oldAccount, $newAccount) + { + $this->migration = $migration; + $this->oldAccount = $oldAccount; + $this->newAccount = $newAccount; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if ($this->batch()->cancelled()) { + return; + } + + $migration = $this->migration; + $profile = $this->oldAccount; + $newAccount = $this->newAccount; + + if ($profile->domain || ! $profile->private_key) { + return; + } + + $audience = $profile->getAudienceInbox(); + $activitypubObject = new Move(); + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($migration, $activitypubObject); + $activity = $fractal->createData($resource)->toArray(); + + $payload = json_encode($activity); + + $client = new Client([ + 'timeout' => config('federation.activitypub.delivery.timeout'), + ]); + + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + + $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) { + foreach ($audience as $url) { + $headers = HttpSignature::sign($profile, $url, $activity, [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]); + yield function () use ($client, $url, $headers, $payload) { + return $client->postAsync($url, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + ], + ]); + }; + } + }; + + $pool = new Pool($client, $requests($audience), [ + 'concurrency' => config('federation.activitypub.delivery.concurrency'), + 'fulfilled' => function ($response, $index) { + }, + 'rejected' => function ($reason, $index) { + }, + ]); + + $promise = $pool->promise(); + + $promise->wait(); + } +} diff --git a/app/Jobs/ProfilePipeline/ProfileMigrationMoveFollowersPipeline.php b/app/Jobs/ProfilePipeline/ProfileMigrationMoveFollowersPipeline.php new file mode 100644 index 000000000..c3d825ec9 --- /dev/null +++ b/app/Jobs/ProfilePipeline/ProfileMigrationMoveFollowersPipeline.php @@ -0,0 +1,95 @@ +oldPid.':newpid-'.$this->newPid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping('profile:migration:move-followers:oldpid-'.$this->oldPid.':newpid-'.$this->newPid))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($oldPid, $newPid) + { + $this->oldPid = $oldPid; + $this->newPid = $newPid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if ($this->batch()->cancelled()) { + return; + } + $og = Profile::find($this->oldPid); + $ne = Profile::find($this->newPid); + if (! $og || ! $ne || $og == $ne) { + return; + } + $ne->followers_count = $og->followers_count; + $ne->save(); + $og->followers_count = 0; + $og->save(); + foreach (Follower::whereFollowingId($this->oldPid)->lazyById(200, 'id') as $follower) { + try { + $follower->following_id = $this->newPid; + $follower->save(); + } catch (Exception $e) { + $follower->delete(); + } + } + AccountService::del($this->oldPid); + AccountService::del($this->newPid); + } +} diff --git a/app/Jobs/ProfilePipeline/ProfilePurgeFollowersByDomain.php b/app/Jobs/ProfilePipeline/ProfilePurgeFollowersByDomain.php new file mode 100644 index 000000000..24fcdc832 --- /dev/null +++ b/app/Jobs/ProfilePipeline/ProfilePurgeFollowersByDomain.php @@ -0,0 +1,119 @@ +pid . ':d-' . $this->domain; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("followers:v1:purge-by-domain:{$this->pid}:d-{$this->domain}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($pid, $domain) + { + $this->pid = $pid; + $this->domain = $domain; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if ($this->batch()->cancelled()) { + return; + } + + $pid = $this->pid; + $domain = $this->domain; + + $query = 'SELECT f.* + FROM followers f + JOIN profiles p ON p.id = f.profile_id OR p.id = f.following_id + WHERE (f.profile_id = ? OR f.following_id = ?) + AND p.domain = ?;'; + $params = [$pid, $pid, $domain]; + + foreach(DB::cursor($query, $params) as $n) { + if(!$n || !$n->id) { + continue; + } + $follower = Follower::find($n->id); + if($follower->following_id == $pid && $follower->profile_id) { + FollowerService::remove($follower->profile_id, $pid, true); + $follower->delete(); + } else if ($follower->profile_id == $pid && $follower->following_id) { + FollowerService::remove($follower->following_id, $pid, true); + $follower->delete(); + } + } + + $profile = Profile::find($pid); + + $followerCount = DB::table('profiles') + ->join('followers', 'profiles.id', '=', 'followers.following_id') + ->where('followers.following_id', $pid) + ->count(); + + $followingCount = DB::table('profiles') + ->join('followers', 'profiles.id', '=', 'followers.following_id') + ->where('followers.profile_id', $pid) + ->count(); + + $profile->followers_count = $followerCount; + $profile->following_count = $followingCount; + $profile->save(); + + AccountService::del($profile->id); + } +} diff --git a/app/Jobs/ProfilePipeline/ProfilePurgeNotificationsByDomain.php b/app/Jobs/ProfilePipeline/ProfilePurgeNotificationsByDomain.php new file mode 100644 index 000000000..ea5a45e4a --- /dev/null +++ b/app/Jobs/ProfilePipeline/ProfilePurgeNotificationsByDomain.php @@ -0,0 +1,91 @@ +pid . ':d-' . $this->domain; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("notify:v1:purge-by-domain:{$this->pid}:d-{$this->domain}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($pid, $domain) + { + $this->pid = $pid; + $this->domain = $domain; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if ($this->batch()->cancelled()) { + return; + } + + $pid = $this->pid; + $domain = $this->domain; + + $query = 'SELECT notifications.* + FROM profiles + JOIN notifications on profiles.id = notifications.actor_id + WHERE notifications.profile_id = ? + AND profiles.domain = ?'; + $params = [$pid, $domain]; + + foreach(DB::cursor($query, $params) as $n) { + if(!$n || !$n->id) { + continue; + } + Notification::where('id', $n->id)->delete(); + NotificationService::del($pid, $n->id); + } + } +} diff --git a/app/Jobs/PushNotificationPipeline/FollowPushNotifyPipeline.php b/app/Jobs/PushNotificationPipeline/FollowPushNotifyPipeline.php new file mode 100644 index 000000000..ee286f5f2 --- /dev/null +++ b/app/Jobs/PushNotificationPipeline/FollowPushNotifyPipeline.php @@ -0,0 +1,38 @@ +pushToken = $pushToken; + $this->actor = $actor; + } + + /** + * Execute the job. + */ + public function handle(): void + { + try { + NotificationAppGatewayService::send($this->pushToken, 'follow', $this->actor); + } catch (Exception $e) { + return; + } + } +} diff --git a/app/Jobs/PushNotificationPipeline/LikePushNotifyPipeline.php b/app/Jobs/PushNotificationPipeline/LikePushNotifyPipeline.php new file mode 100644 index 000000000..892624b5a --- /dev/null +++ b/app/Jobs/PushNotificationPipeline/LikePushNotifyPipeline.php @@ -0,0 +1,38 @@ +pushToken = $pushToken; + $this->actor = $actor; + } + + /** + * Execute the job. + */ + public function handle(): void + { + try { + NotificationAppGatewayService::send($this->pushToken, 'like', $this->actor); + } catch (Exception $e) { + return; + } + } +} diff --git a/app/Jobs/PushNotificationPipeline/MentionPushNotifyPipeline.php b/app/Jobs/PushNotificationPipeline/MentionPushNotifyPipeline.php new file mode 100644 index 000000000..cad8c6fb5 --- /dev/null +++ b/app/Jobs/PushNotificationPipeline/MentionPushNotifyPipeline.php @@ -0,0 +1,38 @@ +pushToken = $pushToken; + $this->actor = $actor; + } + + /** + * Execute the job. + */ + public function handle(): void + { + try { + NotificationAppGatewayService::send($this->pushToken, 'mention', $this->actor); + } catch (Exception $e) { + return; + } + } +} diff --git a/app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php b/app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php index 5b413ecc1..394c2cfb8 100644 --- a/app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php +++ b/app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php @@ -17,6 +17,7 @@ use Log; use Storage; use Zttp\Zttp; use App\Util\ActivityPub\Helpers; +use App\Services\MediaPathService; class RemoteFollowImportRecent implements ShouldQueue { @@ -45,7 +46,6 @@ class RemoteFollowImportRecent implements ShouldQueue 'image/jpg', 'image/jpeg', 'image/png', - 'image/gif', ]; } @@ -208,9 +208,7 @@ class RemoteFollowImportRecent implements ShouldQueue public function importMedia($url, $mime, $status) { $user = $this->profile; - $monthHash = hash('sha1', date('Y').date('m')); - $userHash = hash('sha1', $user->id.(string) $user->created_at); - $storagePath = "public/m/{$monthHash}/{$userHash}"; + $storagePath = MediaPathService::get($user, 2); try { $info = pathinfo($url); diff --git a/app/Jobs/SharePipeline/SharePipeline.php b/app/Jobs/SharePipeline/SharePipeline.php index ae184957e..734c44231 100644 --- a/app/Jobs/SharePipeline/SharePipeline.php +++ b/app/Jobs/SharePipeline/SharePipeline.php @@ -2,9 +2,15 @@ namespace App\Jobs\SharePipeline; -use Cache, Log; -use Illuminate\Support\Facades\Redis; -use App\{Status, Notification}; +use App\Jobs\HomeFeedPipeline\FeedInsertPipeline; +use App\Notification; +use App\Services\ReblogService; +use App\Services\StatusService; +use App\Status; +use App\Transformer\ActivityPub\Verb\Announce; +use App\Util\ActivityPub\HttpSignature; +use GuzzleHttp\Client; +use GuzzleHttp\Pool; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -12,138 +18,136 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; -use App\Transformer\ActivityPub\Verb\Announce; -use GuzzleHttp\{Pool, Client, Promise}; -use App\Util\ActivityPub\HttpSignature; -use App\Services\ReblogService; -use App\Services\StatusService; class SharePipeline implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $status; + protected $status; - /** - * Delete the job if its models no longer exist. - * - * @var bool - */ - public $deleteWhenMissingModels = true; + /** + * Delete the job if its models no longer exist. + * + * @var bool + */ + public $deleteWhenMissingModels = true; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct(Status $status) - { - $this->status = $status; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct(Status $status) + { + $this->status = $status; + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $status = $this->status; - $parent = Status::find($this->status->reblog_of_id); - if(!$parent) { + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $status = $this->status; + $parent = Status::find($this->status->reblog_of_id); + if (! $parent) { return; } - $actor = $status->profile; - $target = $parent->profile; + $actor = $status->profile; + $target = $parent->profile; - if ($status->uri !== null) { - // Ignore notifications to remote statuses - return; - } + if ($status->uri !== null) { + // Ignore notifications to remote statuses + return; + } - if($target->id === $status->profile_id) { - $this->remoteAnnounceDeliver(); - return true; - } + if ($target->id === $status->profile_id) { + $this->remoteAnnounceDeliver(); - ReblogService::addPostReblog($parent->profile_id, $status->id); + return true; + } - $parent->reblogs_count = $parent->reblogs_count + 1; - $parent->save(); - StatusService::del($parent->id); + ReblogService::addPostReblog($parent->profile_id, $status->id); - Notification::firstOrCreate( - [ - 'profile_id' => $target->id, - 'actor_id' => $actor->id, - 'action' => 'share', - 'item_type' => 'App\Status', - 'item_id' => $status->reblog_of_id ?? $status->id, - ] - ); + $parent->reblogs_count = $parent->reblogs_count + 1; + $parent->save(); + StatusService::del($parent->id); - return $this->remoteAnnounceDeliver(); - } + Notification::firstOrCreate( + [ + 'profile_id' => $target->id, + 'actor_id' => $actor->id, + 'action' => 'share', + 'item_type' => 'App\Status', + 'item_id' => $status->reblog_of_id ?? $status->id, + ] + ); - public function remoteAnnounceDeliver() - { - if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) { - return true; - } - $status = $this->status; - $profile = $status->profile; + FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($status, new Announce()); - $activity = $fractal->createData($resource)->toArray(); + return $this->remoteAnnounceDeliver(); + } - $audience = $status->profile->getAudienceInbox(); + public function remoteAnnounceDeliver() + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + return true; + } + $status = $this->status; + $profile = $status->profile; - if(empty($audience) || $status->scope != 'public') { - // Return on profiles with no remote followers - return; - } + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($status, new Announce()); + $activity = $fractal->createData($resource)->toArray(); - $payload = json_encode($activity); + $audience = $status->profile->getAudienceInbox(); - $client = new Client([ - 'timeout' => config('federation.activitypub.delivery.timeout') - ]); + if (empty($audience) || $status->scope != 'public') { + // Return on profiles with no remote followers + return; + } - $version = config('pixelfed.version'); - $appUrl = config('app.url'); - $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + $payload = json_encode($activity); - $requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) { - foreach($audience as $url) { - $headers = HttpSignature::sign($profile, $url, $activity, [ - 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'User-Agent' => $userAgent, - ]); - yield function() use ($client, $url, $headers, $payload) { - return $client->postAsync($url, [ - 'curl' => [ - CURLOPT_HTTPHEADER => $headers, - CURLOPT_POSTFIELDS => $payload, - CURLOPT_HEADER => true - ] - ]); - }; - } - }; + $client = new Client([ + 'timeout' => config('federation.activitypub.delivery.timeout'), + ]); - $pool = new Pool($client, $requests($audience), [ - 'concurrency' => config('federation.activitypub.delivery.concurrency'), - 'fulfilled' => function ($response, $index) { - }, - 'rejected' => function ($reason, $index) { - } - ]); + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; - $promise = $pool->promise(); + $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) { + foreach ($audience as $url) { + $headers = HttpSignature::sign($profile, $url, $activity, [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]); + yield function () use ($client, $url, $headers, $payload) { + return $client->postAsync($url, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true, + ], + ]); + }; + } + }; - $promise->wait(); + $pool = new Pool($client, $requests($audience), [ + 'concurrency' => config('federation.activitypub.delivery.concurrency'), + 'fulfilled' => function ($response, $index) { + }, + 'rejected' => function ($reason, $index) { + }, + ]); - } + $promise = $pool->promise(); + + $promise->wait(); + + } } diff --git a/app/Jobs/SharePipeline/UndoSharePipeline.php b/app/Jobs/SharePipeline/UndoSharePipeline.php index 3850a4752..af3239953 100644 --- a/app/Jobs/SharePipeline/UndoSharePipeline.php +++ b/app/Jobs/SharePipeline/UndoSharePipeline.php @@ -2,9 +2,15 @@ namespace App\Jobs\SharePipeline; -use Cache, Log; -use Illuminate\Support\Facades\Redis; -use App\{Status, Notification}; +use App\Jobs\HomeFeedPipeline\FeedRemovePipeline; +use App\Notification; +use App\Services\ReblogService; +use App\Services\StatusService; +use App\Status; +use App\Transformer\ActivityPub\Verb\UndoAnnounce; +use App\Util\ActivityPub\HttpSignature; +use GuzzleHttp\Client; +use GuzzleHttp\Pool; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -12,125 +18,125 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; -use App\Transformer\ActivityPub\Verb\UndoAnnounce; -use GuzzleHttp\{Pool, Client, Promise}; -use App\Util\ActivityPub\HttpSignature; -use App\Services\ReblogService; -use App\Services\StatusService; class UndoSharePipeline implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $status; - public $deleteWhenMissingModels = true; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(Status $status) - { - $this->status = $status; - } + protected $status; - public function handle() - { - $status = $this->status; - $actor = $status->profile; - $parent = Status::find($status->reblog_of_id); + public $deleteWhenMissingModels = true; - if($parent) { - $target = $parent->profile_id; - ReblogService::removePostReblog($parent->profile_id, $status->id); + public function __construct(Status $status) + { + $this->status = $status; + } - if($parent->reblogs_count > 0) { - $parent->reblogs_count = $parent->reblogs_count - 1; - $parent->save(); - StatusService::del($parent->id); - } + public function handle() + { + $status = $this->status; + $actor = $status->profile; + $parent = Status::find($status->reblog_of_id); - $notification = Notification::whereProfileId($target) - ->whereActorId($status->profile_id) - ->whereAction('share') - ->whereItemId($status->reblog_of_id) - ->whereItemType('App\Status') - ->first(); + FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); - if($notification) { - $notification->forceDelete(); - } - } + if ($parent) { + $target = $parent->profile_id; + ReblogService::removePostReblog($parent->profile_id, $status->id); - if ($status->uri != null) { - return; - } + if ($parent->reblogs_count > 0) { + $parent->reblogs_count = $parent->reblogs_count - 1; + $parent->save(); + StatusService::del($parent->id); + } - if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) { - return $status->delete(); - } else { - return $this->remoteAnnounceDeliver(); - } - } + $notification = Notification::whereProfileId($target) + ->whereActorId($status->profile_id) + ->whereAction('share') + ->whereItemId($status->reblog_of_id) + ->whereItemType('App\Status') + ->first(); - public function remoteAnnounceDeliver() - { - if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) { + if ($notification) { + $notification->forceDelete(); + } + } + + if ($status->uri != null) { + return; + } + + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + return $status->delete(); + } else { + return $this->remoteAnnounceDeliver(); + } + } + + public function remoteAnnounceDeliver() + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { $status->delete(); - return 1; - } - $status = $this->status; - $profile = $status->profile; + return 1; + } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($status, new UndoAnnounce()); - $activity = $fractal->createData($resource)->toArray(); + $status = $this->status; + $profile = $status->profile; - $audience = $status->profile->getAudienceInbox(); + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($status, new UndoAnnounce()); + $activity = $fractal->createData($resource)->toArray(); - if(empty($audience) || $status->scope != 'public') { - return 1; - } + $audience = $status->profile->getAudienceInbox(); - $payload = json_encode($activity); + if (empty($audience) || $status->scope != 'public') { + return 1; + } - $client = new Client([ - 'timeout' => config('federation.activitypub.delivery.timeout') - ]); + $payload = json_encode($activity); - $version = config('pixelfed.version'); - $appUrl = config('app.url'); - $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + $client = new Client([ + 'timeout' => config('federation.activitypub.delivery.timeout'), + ]); - $requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) { - foreach($audience as $url) { - $headers = HttpSignature::sign($profile, $url, $activity, [ - 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'User-Agent' => $userAgent, - ]); - yield function() use ($client, $url, $headers, $payload) { - return $client->postAsync($url, [ - 'curl' => [ - CURLOPT_HTTPHEADER => $headers, - CURLOPT_POSTFIELDS => $payload, - CURLOPT_HEADER => true - ] - ]); - }; - } - }; + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; - $pool = new Pool($client, $requests($audience), [ - 'concurrency' => config('federation.activitypub.delivery.concurrency'), - 'fulfilled' => function ($response, $index) { - }, - 'rejected' => function ($reason, $index) { - } - ]); + $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) { + foreach ($audience as $url) { + $headers = HttpSignature::sign($profile, $url, $activity, [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]); + yield function () use ($client, $url, $headers, $payload) { + return $client->postAsync($url, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true, + ], + ]); + }; + } + }; - $promise = $pool->promise(); + $pool = new Pool($client, $requests($audience), [ + 'concurrency' => config('federation.activitypub.delivery.concurrency'), + 'fulfilled' => function ($response, $index) { + }, + 'rejected' => function ($reason, $index) { + }, + ]); - $promise->wait(); + $promise = $pool->promise(); - $status->delete(); + $promise->wait(); - return 1; - } + $status->delete(); + + return 1; + } } diff --git a/app/Jobs/StatusPipeline/RemoteStatusDelete.php b/app/Jobs/StatusPipeline/RemoteStatusDelete.php index aabb81755..65b72c104 100644 --- a/app/Jobs/StatusPipeline/RemoteStatusDelete.php +++ b/app/Jobs/StatusPipeline/RemoteStatusDelete.php @@ -39,7 +39,8 @@ use App\Services\AccountService; use App\Services\CollectionService; use App\Services\StatusService; use App\Jobs\MediaPipeline\MediaDeletePipeline; -use App\Jobs\ProfilePipeline\DecrementPostCount; +use App\Services\NotificationService; +use App\Services\Account\AccountStatService; class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing { @@ -108,9 +109,7 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing } StatusService::del($status->id, true); - - DecrementPostCount::dispatch($status->profile_id)->onQueue('inbox'); - + // AccountStatService::decrementPostCount($status->profile_id); return $this->unlinkRemoveMedia($status); } @@ -137,14 +136,34 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing CollectionService::removeItem($col->collection_id, $col->object_id); $col->delete(); }); - DirectMessage::whereStatusId($status->id)->delete(); + $dms = DirectMessage::whereStatusId($status->id)->get(); + foreach($dms as $dm) { + $not = Notification::whereItemType('App\DirectMessage') + ->whereItemId($dm->id) + ->first(); + if($not) { + NotificationService::del($not->profile_id, $not->id); + $not->forceDeleteQuietly(); + } + $dm->delete(); + } Like::whereStatusId($status->id)->forceDelete(); Media::whereStatusId($status->id) ->get() ->each(function($media) { MediaDeletePipeline::dispatch($media)->onQueue('mmo'); }); - MediaTag::where('status_id', $status->id)->delete(); + $mediaTags = MediaTag::where('status_id', $status->id)->get(); + foreach($mediaTags as $mtag) { + $not = Notification::whereItemType('App\MediaTag') + ->whereItemId($mtag->id) + ->first(); + if($not) { + NotificationService::del($not->profile_id, $not->id); + $not->forceDeleteQuietly(); + } + $mtag->delete(); + } Mention::whereStatusId($status->id)->forceDelete(); Notification::whereItemType('App\Status') ->whereItemId($status->id) @@ -157,11 +176,11 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing StatusView::whereStatusId($status->id)->delete(); Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); - $status->delete(); - StatusService::del($status->id, true); AccountService::del($status->profile_id); + $status->forceDelete(); + return 1; } } diff --git a/app/Jobs/StatusPipeline/StatusDelete.php b/app/Jobs/StatusPipeline/StatusDelete.php index 19c0ea68d..d85ebdc4a 100644 --- a/app/Jobs/StatusPipeline/StatusDelete.php +++ b/app/Jobs/StatusPipeline/StatusDelete.php @@ -2,204 +2,221 @@ namespace App\Jobs\StatusPipeline; -use DB, Cache, Storage; -use App\{ - AccountInterstitial, - Bookmark, - CollectionItem, - DirectMessage, - Like, - Media, - MediaTag, - Mention, - Notification, - Report, - Status, - StatusArchived, - StatusHashtag, - StatusView -}; +use App\AccountInterstitial; +use App\Bookmark; +use App\CollectionItem; +use App\DirectMessage; +use App\Jobs\MediaPipeline\MediaDeletePipeline; +use App\Like; +use App\Media; +use App\MediaTag; +use App\Mention; +use App\Notification; +use App\Report; +use App\Services\CollectionService; +use App\Services\NotificationService; +use App\Services\StatusService; +use App\Status; +use App\StatusArchived; +use App\StatusHashtag; +use App\StatusView; +use App\Transformer\ActivityPub\Verb\DeleteNote; +use App\Util\ActivityPub\HttpSignature; +use Cache; +use GuzzleHttp\Client; +use GuzzleHttp\Pool; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use League\Fractal; -use Illuminate\Support\Str; use League\Fractal\Serializer\ArraySerializer; -use App\Transformer\ActivityPub\Verb\DeleteNote; -use App\Util\ActivityPub\Helpers; -use GuzzleHttp\Pool; -use GuzzleHttp\Client; -use GuzzleHttp\Promise; -use App\Util\ActivityPub\HttpSignature; -use App\Services\CollectionService; -use App\Services\StatusService; -use App\Jobs\MediaPipeline\MediaDeletePipeline; class StatusDelete implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $status; + protected $status; - /** - * Delete the job if its models no longer exist. - * - * @var bool - */ - public $deleteWhenMissingModels = true; + /** + * Delete the job if its models no longer exist. + * + * @var bool + */ + public $deleteWhenMissingModels = true; public $timeout = 900; + public $tries = 2; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct(Status $status) - { - $this->status = $status; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct(Status $status) + { + $this->status = $status; + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $status = $this->status; - $profile = $this->status->profile; + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $status = $this->status; + $profile = $this->status->profile; - StatusService::del($status->id, true); - if($profile) { - if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { - $profile->status_count = $profile->status_count - 1; - $profile->save(); - } - } + StatusService::del($status->id, true); + if ($profile) { + if (in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + $profile->status_count = $profile->status_count - 1; + $profile->save(); + } + } - Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id); + Cache::forget('pf:atom:user-feed:by-id:'.$status->profile_id); - if(config_cache('federation.activitypub.enabled') == true) { - return $this->fanoutDelete($status); - } else { - return $this->unlinkRemoveMedia($status); - } - } + if ((bool) config_cache('federation.activitypub.enabled') == true) { + return $this->fanoutDelete($status); + } else { + return $this->unlinkRemoveMedia($status); + } + } - public function unlinkRemoveMedia($status) - { + public function unlinkRemoveMedia($status) + { Media::whereStatusId($status->id) - ->get() - ->each(function($media) { - MediaDeletePipeline::dispatch($media); - }); + ->get() + ->each(function ($media) { + MediaDeletePipeline::dispatch($media); + }); - if($status->in_reply_to_id) { - $parent = Status::findOrFail($status->in_reply_to_id); - --$parent->reply_count; - $parent->save(); - StatusService::del($parent->id); - } + if ($status->in_reply_to_id) { + $parent = Status::findOrFail($status->in_reply_to_id); + $parent->reply_count--; + $parent->save(); + StatusService::del($parent->id); + } Bookmark::whereStatusId($status->id)->delete(); CollectionItem::whereObjectType('App\Status') ->whereObjectId($status->id) ->get() - ->each(function($col) { + ->each(function ($col) { CollectionService::removeItem($col->collection_id, $col->object_id); $col->delete(); - }); + }); - DirectMessage::whereStatusId($status->id)->delete(); + $dms = DirectMessage::whereStatusId($status->id)->get(); + foreach ($dms as $dm) { + $not = Notification::whereItemType('App\DirectMessage') + ->whereItemId($dm->id) + ->first(); + if ($not) { + NotificationService::del($not->profile_id, $not->id); + $not->forceDeleteQuietly(); + } + $dm->delete(); + } Like::whereStatusId($status->id)->delete(); - MediaTag::where('status_id', $status->id)->delete(); + $mediaTags = MediaTag::where('status_id', $status->id)->get(); + foreach ($mediaTags as $mtag) { + $not = Notification::whereItemType('App\MediaTag') + ->whereItemId($mtag->id) + ->first(); + if ($not) { + NotificationService::del($not->profile_id, $not->id); + $not->forceDeleteQuietly(); + } + $mtag->delete(); + } Mention::whereStatusId($status->id)->forceDelete(); - Notification::whereItemType('App\Status') - ->whereItemId($status->id) - ->forceDelete(); + Notification::whereItemType('App\Status') + ->whereItemId($status->id) + ->forceDelete(); - Report::whereObjectType('App\Status') - ->whereObjectId($status->id) - ->delete(); + Report::whereObjectType('App\Status') + ->whereObjectId($status->id) + ->delete(); StatusArchived::whereStatusId($status->id)->delete(); StatusHashtag::whereStatusId($status->id)->delete(); StatusView::whereStatusId($status->id)->delete(); - Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); + Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); - AccountInterstitial::where('item_type', 'App\Status') - ->where('item_id', $status->id) - ->delete(); + AccountInterstitial::where('item_type', 'App\Status') + ->where('item_id', $status->id) + ->delete(); - $status->delete(); - - return 1; - } - - public function fanoutDelete($status) - { - $profile = $status->profile; - - if(!$profile) { - return; - } - - $audience = $status->profile->getAudienceInbox(); - - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($status, new DeleteNote()); - $activity = $fractal->createData($resource)->toArray(); - - $this->unlinkRemoveMedia($status); - - $payload = json_encode($activity); - - $client = new Client([ - 'timeout' => config('federation.activitypub.delivery.timeout') - ]); - - $version = config('pixelfed.version'); - $appUrl = config('app.url'); - $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; - - $requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) { - foreach($audience as $url) { - $headers = HttpSignature::sign($profile, $url, $activity, [ - 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'User-Agent' => $userAgent, - ]); - yield function() use ($client, $url, $headers, $payload) { - return $client->postAsync($url, [ - 'curl' => [ - CURLOPT_HTTPHEADER => $headers, - CURLOPT_POSTFIELDS => $payload, - CURLOPT_HEADER => true - ] - ]); - }; - } - }; - - $pool = new Pool($client, $requests($audience), [ - 'concurrency' => config('federation.activitypub.delivery.concurrency'), - 'fulfilled' => function ($response, $index) { - }, - 'rejected' => function ($reason, $index) { - } - ]); - - $promise = $pool->promise(); - - $promise->wait(); + $status->delete(); return 1; - } + } + + public function fanoutDelete($status) + { + $profile = $status->profile; + + if (! $profile) { + return; + } + + $audience = $status->profile->getAudienceInbox(); + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($status, new DeleteNote()); + $activity = $fractal->createData($resource)->toArray(); + + $this->unlinkRemoveMedia($status); + + $payload = json_encode($activity); + + $client = new Client([ + 'timeout' => config('federation.activitypub.delivery.timeout'), + ]); + + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + + $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) { + foreach ($audience as $url) { + $headers = HttpSignature::sign($profile, $url, $activity, [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => $userAgent, + ]); + yield function () use ($client, $url, $headers, $payload) { + return $client->postAsync($url, [ + 'curl' => [ + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HEADER => true, + ], + ]); + }; + } + }; + + $pool = new Pool($client, $requests($audience), [ + 'concurrency' => config('federation.activitypub.delivery.concurrency'), + 'fulfilled' => function ($response, $index) { + }, + 'rejected' => function ($reason, $index) { + }, + ]); + + $promise = $pool->promise(); + + $promise->wait(); + + return 1; + } } diff --git a/app/Jobs/StatusPipeline/StatusEntityLexer.php b/app/Jobs/StatusPipeline/StatusEntityLexer.php index 2bbc92102..8fe767417 100644 --- a/app/Jobs/StatusPipeline/StatusEntityLexer.php +++ b/app/Jobs/StatusPipeline/StatusEntityLexer.php @@ -3,12 +3,16 @@ namespace App\Jobs\StatusPipeline; use App\Hashtag; +use App\Jobs\HomeFeedPipeline\FeedInsertPipeline; use App\Jobs\MentionPipeline\MentionPipeline; use App\Mention; use App\Profile; +use App\Services\AdminShadowFilterService; +use App\Services\PublicTimelineService; +use App\Services\StatusService; +use App\Services\UserFilterService; use App\Status; use App\StatusHashtag; -use App\Services\PublicTimelineService; use App\Util\Lexer\Autolink; use App\Util\Lexer\Extractor; use App\Util\Sentiment\Bouncer; @@ -19,171 +23,181 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use App\Services\UserFilterService; -use App\Services\AdminShadowFilterService; class StatusEntityLexer implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $status; - protected $entities; - protected $autolink; + protected $status; - /** - * Delete the job if its models no longer exist. - * - * @var bool - */ - public $deleteWhenMissingModels = true; + protected $entities; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct(Status $status) - { - $this->status = $status; - } + protected $autolink; - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $profile = $this->status->profile; - $status = $this->status; + /** + * Delete the job if its models no longer exist. + * + * @var bool + */ + public $deleteWhenMissingModels = true; - if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { - $profile->status_count = $profile->status_count + 1; - $profile->save(); - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct(Status $status) + { + $this->status = $status; + } - if($profile->no_autolink == false) { - $this->parseEntities(); - } - } + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $profile = $this->status->profile; + $status = $this->status; - public function parseEntities() - { - $this->extractEntities(); - } + if (in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + $profile->status_count = $profile->status_count + 1; + $profile->save(); + } - public function extractEntities() - { - $this->entities = Extractor::create()->extract($this->status->caption); - $this->autolinkStatus(); - } + if ($profile->no_autolink == false) { + $this->parseEntities(); + } + } - public function autolinkStatus() - { - $this->autolink = Autolink::create()->autolink($this->status->caption); - $this->storeEntities(); - } + public function parseEntities() + { + $this->extractEntities(); + } - public function storeEntities() - { - $this->storeHashtags(); - DB::transaction(function () { - $status = $this->status; - $status->rendered = nl2br($this->autolink); - $status->save(); - }); - } + public function extractEntities() + { + $this->entities = Extractor::create()->extract($this->status->caption); + $this->autolinkStatus(); + } - public function storeHashtags() - { - $tags = array_unique($this->entities['hashtags']); - $status = $this->status; + public function autolinkStatus() + { + $this->autolink = Autolink::create()->autolink($this->status->caption); + $this->storeEntities(); + } - foreach ($tags as $tag) { - if(mb_strlen($tag) > 124) { - continue; - } - DB::transaction(function () use ($status, $tag) { - $slug = str_slug($tag, '-', false); - $hashtag = Hashtag::where('slug', $slug)->first(); - if (!$hashtag) { - $hashtag = Hashtag::create( - ['name' => $tag, 'slug' => $slug] - ); - } + public function storeEntities() + { + $this->storeHashtags(); + } - StatusHashtag::firstOrCreate( - [ - 'status_id' => $status->id, - 'hashtag_id' => $hashtag->id, - 'profile_id' => $status->profile_id, - 'status_visibility' => $status->visibility, - ] - ); - }); - } - $this->storeMentions(); - } + public function storeHashtags() + { + $tags = array_unique($this->entities['hashtags']); + $status = $this->status; - public function storeMentions() - { - $mentions = array_unique($this->entities['mentions']); - $status = $this->status; + foreach ($tags as $tag) { + if (mb_strlen($tag) > 124) { + continue; + } + DB::transaction(function () use ($status, $tag) { + $slug = str_slug($tag, '-', false); - foreach ($mentions as $mention) { - $mentioned = Profile::whereUsername($mention)->first(); + $hashtag = Hashtag::firstOrCreate([ + 'slug' => $slug, + ], [ + 'name' => $tag, + ]); - if (empty($mentioned) || !isset($mentioned->id)) { - continue; - } + StatusHashtag::firstOrCreate( + [ + 'status_id' => $status->id, + 'hashtag_id' => $hashtag->id, + 'profile_id' => $status->profile_id, + 'status_visibility' => $status->visibility, + ] + ); + }); + } + $this->storeMentions(); + } + + public function storeMentions() + { + $mentions = array_unique($this->entities['mentions']); + $status = $this->status; + + foreach ($mentions as $mention) { + $mentioned = Profile::whereUsername($mention)->first(); + + if (empty($mentioned) || ! isset($mentioned->id)) { + continue; + } $blocks = UserFilterService::blocks($mentioned->id); - if($blocks && in_array($status->profile_id, $blocks)) { + if ($blocks && in_array($status->profile_id, $blocks)) { continue; } - DB::transaction(function () use ($status, $mentioned) { - $m = new Mention(); - $m->status_id = $status->id; - $m->profile_id = $mentioned->id; - $m->save(); + DB::transaction(function () use ($status, $mentioned) { + $m = new Mention; + $m->status_id = $status->id; + $m->profile_id = $mentioned->id; + $m->save(); - MentionPipeline::dispatch($status, $m); - }); - } - $this->deliver(); - } + MentionPipeline::dispatch($status, $m); + }); + } + $this->fanout(); + } - public function deliver() - { - $status = $this->status; - $types = [ - 'photo', - 'photo:album', - 'video', - 'video:album', - 'photo:video:album' - ]; + public function fanout() + { + $status = $this->status; + StatusService::refresh($status->id); - if(config_cache('pixelfed.bouncer.enabled')) { - Bouncer::get($status); - } - - Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id); - $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); - if( $status->uri == null && - $status->scope == 'public' && - in_array($status->type, $types) && - $status->in_reply_to_id === null && - $status->reblog_of_id === null && - ($hideNsfw ? $status->is_nsfw == false : true) - ) { - if(AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) { - PublicTimelineService::add($status->id); + if (config('exp.cached_home_timeline')) { + if ($status->in_reply_to_id === null && + in_array($status->scope, ['public', 'unlisted', 'private']) + ) { + FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); } - } + } + $this->deliver(); + } - if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') { - StatusActivityPubDeliver::dispatch($status); - } - } + public function deliver() + { + $status = $this->status; + $types = [ + 'photo', + 'photo:album', + 'video', + 'video:album', + 'photo:video:album', + ]; + + if ((bool) config_cache('pixelfed.bouncer.enabled')) { + Bouncer::get($status); + } + + Cache::forget('pf:atom:user-feed:by-id:'.$status->profile_id); + $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); + if ($status->uri == null && + $status->scope == 'public' && + in_array($status->type, $types) && + $status->in_reply_to_id === null && + $status->reblog_of_id === null && + ($hideNsfw ? $status->is_nsfw == false : true) + ) { + if (AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) { + PublicTimelineService::add($status->id); + } + } + + if ((bool) config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') { + StatusActivityPubDeliver::dispatch($status); + } + } } diff --git a/app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php b/app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php index 6cb11ddc6..b216c0531 100644 --- a/app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php +++ b/app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php @@ -2,172 +2,170 @@ namespace App\Jobs\StatusPipeline; +use App\Media; +use App\Models\StatusEdit; +use App\ModLog; +use App\Profile; +use App\Services\StatusService; +use App\Status; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use App\Media; -use App\ModLog; -use App\Profile; -use App\Status; -use App\Models\StatusEdit; -use App\Services\StatusService; -use Purify; use Illuminate\Support\Facades\Http; +use Purify; class StatusRemoteUpdatePipeline implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public $activity; + public $activity; - /** - * Create a new job instance. - */ - public function __construct($activity) - { - $this->activity = $activity; - } + /** + * Create a new job instance. + */ + public function __construct($activity) + { + $this->activity = $activity; + } - /** - * Execute the job. - */ - public function handle(): void - { - $activity = $this->activity; - $status = Status::with('media')->whereObjectUrl($activity['id'])->first(); - if(!$status) { - return; - } - $this->createPreviousEdit($status); - $this->updateMedia($status, $activity); - $this->updateImmediateAttributes($status, $activity); - $this->createEdit($status, $activity); - } + /** + * Execute the job. + */ + public function handle(): void + { + $activity = $this->activity; + $status = Status::with('media')->whereObjectUrl($activity['id'])->first(); + if (! $status) { + return; + } + $this->createPreviousEdit($status); + $this->updateMedia($status, $activity); + $this->updateImmediateAttributes($status, $activity); + $this->createEdit($status, $activity); + } - protected function createPreviousEdit($status) - { - if(!$status->edits()->count()) { - StatusEdit::create([ - 'status_id' => $status->id, - 'profile_id' => $status->profile_id, - 'caption' => $status->caption, - 'spoiler_text' => $status->cw_summary, - 'is_nsfw' => $status->is_nsfw, - 'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(), - 'created_at' => $status->created_at - ]); - } - } + protected function createPreviousEdit($status) + { + if (! $status->edits()->count()) { + StatusEdit::create([ + 'status_id' => $status->id, + 'profile_id' => $status->profile_id, + 'caption' => $status->caption, + 'spoiler_text' => $status->cw_summary, + 'is_nsfw' => $status->is_nsfw, + 'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(), + 'created_at' => $status->created_at, + ]); + } + } - protected function updateMedia($status, $activity) - { - if(!isset($activity['attachment'])) { - return; - } - $ogm = $status->media->count() ? $status->media()->orderBy('order')->get() : collect([]); - $nm = collect($activity['attachment'])->filter(function($nm) { - return isset( - $nm['type'], - $nm['mediaType'], - $nm['url'] - ) && - in_array($nm['type'], ['Document', 'Image', 'Video']) && - in_array($nm['mediaType'], explode(',', config('pixelfed.media_types'))); - }); + protected function updateMedia($status, $activity) + { + if (! isset($activity['attachment'])) { + return; + } + $ogm = $status->media->count() ? $status->media()->orderBy('order')->get() : collect([]); + $nm = collect($activity['attachment'])->filter(function ($nm) { + return isset( + $nm['type'], + $nm['mediaType'], + $nm['url'] + ) && + in_array($nm['type'], ['Document', 'Image', 'Video']) && + in_array($nm['mediaType'], explode(',', config_cache('pixelfed.media_types'))); + }); - // Skip when no media - if(!$ogm->count() && !$nm->count()) { - return; - } + // Skip when no media + if (! $ogm->count() && ! $nm->count()) { + return; + } - Media::whereProfileId($status->profile_id) - ->whereStatusId($status->id) - ->update([ - 'status_id' => null - ]); + Media::whereProfileId($status->profile_id) + ->whereStatusId($status->id) + ->update([ + 'status_id' => null, + ]); - $nm->each(function($n, $key) use($status) { - $res = Http::retry(3, 100, throw: false)->head($n['url']); + $nm->each(function ($n, $key) use ($status) { + $res = Http::withOptions(['allow_redirects' => false])->retry(3, 100, throw: false)->head($n['url']); - if(!$res->successful()) { - return; - } + if (! $res->successful()) { + return; + } - if(!in_array($res->header('content-type'), explode(',',config('pixelfed.media_types')))) { - return; - } + if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) { + return; + } - $m = new Media; - $m->status_id = $status->id; - $m->profile_id = $status->profile_id; - $m->remote_media = true; - $m->media_path = $n['url']; + $m = new Media; + $m->status_id = $status->id; + $m->profile_id = $status->profile_id; + $m->remote_media = true; + $m->media_path = $n['url']; $m->mime = $res->header('content-type'); $m->size = $res->hasHeader('content-length') ? $res->header('content-length') : null; - $m->caption = isset($n['name']) && !empty($n['name']) ? Purify::clean($n['name']) : null; - $m->remote_url = $n['url']; + $m->caption = isset($n['name']) && ! empty($n['name']) ? Purify::clean($n['name']) : null; + $m->remote_url = $n['url']; $m->blurhash = isset($n['blurhash']) && (strlen($n['blurhash']) < 50) ? $n['blurhash'] : null; - $m->width = isset($n['width']) && !empty($n['width']) ? $n['width'] : null; - $m->height = isset($n['height']) && !empty($n['height']) ? $n['height'] : null; - $m->skip_optimize = true; - $m->order = $key + 1; - $m->save(); - }); - } + $m->width = isset($n['width']) && ! empty($n['width']) ? $n['width'] : null; + $m->height = isset($n['height']) && ! empty($n['height']) ? $n['height'] : null; + $m->skip_optimize = true; + $m->order = $key + 1; + $m->save(); + }); + } - protected function updateImmediateAttributes($status, $activity) - { - if(isset($activity['content'])) { - $status->caption = strip_tags($activity['content']); - $status->rendered = Purify::clean($activity['content']); - } + protected function updateImmediateAttributes($status, $activity) + { + if (isset($activity['content'])) { + $status->caption = strip_tags(Purify::clean($activity['content'])); + } - if(isset($activity['sensitive'])) { - if((bool) $activity['sensitive'] == false) { - $status->is_nsfw = false; - $exists = ModLog::whereObjectType('App\Status::class') - ->whereObjectId($status->id) - ->whereAction('admin.status.moderate') - ->exists(); - if($exists == true) { - $status->is_nsfw = true; - } - $profile = Profile::find($status->profile_id); - if(!$profile || $profile->cw == true) { - $status->is_nsfw = true; - } - } else { - $status->is_nsfw = true; - } - } + if (isset($activity['sensitive'])) { + if ((bool) $activity['sensitive'] == false) { + $status->is_nsfw = false; + $exists = ModLog::whereObjectType('App\Status::class') + ->whereObjectId($status->id) + ->whereAction('admin.status.moderate') + ->exists(); + if ($exists == true) { + $status->is_nsfw = true; + } + $profile = Profile::find($status->profile_id); + if (! $profile || $profile->cw == true) { + $status->is_nsfw = true; + } + } else { + $status->is_nsfw = true; + } + } - if(isset($activity['summary'])) { - $status->cw_summary = Purify::clean($activity['summary']); - } else { - $status->cw_summary = null; - } + if (isset($activity['summary'])) { + $status->cw_summary = Purify::clean($activity['summary']); + } else { + $status->cw_summary = null; + } - $status->edited_at = now(); - $status->save(); - StatusService::del($status->id); - } + $status->edited_at = now(); + $status->save(); + StatusService::del($status->id); + } - protected function createEdit($status, $activity) - { - $cleaned = isset($activity['content']) ? Purify::clean($activity['content']) : null; - $spoiler_text = isset($activity['summary']) ? Purify::clean($activity['summary']) : null; - $sensitive = isset($activity['sensitive']) ? $activity['sensitive'] : null; - $mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null; - StatusEdit::create([ - 'status_id' => $status->id, - 'profile_id' => $status->profile_id, - 'caption' => $cleaned, - 'spoiler_text' => $spoiler_text, - 'is_nsfw' => $sensitive, - 'ordered_media_attachment_ids' => $mids - ]); - } + protected function createEdit($status, $activity) + { + $cleaned = isset($activity['content']) ? Purify::clean($activity['content']) : null; + $spoiler_text = isset($activity['summary']) ? Purify::clean($activity['summary']) : null; + $sensitive = isset($activity['sensitive']) ? $activity['sensitive'] : null; + $mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null; + StatusEdit::create([ + 'status_id' => $status->id, + 'profile_id' => $status->profile_id, + 'caption' => $cleaned, + 'spoiler_text' => $spoiler_text, + 'is_nsfw' => $sensitive, + 'ordered_media_attachment_ids' => $mids, + ]); + } } diff --git a/app/Jobs/StatusPipeline/StatusReplyPipeline.php b/app/Jobs/StatusPipeline/StatusReplyPipeline.php index 35238d293..d8af7b96b 100644 --- a/app/Jobs/StatusPipeline/StatusReplyPipeline.php +++ b/app/Jobs/StatusPipeline/StatusReplyPipeline.php @@ -87,18 +87,20 @@ class StatusReplyPipeline implements ShouldQueue Cache::forget('status:replies:all:' . $reply->id); Cache::forget('status:replies:all:' . $status->id); - DB::transaction(function() use($target, $actor, $status) { - $notification = new Notification(); - $notification->profile_id = $target->id; - $notification->actor_id = $actor->id; - $notification->action = 'comment'; - $notification->item_id = $status->id; - $notification->item_type = "App\Status"; - $notification->save(); + if($target->user_id && $target->domain === null) { + DB::transaction(function() use($target, $actor, $status) { + $notification = new Notification(); + $notification->profile_id = $target->id; + $notification->actor_id = $actor->id; + $notification->action = 'comment'; + $notification->item_id = $status->id; + $notification->item_type = "App\Status"; + $notification->save(); - NotificationService::setNotification($notification); - NotificationService::set($notification->profile_id, $notification->id); - }); + NotificationService::setNotification($notification); + NotificationService::set($notification->profile_id, $notification->id); + }); + } if($exists = Cache::get('status:replies:all:' . $reply->id)) { if($exists && $exists->count() == 3) { diff --git a/app/Jobs/StatusPipeline/StatusTagsPipeline.php b/app/Jobs/StatusPipeline/StatusTagsPipeline.php index 893fa6a83..003196e0d 100644 --- a/app/Jobs/StatusPipeline/StatusTagsPipeline.php +++ b/app/Jobs/StatusPipeline/StatusTagsPipeline.php @@ -20,117 +20,119 @@ use App\Util\ActivityPub\Helpers; class StatusTagsPipeline implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $activity; - protected $status; + protected $activity; + protected $status; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct($activity, $status) - { - $this->activity = $activity; - $this->status = $status; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct($activity, $status) + { + $this->activity = $activity; + $this->status = $status; + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $res = $this->activity; - $status = $this->status; + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $res = $this->activity; + $status = $this->status; if(isset($res['tag']['type'], $res['tag']['name'])) { $res['tag'] = [$res['tag']]; } - $tags = collect($res['tag']); + $tags = collect($res['tag']); - // Emoji - $tags->filter(function($tag) { - return $tag && isset($tag['id'], $tag['icon'], $tag['name'], $tag['type']) && $tag['type'] == 'Emoji'; - }) - ->map(function($tag) { - CustomEmojiService::import($tag['id'], $this->status->id); - }); + // Emoji + $tags->filter(function($tag) { + return $tag && isset($tag['id'], $tag['icon'], $tag['name'], $tag['type']) && $tag['type'] == 'Emoji'; + }) + ->map(function($tag) { + CustomEmojiService::import($tag['id'], $this->status->id); + }); - // Hashtags - $tags->filter(function($tag) { - return $tag && $tag['type'] == 'Hashtag' && isset($tag['href'], $tag['name']); - }) - ->map(function($tag) use($status) { - $name = substr($tag['name'], 0, 1) == '#' ? - substr($tag['name'], 1) : $tag['name']; + // Hashtags + $tags->filter(function($tag) { + return $tag && $tag['type'] == 'Hashtag' && isset($tag['href'], $tag['name']); + }) + ->map(function($tag) use($status) { + $name = substr($tag['name'], 0, 1) == '#' ? + substr($tag['name'], 1) : $tag['name']; - $banned = TrendingHashtagService::getBannedHashtagNames(); + $banned = TrendingHashtagService::getBannedHashtagNames(); - if(count($banned)) { + if(count($banned)) { if(in_array(strtolower($name), array_map('strtolower', $banned))) { - return; + return; } } if(config('database.default') === 'pgsql') { - $hashtag = Hashtag::where('name', 'ilike', $name) - ->orWhere('slug', 'ilike', str_slug($name, '-', false)) - ->first(); + $hashtag = Hashtag::where('name', 'ilike', $name) + ->orWhere('slug', 'ilike', str_slug($name, '-', false)) + ->first(); - if(!$hashtag) { - $hashtag = Hashtag::updateOrCreate([ - 'slug' => str_slug($name, '-', false), - 'name' => $name - ]); - } + if(!$hashtag) { + $hashtag = Hashtag::updateOrCreate([ + 'slug' => str_slug($name, '-', false), + 'name' => $name + ]); + } } else { - $hashtag = Hashtag::updateOrCreate([ - 'slug' => str_slug($name, '-', false), - 'name' => $name - ]); + $hashtag = Hashtag::updateOrCreate([ + 'slug' => str_slug($name, '-', false), + 'name' => $name + ]); } - StatusHashtag::firstOrCreate([ - 'status_id' => $status->id, - 'hashtag_id' => $hashtag->id, - 'profile_id' => $status->profile_id, - 'status_visibility' => $status->scope - ]); - }); + StatusHashtag::firstOrCreate([ + 'status_id' => $status->id, + 'hashtag_id' => $hashtag->id, + 'profile_id' => $status->profile_id, + 'status_visibility' => $status->scope + ]); + }); - // Mentions - $tags->filter(function($tag) { - return $tag && - $tag['type'] == 'Mention' && - isset($tag['href']) && - substr($tag['href'], 0, 8) === 'https://'; - }) - ->map(function($tag) use($status) { - if(Helpers::validateLocalUrl($tag['href'])) { - $parts = explode('/', $tag['href']); - if(!$parts) { - return; - } - $pid = AccountService::usernameToId(end($parts)); - if(!$pid) { - return; - } - } else { - $acct = Helpers::profileFetch($tag['href']); - if(!$acct) { - return; - } - $pid = $acct->id; - } - $mention = new Mention; - $mention->status_id = $status->id; - $mention->profile_id = $pid; - $mention->save(); - MentionPipeline::dispatch($status, $mention); - }); - } + // Mentions + $tags->filter(function($tag) { + return $tag && + $tag['type'] == 'Mention' && + isset($tag['href']) && + substr($tag['href'], 0, 8) === 'https://'; + }) + ->map(function($tag) use($status) { + if(Helpers::validateLocalUrl($tag['href'])) { + $parts = explode('/', $tag['href']); + if(!$parts) { + return; + } + $pid = AccountService::usernameToId(end($parts)); + if(!$pid) { + return; + } + } else { + $acct = Helpers::profileFetch($tag['href']); + if(!$acct) { + return; + } + $pid = $acct->id; + } + $mention = new Mention; + $mention->status_id = $status->id; + $mention->profile_id = $pid; + $mention->save(); + MentionPipeline::dispatch($status, $mention); + }); + + StatusService::refresh($status->id); + } } diff --git a/app/Jobs/VideoPipeline/VideoThumbnailToCloudPipeline.php b/app/Jobs/VideoPipeline/VideoThumbnailToCloudPipeline.php new file mode 100644 index 000000000..87931bd7a --- /dev/null +++ b/app/Jobs/VideoPipeline/VideoThumbnailToCloudPipeline.php @@ -0,0 +1,147 @@ +media->id; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("media:video-thumb-to-cloud:id-{$this->media->id}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct(Media $media) + { + $this->media = $media; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if((bool) config_cache('pixelfed.cloud_storage') === false) { + return; + } + + $media = $this->media; + + if($media->mime != 'video/mp4') { + return; + } + + if($media->profile_id === null || $media->status_id === null) { + return; + } + + if($media->thumbnail_url) { + return; + } + + $base = $media->media_path; + $path = explode('/', $base); + $name = last($path); + + try { + $t = explode('.', $name); + $t = $t[0].'_thumb.jpeg'; + $i = count($path) - 1; + $path[$i] = $t; + $save = implode('/', $path); + $video = FFMpeg::open($base) + ->getFrameFromSeconds(1) + ->export() + ->toDisk('local') + ->save($save); + + if(!$save) { + return; + } + + $media->thumbnail_path = $save; + $p = explode('/', $media->media_path); + array_pop($p); + $pt = explode('/', $save); + $thumbname = array_pop($pt); + $storagePath = implode('/', $p); + $thumb = storage_path('app/' . $save); + $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); + $media->thumbnail_url = $thumbUrl; + $media->save(); + + $blurhash = Blurhash::generate($media); + if($blurhash) { + $media->blurhash = $blurhash; + $media->save(); + } + + if(str_starts_with($save, 'public/m/_v2/') && str_ends_with($save, '.jpeg')) { + Storage::delete($save); + } + + if(str_starts_with($media->media_path, 'public/m/_v2/') && str_ends_with($media->media_path, '.mp4')) { + Storage::disk('local')->delete($media->media_path); + } + } catch (Exception $e) { + } + + if($media->status_id) { + Cache::forget('status:transformer:media:attachments:' . $media->status_id); + MediaService::del($media->status_id); + Cache::forget('status:thumb:nsfw0' . $media->status_id); + Cache::forget('status:thumb:nsfw1' . $media->status_id); + Cache::forget('pf:services:sh:id:' . $media->status_id); + StatusService::del($media->status_id); + } + } +} diff --git a/app/Like.php b/app/Like.php index c5b000c66..0c2c7f363 100644 --- a/app/Like.php +++ b/app/Like.php @@ -9,7 +9,7 @@ class Like extends Model { use SoftDeletes; - const MAX_PER_DAY = 500; + const MAX_PER_DAY = 1500; /** * The attributes that should be mutated to dates. diff --git a/app/Mail/AdminMessageResponse.php b/app/Mail/AdminMessageResponse.php new file mode 100644 index 000000000..a42dfa14d --- /dev/null +++ b/app/Mail/AdminMessageResponse.php @@ -0,0 +1,71 @@ +contact->getMessageId(); + + return new Headers( + messageId: $mid, + text: [ + 'X-Entity-Ref-ID' => $mid, + ], + ); + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: ucfirst(strtolower(config('pixelfed.domain.app'))).' Contact Form Response [Ticket #'.$this->contact->id.']', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.contact.admin-response', + with: [ + 'url' => $this->contact->userResponseUrl(), + ], + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterAcceptUser.php b/app/Mail/CuratedRegisterAcceptUser.php new file mode 100644 index 000000000..a87278ace --- /dev/null +++ b/app/Mail/CuratedRegisterAcceptUser.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.request-accepted', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterConfirmEmail.php b/app/Mail/CuratedRegisterConfirmEmail.php new file mode 100644 index 000000000..bf06c4311 --- /dev/null +++ b/app/Mail/CuratedRegisterConfirmEmail.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Welcome to Pixelfed! Please Confirm Your Email', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.confirm_email', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterNotifyAdmin.php b/app/Mail/CuratedRegisterNotifyAdmin.php new file mode 100644 index 000000000..28bdad971 --- /dev/null +++ b/app/Mail/CuratedRegisterNotifyAdmin.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: '[Requires Action]: New Curated Onboarding Application', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.admin_notify', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterNotifyAdminUserResponse.php b/app/Mail/CuratedRegisterNotifyAdminUserResponse.php new file mode 100644 index 000000000..bc54d5c3e --- /dev/null +++ b/app/Mail/CuratedRegisterNotifyAdminUserResponse.php @@ -0,0 +1,55 @@ +activity = $activity; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Curated Register Notify Admin User Response', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.admin_notify_user_response', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterRejectUser.php b/app/Mail/CuratedRegisterRejectUser.php new file mode 100644 index 000000000..448ea8462 --- /dev/null +++ b/app/Mail/CuratedRegisterRejectUser.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.request-rejected', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterRequestDetailsFromUser.php b/app/Mail/CuratedRegisterRequestDetailsFromUser.php new file mode 100644 index 000000000..b0ff1bb4d --- /dev/null +++ b/app/Mail/CuratedRegisterRequestDetailsFromUser.php @@ -0,0 +1,58 @@ +verify = $verify; + $this->activity = $activity; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: '[Action Needed]: Additional information requested', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.request-details-from-user', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterSendMessage.php b/app/Mail/CuratedRegisterSendMessage.php new file mode 100644 index 000000000..20ffc2749 --- /dev/null +++ b/app/Mail/CuratedRegisterSendMessage.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.message-from-admin', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/ParentChildInvite.php b/app/Mail/ParentChildInvite.php new file mode 100644 index 000000000..843ea472d --- /dev/null +++ b/app/Mail/ParentChildInvite.php @@ -0,0 +1,49 @@ + + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/UserEmailForgotReminder.php b/app/Mail/UserEmailForgotReminder.php new file mode 100644 index 000000000..f7e5dbc10 --- /dev/null +++ b/app/Mail/UserEmailForgotReminder.php @@ -0,0 +1,55 @@ +user = $user; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: '[' . config('pixelfed.domain.app') . '] Pixelfed Account Email Reminder', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.forgot-email.message', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Media.php b/app/Media.php index 1c709e7f3..9ecc7b17e 100644 --- a/app/Media.php +++ b/app/Media.php @@ -2,11 +2,11 @@ namespace App; +use App\Util\Media\License; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; -use App\Util\Media\License; -use Storage; use Illuminate\Support\Str; +use Storage; class Media extends Model { @@ -37,12 +37,12 @@ class Media extends Model public function url() { - if($this->cdn_url) { + if ($this->cdn_url) { // return Storage::disk(config('filesystems.cloud'))->url($this->media_path); return $this->cdn_url; } - if($this->remote_media && $this->remote_url) { + if ($this->remote_media && $this->remote_url) { return $this->remote_url; } @@ -51,19 +51,19 @@ class Media extends Model public function thumbnailUrl() { - if($this->thumbnail_url) { + if ($this->thumbnail_url) { return $this->thumbnail_url; } - if(!$this->remote_media && $this->thumbnail_path) { + if (! $this->remote_media && $this->thumbnail_path) { return url(Storage::url($this->thumbnail_path)); } - if($this->remote_media && !$this->thumbnail_path && $this->cdn_url) { + if (! $this->thumbnail_path && $this->cdn_url) { return $this->cdn_url; } - if($this->media_path && $this->mime && in_array($this->mime, ['image/jpeg', 'image/png'])) { + if ($this->media_path && $this->mime && in_array($this->mime, ['image/jpeg', 'image/png'])) { return $this->remote_media || Str::startsWith($this->media_path, 'http') ? $this->media_path : url(Storage::url($this->media_path)); @@ -79,9 +79,10 @@ class Media extends Model public function mimeType() { - if(!$this->mime) { + if (! $this->mime) { return; } + return explode('/', $this->mime)[0]; } @@ -105,6 +106,7 @@ class Media extends Model $verb = 'Document'; break; } + return $verb; } @@ -115,11 +117,11 @@ class Media extends Model public function getModel() { - if(empty($this->metadata)) { + if (empty($this->metadata)) { return false; } $meta = $this->getMetadata(); - if($meta && isset($meta['Model'])) { + if ($meta && isset($meta['Model'])) { return $meta['Model']; } } @@ -128,11 +130,11 @@ class Media extends Model { $license = $this->license; - if(!$license || strlen($license) > 2 || $license == 1) { + if (! $license || strlen($license) > 2 || $license == 1) { return null; } - if(!in_array($license, License::keys())) { + if (! in_array($license, License::keys())) { return null; } @@ -141,7 +143,7 @@ class Media extends Model return [ 'id' => $res['id'], 'title' => $res['title'], - 'url' => $res['url'] + 'url' => $res['url'], ]; } } diff --git a/app/MediaTag.php b/app/MediaTag.php index 49dd52b16..df253fc6e 100644 --- a/app/MediaTag.php +++ b/app/MediaTag.php @@ -8,8 +8,14 @@ class MediaTag extends Model { protected $guarded = []; + protected $visible = [ + 'status_id', + 'profile_id', + 'tagged_username', + ]; + public function status() { - return $this->belongsTo(Status::class); + return $this->belongsTo(Status::class); } } diff --git a/app/Models/AdminShadowFilter.php b/app/Models/AdminShadowFilter.php index f98086f7f..8a163feeb 100644 --- a/app/Models/AdminShadowFilter.php +++ b/app/Models/AdminShadowFilter.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Services\AccountService; +use App\Profile; class AdminShadowFilter extends Model { @@ -24,4 +25,9 @@ class AdminShadowFilter extends Model return; } + + public function profile() + { + return $this->belongsTo(Profile::class, 'item_id'); + } } diff --git a/app/Models/CuratedRegister.php b/app/Models/CuratedRegister.php new file mode 100644 index 000000000..eeeb82784 --- /dev/null +++ b/app/Models/CuratedRegister.php @@ -0,0 +1,67 @@ + 'array', + 'admin_notes' => 'array', + 'email_verified_at' => 'datetime', + 'admin_notified_at' => 'datetime', + 'action_taken_at' => 'datetime', + 'user_has_responded' => 'boolean', + 'is_awaiting_more_info' => 'boolean', + 'is_accepted' => 'boolean', + 'is_rejected' => 'boolean', + 'is_closed' => 'boolean', + ]; + + public function adminStatusLabel() + { + if($this->user_has_responded) { + return 'Awaiting Admin Response'; + } + if(!$this->email_verified_at) { + return 'Unverified email'; + } + if($this->is_approved) { + return 'Approved'; + } + if($this->is_rejected) { + return 'Rejected'; + } + if($this->is_awaiting_more_info ) { + return 'Awaiting User Response'; + } + if($this->is_closed ) { + return 'Closed'; + } + + return 'Open'; + } + + public function emailConfirmUrl() + { + return url('/auth/sign_up/confirm?sid=' . $this->id . '&code=' . $this->verify_code); + } + + public function emailReplyUrl() + { + return url('/auth/sign_up/concierge?sid=' . $this->id . '&code=' . $this->verify_code . '&sc=' . str_random(8)); + } + + public function adminReviewUrl() + { + return url('/i/admin/curated-onboarding/show/' . $this->id); + } +} diff --git a/app/Models/CuratedRegisterActivity.php b/app/Models/CuratedRegisterActivity.php new file mode 100644 index 000000000..5b5071e01 --- /dev/null +++ b/app/Models/CuratedRegisterActivity.php @@ -0,0 +1,38 @@ + 'array', + 'admin_notified_at' => 'datetime', + 'action_taken_at' => 'datetime', + ]; + + public function application() + { + return $this->belongsTo(CuratedRegister::class, 'register_id'); + } + + public function emailReplyUrl() + { + return url('/auth/sign_up/concierge?sid='.$this->register_id . '&id=' . $this->id . '&code=' . $this->secret_code); + } + + public function adminReviewUrl() + { + $url = '/i/admin/curated-onboarding/show/' . $this->register_id . '/?ah=' . $this->id; + if($this->reply_to_id) { + $url .= '&rtid=' . $this->reply_to_id; + } + return url($url); + } +} diff --git a/app/Models/CuratedRegisterTemplate.php b/app/Models/CuratedRegisterTemplate.php new file mode 100644 index 000000000..a5def0cef --- /dev/null +++ b/app/Models/CuratedRegisterTemplate.php @@ -0,0 +1,19 @@ + 'boolean', + ]; +} diff --git a/app/Models/CustomEmoji.php b/app/Models/CustomEmoji.php index 1ff026a19..47aa0d1a8 100644 --- a/app/Models/CustomEmoji.php +++ b/app/Models/CustomEmoji.php @@ -18,7 +18,7 @@ class CustomEmoji extends Model public static function scan($text, $activitypub = false) { - if(config('federation.custom_emoji.enabled') == false) { + if((bool) config_cache('federation.custom_emoji.enabled') == false) { return []; } diff --git a/app/Models/DefaultDomainBlock.php b/app/Models/DefaultDomainBlock.php new file mode 100644 index 000000000..d90816a32 --- /dev/null +++ b/app/Models/DefaultDomainBlock.php @@ -0,0 +1,13 @@ + 'json' + ]; + + public function url() + { + return url("/groups/{$this->id}"); + } + + public function permalink($suffix = null) + { + if(!$this->local) { + return $this->remote_url; + } + return $this->url() . $suffix; + } + + public function members() + { + return $this->hasMany(GroupMember::class); + } + + public function admin() + { + return $this->belongsTo(Profile::class, 'profile_id'); + } + + public function isMember($id = false) + { + $id = $id ?? request()->user()->profile_id; + // return $this->members()->whereProfileId($id)->whereJoinRequest(false)->exists(); + return GroupService::isMember($this->id, $id); + } + + public function getMembershipType() + { + return $this->is_private ? 'private' : ($this->is_local ? 'local' : 'all'); + } + + public function selfRole($id = false) + { + $id = $id ?? request()->user()->profile_id; + return optional($this->members()->whereProfileId($id)->first())->role ?? null; + } +} diff --git a/app/Models/GroupActivityGraph.php b/app/Models/GroupActivityGraph.php new file mode 100644 index 000000000..55981d20a --- /dev/null +++ b/app/Models/GroupActivityGraph.php @@ -0,0 +1,11 @@ +belongsTo(Profile::class); + } + + public function url() + { + return '/group/' . $this->group_id . '/c/' . $this->id; + } +} diff --git a/app/Models/GroupEvent.php b/app/Models/GroupEvent.php new file mode 100644 index 000000000..ddcd074cc --- /dev/null +++ b/app/Models/GroupEvent.php @@ -0,0 +1,11 @@ + 'array' + ]; +} diff --git a/app/Models/GroupInvitation.php b/app/Models/GroupInvitation.php new file mode 100644 index 000000000..adcd38ea4 --- /dev/null +++ b/app/Models/GroupInvitation.php @@ -0,0 +1,11 @@ + 'json', + 'metadata' => 'json' + ]; + + protected $fillable = [ + 'profile_id', + 'group_id' + ]; +} diff --git a/app/Models/GroupMedia.php b/app/Models/GroupMedia.php new file mode 100644 index 000000000..12f424151 --- /dev/null +++ b/app/Models/GroupMedia.php @@ -0,0 +1,39 @@ + + */ + protected function casts(): array + { + return [ + 'metadata' => 'json', + 'processed_at' => 'datetime', + 'thumbnail_generated' => 'datetime' + ]; + } + + public function url() + { + if($this->cdn_url) { + return $this->cdn_url; + } + return Storage::url($this->media_path); + } + + public function thumbnailUrl() + { + return $this->thumbnail_url; + } +} diff --git a/app/Models/GroupMember.php b/app/Models/GroupMember.php new file mode 100644 index 000000000..4f15e0d3e --- /dev/null +++ b/app/Models/GroupMember.php @@ -0,0 +1,16 @@ +belongsTo(Group::class); + } +} diff --git a/app/Models/GroupPost.php b/app/Models/GroupPost.php new file mode 100644 index 000000000..59693ec6b --- /dev/null +++ b/app/Models/GroupPost.php @@ -0,0 +1,57 @@ +group_id . '/' . $this->id; + } + + public function group() + { + return $this->belongsTo(Group::class); + } + + public function status() + { + return $this->belongsTo(Status::class); + } + + public function profile() + { + return $this->belongsTo(Profile::class); + } + + public function url() + { + return '/groups/' . $this->group_id . '/p/' . $this->id; + } +} diff --git a/app/Models/GroupPostHashtag.php b/app/Models/GroupPostHashtag.php new file mode 100644 index 000000000..46165dd7c --- /dev/null +++ b/app/Models/GroupPostHashtag.php @@ -0,0 +1,22 @@ + 'array', + 'last_calculated_at' => 'datetime', + 'last_moderated_at' => 'datetime', + ]; +} diff --git a/app/Models/ModeratedProfile.php b/app/Models/ModeratedProfile.php new file mode 100644 index 000000000..e9bc2fbf1 --- /dev/null +++ b/app/Models/ModeratedProfile.php @@ -0,0 +1,13 @@ + 'array', + 'email_sent_at' => 'datetime', + 'email_verified_at' => 'datetime' + ]; + + protected $guarded = []; + + public function parent() + { + return $this->belongsTo(User::class, 'parent_id'); + } + + public function child() + { + return $this->belongsTo(User::class, 'child_id'); + } + + public function childAccount() + { + if($u = $this->child) { + if($u->profile_id) { + return AccountService::get($u->profile_id, true); + } else { + return []; + } + } else { + return []; + } + } + + public function manageUrl() + { + return url('/settings/parental-controls/manage/' . $this->id); + } + + public function inviteUrl() + { + return url('/auth/pci/' . $this->id . '/' . $this->verify_code); + } +} diff --git a/app/Models/ProfileAlias.php b/app/Models/ProfileAlias.php index b7a3bdc9c..aef91bebc 100644 --- a/app/Models/ProfileAlias.php +++ b/app/Models/ProfileAlias.php @@ -10,6 +10,8 @@ class ProfileAlias extends Model { use HasFactory; + protected $guarded = []; + public function profile() { return $this->belongsTo(Profile::class); diff --git a/app/Models/ProfileMigration.php b/app/Models/ProfileMigration.php new file mode 100644 index 000000000..1b04aed7b --- /dev/null +++ b/app/Models/ProfileMigration.php @@ -0,0 +1,24 @@ +belongsTo(Profile::class, 'profile_id'); + } + + public function target() + { + return $this->belongsTo(Profile::class, 'target_profile_id'); + } +} diff --git a/app/Models/UserDomainBlock.php b/app/Models/UserDomainBlock.php new file mode 100644 index 000000000..900e026f2 --- /dev/null +++ b/app/Models/UserDomainBlock.php @@ -0,0 +1,21 @@ +belongsTo(Profile::class, 'profile_id'); + } +} diff --git a/app/Models/UserEmailForgot.php b/app/Models/UserEmailForgot.php new file mode 100644 index 000000000..9e549aff4 --- /dev/null +++ b/app/Models/UserEmailForgot.php @@ -0,0 +1,17 @@ + 'datetime', + ]; +} diff --git a/app/Models/UserRoles.php b/app/Models/UserRoles.php new file mode 100644 index 000000000..8d289971a --- /dev/null +++ b/app/Models/UserRoles.php @@ -0,0 +1,23 @@ + 'array' + ]; + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Observers/AvatarObserver.php b/app/Observers/AvatarObserver.php index b7854e66f..557773ce0 100644 --- a/app/Observers/AvatarObserver.php +++ b/app/Observers/AvatarObserver.php @@ -3,9 +3,9 @@ namespace App\Observers; use App\Avatar; +use App\Services\AccountService; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -use App\Services\AccountService; class AvatarObserver { @@ -19,7 +19,6 @@ class AvatarObserver /** * Handle the avatar "created" event. * - * @param \App\Avatar $avatar * @return void */ public function created(Avatar $avatar) @@ -30,7 +29,6 @@ class AvatarObserver /** * Handle the avatar "updated" event. * - * @param \App\Avatar $avatar * @return void */ public function updated(Avatar $avatar) @@ -41,7 +39,6 @@ class AvatarObserver /** * Handle the avatar "deleted" event. * - * @param \App\Avatar $avatar * @return void */ public function deleted(Avatar $avatar) @@ -52,23 +49,22 @@ class AvatarObserver /** * Handle the avatar "deleting" event. * - * @param \App\Avatar $avatar * @return void */ public function deleting(Avatar $avatar) { $path = storage_path('app/'.$avatar->media_path); - if( is_file($path) && + if (is_file($path) && $avatar->media_path != 'public/avatars/default.png' && $avatar->media_path != 'public/avatars/default.jpg' ) { @unlink($path); } - if(config_cache('pixelfed.cloud_storage')) { + if ((bool) config_cache('pixelfed.cloud_storage')) { $disk = Storage::disk(config('filesystems.cloud')); $base = Str::startsWith($avatar->media_path, 'cache/avatars/'); - if($base && $disk->exists($avatar->media_path)) { + if ($base && $disk->exists($avatar->media_path)) { $disk->delete($avatar->media_path); } } @@ -78,7 +74,6 @@ class AvatarObserver /** * Handle the avatar "restored" event. * - * @param \App\Avatar $avatar * @return void */ public function restored(Avatar $avatar) @@ -89,7 +84,6 @@ class AvatarObserver /** * Handle the avatar "force deleted" event. * - * @param \App\Avatar $avatar * @return void */ public function forceDeleted(Avatar $avatar) diff --git a/app/Observers/FollowerObserver.php b/app/Observers/FollowerObserver.php index f230bb79f..bc85e48a0 100644 --- a/app/Observers/FollowerObserver.php +++ b/app/Observers/FollowerObserver.php @@ -5,6 +5,8 @@ namespace App\Observers; use App\Follower; use App\Services\FollowerService; use Cache; +use App\Jobs\HomeFeedPipeline\FeedFollowPipeline; +use App\Jobs\HomeFeedPipeline\FeedUnfollowPipeline; class FollowerObserver { @@ -21,6 +23,7 @@ class FollowerObserver } FollowerService::add($follower->profile_id, $follower->following_id); + FeedFollowPipeline::dispatch($follower->profile_id, $follower->following_id)->onQueue('follow'); } /** diff --git a/app/Observers/HashtagFollowObserver.php b/app/Observers/HashtagFollowObserver.php new file mode 100644 index 000000000..56158c21a --- /dev/null +++ b/app/Observers/HashtagFollowObserver.php @@ -0,0 +1,51 @@ +hashtag_id, $hashtagFollow->profile_id); + } + + /** + * Handle the HashtagFollow "updated" event. + */ + public function updated(HashtagFollow $hashtagFollow): void + { + // + } + + /** + * Handle the HashtagFollow "deleting" event. + */ + public function deleting(HashtagFollow $hashtagFollow): void + { + HashtagFollowService::unfollow($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); + } + + /** + * Handle the HashtagFollow "restored" event. + */ + public function restored(HashtagFollow $hashtagFollow): void + { + // + } + + /** + * Handle the HashtagFollow "force deleted" event. + */ + public function forceDeleted(HashtagFollow $hashtagFollow): void + { + HashtagFollowService::unfollow($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); + } +} diff --git a/app/Observers/StatusHashtagObserver.php b/app/Observers/StatusHashtagObserver.php index fa38ea3c3..cac223d51 100644 --- a/app/Observers/StatusHashtagObserver.php +++ b/app/Observers/StatusHashtagObserver.php @@ -5,32 +5,31 @@ namespace App\Observers; use DB; use App\StatusHashtag; use App\Services\StatusHashtagService; +use App\Jobs\HomeFeedPipeline\HashtagInsertFanoutPipeline; +use App\Jobs\HomeFeedPipeline\HashtagRemoveFanoutPipeline; +use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit; -class StatusHashtagObserver +class StatusHashtagObserver implements ShouldHandleEventsAfterCommit { - /** - * Handle events after all transactions are committed. - * - * @var bool - */ - public $afterCommit = true; - /** * Handle the notification "created" event. * - * @param \App\Notification $notification + * @param \App\StatusHashtag $hashtag * @return void */ public function created(StatusHashtag $hashtag) { StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id); DB::table('hashtags')->where('id', $hashtag->hashtag_id)->increment('cached_count'); + if($hashtag->status_visibility && $hashtag->status_visibility === 'public') { + HashtagInsertFanoutPipeline::dispatch($hashtag)->onQueue('feed'); + } } /** * Handle the notification "updated" event. * - * @param \App\Notification $notification + * @param \App\StatusHashtag $hashtag * @return void */ public function updated(StatusHashtag $hashtag) @@ -41,19 +40,22 @@ class StatusHashtagObserver /** * Handle the notification "deleted" event. * - * @param \App\Notification $notification + * @param \App\StatusHashtag $hashtag * @return void */ public function deleted(StatusHashtag $hashtag) { StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id); DB::table('hashtags')->where('id', $hashtag->hashtag_id)->decrement('cached_count'); + if($hashtag->status_visibility && $hashtag->status_visibility === 'public') { + HashtagRemoveFanoutPipeline::dispatch($hashtag->status_id, $hashtag->hashtag_id)->onQueue('feed'); + } } /** * Handle the notification "restored" event. * - * @param \App\Notification $notification + * @param \App\StatusHashtag $hashtag * @return void */ public function restored(StatusHashtag $hashtag) @@ -64,7 +66,7 @@ class StatusHashtagObserver /** * Handle the notification "force deleted" event. * - * @param \App\Notification $notification + * @param \App\StatusHashtag $hashtag * @return void */ public function forceDeleted(StatusHashtag $hashtag) diff --git a/app/Observers/StatusObserver.php b/app/Observers/StatusObserver.php index e58997165..6c2c4c36d 100644 --- a/app/Observers/StatusObserver.php +++ b/app/Observers/StatusObserver.php @@ -7,6 +7,8 @@ use App\Services\ProfileStatusService; use Cache; use App\Models\ImportPost; use App\Services\ImportService; +use App\Jobs\HomeFeedPipeline\FeedRemovePipeline; +use App\Jobs\HomeFeedPipeline\FeedRemoveRemotePipeline; class StatusObserver { @@ -36,6 +38,10 @@ class StatusObserver */ public function updated(Status $status) { + if(!in_array($status->scope, ['public', 'unlisted', 'private'])) { + return; + } + if(config('instance.timeline.home.cached')) { Cache::forget('pf:timelines:home:' . $status->profile_id); } @@ -53,6 +59,10 @@ class StatusObserver */ public function deleted(Status $status) { + if(!in_array($status->scope, ['public', 'unlisted', 'private'])) { + return; + } + if(config('instance.timeline.home.cached')) { Cache::forget('pf:timelines:home:' . $status->profile_id); } @@ -63,6 +73,14 @@ class StatusObserver ImportPost::whereProfileId($status->profile_id)->whereStatusId($status->id)->delete(); ImportService::clearImportedFiles($status->profile_id); } + + if(config('exp.cached_home_timeline')) { + if($status->uri) { + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } else { + FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } + } } /** diff --git a/app/Observers/UserFilterObserver.php b/app/Observers/UserFilterObserver.php index 8e149e7d9..75867e64b 100644 --- a/app/Observers/UserFilterObserver.php +++ b/app/Observers/UserFilterObserver.php @@ -4,6 +4,8 @@ namespace App\Observers; use App\UserFilter; use App\Services\UserFilterService; +use App\Jobs\HomeFeedPipeline\FeedFollowPipeline; +use App\Jobs\HomeFeedPipeline\FeedUnfollowPipeline; class UserFilterObserver { @@ -78,10 +80,12 @@ class UserFilterObserver switch ($userFilter->filter_type) { case 'mute': UserFilterService::mute($userFilter->user_id, $userFilter->filterable_id); + FeedUnfollowPipeline::dispatch($userFilter->user_id, $userFilter->filterable_id)->onQueue('feed'); break; case 'block': UserFilterService::block($userFilter->user_id, $userFilter->filterable_id); + FeedUnfollowPipeline::dispatch($userFilter->user_id, $userFilter->filterable_id)->onQueue('feed'); break; } } @@ -96,10 +100,12 @@ class UserFilterObserver switch ($userFilter->filter_type) { case 'mute': UserFilterService::unmute($userFilter->user_id, $userFilter->filterable_id); + FeedFollowPipeline::dispatch($userFilter->user_id, $userFilter->filterable_id)->onQueue('feed'); break; case 'block': UserFilterService::unblock($userFilter->user_id, $userFilter->filterable_id); + FeedFollowPipeline::dispatch($userFilter->user_id, $userFilter->filterable_id)->onQueue('feed'); break; } } diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index ec4ef9f34..2b5d46116 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -7,90 +7,52 @@ use App\Follower; use App\Profile; use App\User; use App\UserSetting; +use App\Services\UserFilterService; +use App\Models\DefaultDomainBlock; +use App\Models\UserDomainBlock; use App\Jobs\FollowPipeline\FollowPipeline; use DB; use App\Services\FollowerService; class UserObserver { - /** - * Listen to the User created event. - * - * @param \App\User $user - * - * @return void - */ - public function saved(User $user) - { - if($user->status == 'deleted') { - return; - } + /** + * Handle the notification "created" event. + * + * @param \App\User $user + * @return void + */ + public function created(User $user): void + { + $this->handleUser($user); + } - if(Profile::whereUsername($user->username)->exists()) { - return; + /** + * Listen to the User saved event. + * + * @param \App\User $user + * + * @return void + */ + public function saved(User $user) + { + $this->handleUser($user); + } + + /** + * Listen to the User updated event. + * + * @param \App\User $user + * + * @return void + */ + public function updated(User $user): void + { + $this->handleUser($user); + if($user->profile) { + $this->applyDefaultDomainBlocks($user); } - - if (empty($user->profile)) { - $profile = DB::transaction(function() use($user) { - $profile = new Profile(); - $profile->user_id = $user->id; - $profile->username = $user->username; - $profile->name = $user->name; - $pkiConfig = [ - 'digest_alg' => 'sha512', - 'private_key_bits' => 2048, - 'private_key_type' => OPENSSL_KEYTYPE_RSA, - ]; - $pki = openssl_pkey_new($pkiConfig); - openssl_pkey_export($pki, $pki_private); - $pki_public = openssl_pkey_get_details($pki); - $pki_public = $pki_public['key']; - - $profile->private_key = $pki_private; - $profile->public_key = $pki_public; - $profile->save(); - return $profile; - }); - - DB::transaction(function() use($user, $profile) { - $user = User::findOrFail($user->id); - $user->profile_id = $profile->id; - $user->save(); - - CreateAvatar::dispatch($profile); - }); - - if(config_cache('account.autofollow') == true) { - $names = config_cache('account.autofollow_usernames'); - $names = explode(',', $names); - - if(!$names || !last($names)) { - return; - } - - $profiles = Profile::whereIn('username', $names)->get(); - - if($profiles) { - foreach($profiles as $p) { - $follower = new Follower; - $follower->profile_id = $profile->id; - $follower->following_id = $p->id; - $follower->save(); - - FollowPipeline::dispatch($follower); - } - } - } - } - - if (empty($user->settings)) { - DB::transaction(function() use($user) { - UserSetting::firstOrCreate([ - 'user_id' => $user->id - ]); - }); - } - } + } /** * Handle the user "deleted" event. @@ -102,4 +64,97 @@ class UserObserver { FollowerService::delCache($user->profile_id); } + + protected function handleUser($user) + { + if(in_array($user->status, ['deleted', 'delete'])) { + return; + } + + if(Profile::whereUsername($user->username)->exists()) { + return; + } + + if (empty($user->profile)) { + $profile = DB::transaction(function() use($user) { + $profile = new Profile(); + $profile->user_id = $user->id; + $profile->username = $user->username; + $profile->name = $user->name; + $pkiConfig = [ + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]; + $pki = openssl_pkey_new($pkiConfig); + openssl_pkey_export($pki, $pki_private); + $pki_public = openssl_pkey_get_details($pki); + $pki_public = $pki_public['key']; + + $profile->private_key = $pki_private; + $profile->public_key = $pki_public; + $profile->save(); + $this->applyDefaultDomainBlocks($user); + return $profile; + }); + + + DB::transaction(function() use($user, $profile) { + $user = User::findOrFail($user->id); + $user->profile_id = $profile->id; + $user->save(); + + CreateAvatar::dispatch($profile); + }); + + if((bool) config_cache('account.autofollow') == true) { + $names = config_cache('account.autofollow_usernames'); + $names = explode(',', $names); + + if(!$names || !last($names)) { + return; + } + + $profiles = Profile::whereIn('username', $names)->get(); + + if($profiles) { + foreach($profiles as $p) { + $follower = new Follower; + $follower->profile_id = $profile->id; + $follower->following_id = $p->id; + $follower->save(); + + FollowPipeline::dispatch($follower); + } + } + } + } + + if (empty($user->settings)) { + DB::transaction(function() use($user) { + UserSetting::firstOrCreate([ + 'user_id' => $user->id + ]); + }); + } + } + + protected function applyDefaultDomainBlocks($user) + { + if($user->profile_id == null) { + return; + } + $defaultDomainBlocks = DefaultDomainBlock::pluck('domain')->toArray(); + + if(!$defaultDomainBlocks || !count($defaultDomainBlocks)) { + return; + } + + foreach($defaultDomainBlocks as $domain) { + UserDomainBlock::updateOrCreate([ + 'profile_id' => $user->profile_id, + 'domain' => strtolower(trim($domain)) + ]); + } + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 774177758..b080b3b2f 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,7 @@ namespace App\Providers; use App\Observers\{ AvatarObserver, FollowerObserver, + HashtagFollowObserver, LikeObserver, NotificationObserver, ModLogObserver, @@ -17,6 +18,7 @@ use App\Observers\{ use App\{ Avatar, Follower, + HashtagFollow, Like, Notification, ModLog, @@ -32,6 +34,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider; use Illuminate\Pagination\Paginator; use Illuminate\Support\Facades\Validator; +use Illuminate\Database\Eloquent\Model; class AppServiceProvider extends ServiceProvider { @@ -50,6 +53,7 @@ class AppServiceProvider extends ServiceProvider Paginator::useBootstrap(); Avatar::observe(AvatarObserver::class); Follower::observe(FollowerObserver::class); + HashtagFollow::observe(HashtagFollowObserver::class); Like::observe(LikeObserver::class); Notification::observe(NotificationObserver::class); ModLog::observe(ModLogObserver::class); @@ -62,6 +66,8 @@ class AppServiceProvider extends ServiceProvider return Auth::check() && $request->user()->is_admin; }); Validator::includeUnvalidatedArrayKeys(); + + // Model::preventLazyLoading(true); } /** diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 0ec8c1895..8bedbfd53 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -2,9 +2,9 @@ namespace App\Providers; +use Gate; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Laravel\Passport\Passport; -use Gate; class AuthServiceProvider extends ServiceProvider { @@ -14,7 +14,7 @@ class AuthServiceProvider extends ServiceProvider * @var array */ protected $policies = [ - 'App\Model' => 'App\Policies\ModelPolicy', + // 'App\Model' => 'App\Policies\ModelPolicy', ]; /** @@ -24,24 +24,30 @@ class AuthServiceProvider extends ServiceProvider */ public function boot() { - if(config('app.env') === 'production' && config('pixelfed.oauth_enabled') == true) { + if(config('pixelfed.oauth_enabled') == true) { + Passport::ignoreRoutes(); Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356))); Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400))); Passport::enableImplicitGrant(); - if(config('instance.oauth.pat.enabled')) { + if (config('instance.oauth.pat.enabled')) { Passport::personalAccessClientId(config('instance.oauth.pat.id')); } - Passport::setDefaultScope([ - 'read', - 'write', - 'follow', - ]); Passport::tokensCan([ 'read' => 'Full read access to your account', 'write' => 'Full write access to your account', 'follow' => 'Ability to follow other profiles', - 'push' => '' + 'admin:read' => 'Read all data on the server', + 'admin:read:domain_blocks' => 'Read sensitive information of all domain blocks', + 'admin:write' => 'Modify all data on the server', + 'admin:write:domain_blocks' => 'Perform moderation actions on domain blocks', + 'push' => 'Receive your push notifications' + ]); + + Passport::setDefaultScope([ + 'read', + 'write', + 'follow', ]); } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 37aac4ac3..2452eb2a8 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -23,8 +23,6 @@ class RouteServiceProvider extends ServiceProvider */ public function boot() { - // - parent::boot(); } @@ -36,10 +34,7 @@ class RouteServiceProvider extends ServiceProvider public function map() { $this->mapApiRoutes(); - $this->mapWebRoutes(); - - // } /** @@ -51,6 +46,18 @@ class RouteServiceProvider extends ServiceProvider */ protected function mapWebRoutes() { + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web-admin.php')); + + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web-portfolio.php')); + + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web-api.php')); + Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php')); diff --git a/app/Rules/ExpoPushTokenRule.php b/app/Rules/ExpoPushTokenRule.php new file mode 100644 index 000000000..27fb9670b --- /dev/null +++ b/app/Rules/ExpoPushTokenRule.php @@ -0,0 +1,33 @@ +setSerializer(new ArraySerializer()); - $profile = Profile::find($id); - if(!$profile || $profile->status === 'delete') { - return null; - } - $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); - return $fractal->createData($resource)->toArray(); - }); + const CACHE_PF_ACCT_SETTINGS_KEY = 'pf:services:account-settings:'; - if(!$res) { - return $softFail ? null : abort(404); - } - return $res; - } + public static function get($id, $softFail = false) + { + $res = Cache::remember(self::CACHE_KEY.$id, 43200, function () use ($id) { + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $profile = Profile::find($id); + if (! $profile || $profile->status === 'delete') { + return null; + } + $resource = new Fractal\Resource\Item($profile, new AccountTransformer); - public static function getMastodon($id, $softFail = false) - { - $account = self::get($id, $softFail); - if(!$account) { - return null; - } + return $fractal->createData($resource)->toArray(); + }); - if(config('exp.emc') == false) { - return $account; - } + if (! $res) { + return $softFail ? null : abort(404); + } - unset( - $account['header_bg'], - $account['is_admin'], - $account['last_fetched_at'], - $account['local'], - $account['location'], - $account['note_text'], - $account['pronouns'], - $account['website'] - ); + return $res; + } - $account['avatar_static'] = $account['avatar']; - $account['bot'] = false; - $account['emojis'] = []; - $account['fields'] = []; - $account['header'] = url('/storage/headers/missing.png'); - $account['header_static'] = url('/storage/headers/missing.png'); - $account['last_status_at'] = null; + public static function getMastodon($id, $softFail = false) + { + $account = self::get($id, $softFail); + if (! $account) { + return null; + } - return $account; - } + if (config('exp.emc') == false) { + return $account; + } - public static function del($id) - { - Cache::forget('pf:activitypub:user-object:by-id:' . $id); - return Cache::forget(self::CACHE_KEY . $id); - } + unset( + $account['header_bg'], + $account['is_admin'], + $account['last_fetched_at'], + $account['local'], + $account['location'], + $account['note_text'], + $account['pronouns'], + $account['website'] + ); - public static function settings($id) - { - return Cache::remember('profile:compose:settings:' . $id, 604800, function() use($id) { - $settings = UserSetting::whereUserId($id)->first(); - if(!$settings) { - return self::defaultSettings(); - } - return collect($settings) - ->filter(function($item, $key) { - return in_array($key, array_keys(self::defaultSettings())) == true; - }) - ->map(function($item, $key) { - if($key == 'compose_settings') { - $cs = self::defaultSettings()['compose_settings']; - $ms = is_array($item) ? $item : []; - return array_merge($cs, $ms); - } + $account['avatar_static'] = $account['avatar']; + $account['bot'] = false; + $account['emojis'] = []; + $account['fields'] = []; + $account['header'] = url('/storage/headers/missing.png'); + $account['header_static'] = url('/storage/headers/missing.png'); + $account['last_status_at'] = null; - if($key == 'other') { - $other = self::defaultSettings()['other']; - $mo = is_array($item) ? $item : []; - return array_merge($other, $mo); - } - return $item; - }); - }); - } + return $account; + } - public static function canEmbed($id) - { - return self::settings($id)['other']['disable_embeds'] == false; - } + public static function del($id) + { + Cache::forget('pf:activitypub:user-object:by-id:'.$id); - public static function defaultSettings() - { - return [ - 'crawlable' => true, - 'public_dm' => false, - 'reduce_motion' => false, - 'high_contrast_mode' => false, - 'video_autoplay' => false, - 'show_profile_follower_count' => true, - 'show_profile_following_count' => true, - 'compose_settings' => [ - 'default_scope' => 'public', - 'default_license' => 1, - 'media_descriptions' => false - ], - 'other' => [ - 'advanced_atom' => false, - 'disable_embeds' => false, - 'mutual_mention_notifications' => false, - 'hide_collections' => false, - 'hide_like_counts' => false, - 'hide_groups' => false, - 'hide_stories' => false, - 'disable_cw' => false, - ] - ]; - } + return Cache::forget(self::CACHE_KEY.$id); + } - public static function syncPostCount($id) - { - $profile = Profile::find($id); + public static function settings($id) + { + return Cache::remember('profile:compose:settings:'.$id, 604800, function () use ($id) { + $settings = UserSetting::whereUserId($id)->first(); + if (! $settings) { + return self::defaultSettings(); + } - if(!$profile) { - return false; - } + return collect($settings) + ->filter(function ($item, $key) { + return in_array($key, array_keys(self::defaultSettings())) == true; + }) + ->map(function ($item, $key) { + if ($key == 'compose_settings') { + $cs = self::defaultSettings()['compose_settings']; + $ms = is_array($item) ? $item : []; - $key = self::CACHE_KEY . 'pcs:' . $id; + return array_merge($cs, $ms); + } - if(Cache::has($key)) { - return; - } + if ($key == 'other') { + $other = self::defaultSettings()['other']; + $mo = is_array($item) ? $item : []; - $count = Status::whereProfileId($id) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->whereIn('scope', ['public', 'unlisted', 'private']) - ->count(); + return array_merge($other, $mo); + } - $profile->status_count = $count; - $profile->save(); + return $item; + }); + }); + } - Cache::put($key, 1, 900); - return true; - } + public static function getAccountSettings($pid) + { + $key = self::CACHE_PF_ACCT_SETTINGS_KEY.$pid; - public static function usernameToId($username) - { - $key = self::CACHE_KEY . 'u2id:' . hash('sha256', $username); - return Cache::remember($key, 900, function() use($username) { - $s = Str::of($username); - if($s->contains('@') && !$s->startsWith('@')) { - $username = "@{$username}"; - } - $profile = DB::table('profiles') - ->whereUsername($username) - ->first(); - if(!$profile) { - return null; - } - return (string) $profile->id; - }); - } + return Cache::remember($key, 14400, function () use ($pid) { + $user = User::with('profile')->whereProfileId($pid)->whereNull('status')->first(); + if (! $user) { + return []; + } - public static function hiddenFollowers($id) - { - $account = self::get($id, true); - if(!$account || !isset($account['local']) || $account['local'] == false) { - return false; - } + $settings = $user->settings; + $other = array_merge(self::defaultSettings()['other'], $settings->other ?? []); - return Cache::remember('pf:acct:settings:hidden-followers:' . $id, 43200, function() use($id) { - $user = User::whereProfileId($id)->first(); - if(!$user) { - return false; - } - $settings = UserSetting::whereUserId($user->id)->first(); - if($settings) { - return $settings->show_profile_follower_count == false; - } - return false; - }); - } + return [ + 'reduce_motion' => (bool) $settings->reduce_motion, + 'high_contrast_mode' => (bool) $settings->high_contrast_mode, + 'video_autoplay' => (bool) $settings->video_autoplay, + 'media_descriptions' => (bool) $settings->media_descriptions, + 'crawlable' => (bool) $settings->crawlable, + 'show_profile_follower_count' => (bool) $settings->show_profile_follower_count, + 'show_profile_following_count' => (bool) $settings->show_profile_following_count, + 'public_dm' => (bool) $settings->public_dm, + 'disable_embeds' => (bool) $other['disable_embeds'], + 'show_atom' => (bool) $settings->show_atom, + 'is_suggestable' => (bool) $user->profile->is_suggestable, + 'indexable' => (bool) $user->profile->indexable, + ]; + }); + } - public static function hiddenFollowing($id) - { - $account = self::get($id, true); - if(!$account || !isset($account['local']) || $account['local'] == false) { - return false; - } + public static function forgetAccountSettings($pid) + { + return Cache::forget(self::CACHE_PF_ACCT_SETTINGS_KEY.$pid); + } - return Cache::remember('pf:acct:settings:hidden-following:' . $id, 43200, function() use($id) { - $user = User::whereProfileId($id)->first(); - if(!$user) { - return false; - } - $settings = UserSetting::whereUserId($user->id)->first(); - if($settings) { - return $settings->show_profile_following_count == false; - } - return false; - }); - } + public static function canEmbed($id) + { + $res = self::getAccountSettings($id); + if (! $res || ! isset($res['disable_embeds'])) { + return false; + } + + return ! $res['disable_embeds']; + } + + public static function defaultSettings() + { + return [ + 'crawlable' => true, + 'public_dm' => false, + 'reduce_motion' => false, + 'high_contrast_mode' => false, + 'video_autoplay' => false, + 'show_profile_follower_count' => true, + 'show_profile_following_count' => true, + 'compose_settings' => [ + 'default_scope' => 'public', + 'default_license' => 1, + 'media_descriptions' => false, + ], + 'other' => [ + 'advanced_atom' => false, + 'disable_embeds' => false, + 'mutual_mention_notifications' => false, + 'hide_collections' => false, + 'hide_like_counts' => false, + 'hide_groups' => false, + 'hide_stories' => false, + 'disable_cw' => false, + ], + ]; + } + + public static function syncPostCount($id) + { + $profile = Profile::find($id); + + if (! $profile) { + return false; + } + + $key = self::CACHE_KEY.'pcs:'.$id; + + if (Cache::has($key)) { + return; + } + + $count = Status::whereProfileId($id) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->whereIn('scope', ['public', 'unlisted', 'private']) + ->count(); + + $profile->status_count = $count; + $profile->save(); + + Cache::put($key, 1, 259200); + + return true; + } + + public static function usernameToId($username) + { + $key = self::CACHE_KEY.'u2id:'.hash('sha256', $username); + + return Cache::remember($key, 14400, function () use ($username) { + $s = Str::of($username); + if ($s->contains('@') && ! $s->startsWith('@')) { + $username = "@{$username}"; + } + $profile = DB::table('profiles') + ->whereUsername($username) + ->first(); + if (! $profile) { + return null; + } + + return (string) $profile->id; + }); + } + + public static function hiddenFollowers($id) + { + $account = self::get($id, true); + if (! $account || ! isset($account['local']) || $account['local'] == false) { + return false; + } + + return Cache::remember('pf:acct:settings:hidden-followers:'.$id, 43200, function () use ($id) { + $user = User::whereProfileId($id)->first(); + if (! $user) { + return false; + } + $settings = UserSetting::whereUserId($user->id)->first(); + if ($settings) { + return $settings->show_profile_follower_count == false; + } + + return false; + }); + } + + public static function hiddenFollowing($id) + { + $account = self::get($id, true); + if (! $account || ! isset($account['local']) || $account['local'] == false) { + return false; + } + + return Cache::remember('pf:acct:settings:hidden-following:'.$id, 43200, function () use ($id) { + $user = User::whereProfileId($id)->first(); + if (! $user) { + return false; + } + $settings = UserSetting::whereUserId($user->id)->first(); + if ($settings) { + return $settings->show_profile_following_count == false; + } + + return false; + }); + } + + public static function setLastActive($id = false) + { + if (! $id) { + return; + } + $key = 'user:last_active_at:id:'.$id; + if (! Cache::has($key)) { + $user = User::find($id); + if (! $user) { + return; + } + $user->last_active_at = now(); + $user->save(); + Cache::put($key, 1, 14400); + } + } + + public static function blocksDomain($pid, $domain = false) + { + if (! $domain) { + return; + } + + return UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->exists(); + } + + public static function formatNumber($num) + { + if (! $num || $num < 1) { + return '0'; + } + $num = intval($num); + $formatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL); + $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 1); + + if ($num >= 1000000000) { + return $formatter->format($num / 1000000000).'B'; + } elseif ($num >= 1000000) { + return $formatter->format($num / 1000000).'M'; + } elseif ($num >= 1000) { + return $formatter->format($num / 1000).'K'; + } else { + return $formatter->format($num); + } + } + + public static function getMetaDescription($id) + { + $account = self::get($id, true); + + if (! $account) { + return ''; + } + + $posts = self::formatNumber($account['statuses_count']).' Posts, '; + $following = self::formatNumber($account['following_count']).' Following, '; + $followers = self::formatNumber($account['followers_count']).' Followers'; + $note = $account['note'] && strlen($account['note']) ? + ' · '.\Purify::clean(strip_tags(str_replace("\n", '', str_replace("\r", '', $account['note'])))) : + ''; + + return $posts.$following.$followers.$note; + } } diff --git a/app/Services/ActivityPubFetchService.php b/app/Services/ActivityPubFetchService.php index 3d1980a11..761be5c77 100644 --- a/app/Services/ActivityPubFetchService.php +++ b/app/Services/ActivityPubFetchService.php @@ -2,47 +2,137 @@ namespace App\Services; -use Illuminate\Support\Facades\Http; -use App\Profile; -use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\HttpSignature; +use Cache; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\RequestException; +use Illuminate\Support\Facades\Http; class ActivityPubFetchService { - public static function get($url, $validateUrl = true) - { - if($validateUrl === true) { - if(!Helpers::validateUrl($url)) { - return 0; - } + const CACHE_KEY = 'pf:services:apfetchs:'; + + public static function get($url, $validateUrl = true) + { + if (! self::validateUrl($url)) { + return false; + } + $domain = parse_url($url, PHP_URL_HOST); + if (! $domain) { + return false; + } + $domainKey = base64_encode($domain); + $urlKey = hash('sha256', $url); + $key = self::CACHE_KEY.$domainKey.':'.$urlKey; + + return Cache::remember($key, 450, function () use ($url) { + return self::fetchRequest($url); + }); + } + + public static function validateUrl($url) + { + if (is_array($url)) { + $url = $url[0]; } - $baseHeaders = [ - 'Accept' => 'application/activity+json, application/ld+json', - ]; + $localhosts = [ + '127.0.0.1', 'localhost', '::1', + ]; - $headers = HttpSignature::instanceActorSign($url, false, $baseHeaders, 'get'); - $headers['Accept'] = 'application/activity+json, application/ld+json'; - $headers['User-Agent'] = 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'; + if (strtolower(mb_substr($url, 0, 8)) !== 'https://') { + return false; + } - try { - $res = Http::withHeaders($headers) - ->timeout(30) - ->connectTimeout(5) - ->retry(3, 500) - ->get($url); - } catch (RequestException $e) { - return; - } catch (ConnectionException $e) { - return; - } catch (Exception $e) { - return; - } - if(!$res->ok()) { - return; - } - return $res->body(); - } + if (substr_count($url, '://') !== 1) { + return false; + } + + if (mb_substr($url, 0, 8) !== 'https://') { + $url = 'https://'.substr($url, 8); + } + + $valid = filter_var($url, FILTER_VALIDATE_URL); + + if (! $valid) { + return false; + } + + $host = parse_url($valid, PHP_URL_HOST); + + if (in_array($host, $localhosts)) { + return false; + } + + if (config('security.url.verify_dns')) { + if (DomainService::hasValidDns($host) === false) { + return false; + } + } + + if (app()->environment() === 'production') { + $bannedInstances = InstanceService::getBannedDomains(); + if (in_array($host, $bannedInstances)) { + return false; + } + } + + return $url; + } + + public static function fetchRequest($url, $returnJsonFormat = false) + { + $baseHeaders = [ + 'Accept' => 'application/activity+json', + ]; + + $headers = HttpSignature::instanceActorSign($url, false, $baseHeaders, 'get'); + $headers['Accept'] = 'application/activity+json'; + $headers['User-Agent'] = 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'; + + try { + $res = Http::withOptions([ + 'allow_redirects' => [ + 'max' => 2, + 'protocols' => ['https'], + ]]) + ->withHeaders($headers) + ->timeout(30) + ->connectTimeout(5) + ->retry(3, 500) + ->get($url); + } catch (RequestException $e) { + return; + } catch (ConnectionException $e) { + return; + } catch (Exception $e) { + return; + } + + if (! $res->ok()) { + return; + } + + if (! $res->hasHeader('Content-Type')) { + return; + } + + $acceptedTypes = [ + 'application/activity+json; charset=utf-8', + 'application/activity+json', + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + ]; + + $contentType = $res->getHeader('Content-Type')[0]; + + if (! $contentType) { + return; + } + + if (! in_array($contentType, $acceptedTypes)) { + return; + } + + return $returnJsonFormat ? $res->json() : $res->body(); + } } diff --git a/app/Services/AdminSettingsService.php b/app/Services/AdminSettingsService.php new file mode 100644 index 000000000..6a261f5a3 --- /dev/null +++ b/app/Services/AdminSettingsService.php @@ -0,0 +1,167 @@ + self::getFeatures(), + 'landing' => self::getLanding(), + 'branding' => self::getBranding(), + 'media' => self::getMedia(), + 'rules' => self::getRules(), + 'suggested_rules' => self::getSuggestedRules(), + 'users' => self::getUsers(), + 'posts' => self::getPosts(), + 'platform' => self::getPlatform(), + 'storage' => self::getStorage(), + ]; + } + + public static function getFeatures() + { + $cloud_storage = (bool) config_cache('pixelfed.cloud_storage'); + $cloud_disk = config('filesystems.cloud'); + $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret')); + $openReg = (bool) config_cache('pixelfed.open_registration'); + $curOnboarding = (bool) config_cache('instance.curated_registration.enabled'); + $regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed'); + + return [ + 'registration_status' => $regState, + 'cloud_storage' => $cloud_ready && $cloud_storage, + 'activitypub_enabled' => (bool) config_cache('federation.activitypub.enabled'), + 'authorized_fetch' => (bool) config_cache('federation.activitypub.authorized_fetch'), + 'account_migration' => (bool) config_cache('federation.migration'), + 'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'), + 'stories' => (bool) config_cache('instance.stories.enabled'), + 'instagram_import' => (bool) config_cache('pixelfed.import.instagram.enabled'), + 'autospam_enabled' => (bool) config_cache('pixelfed.bouncer.enabled'), + ]; + } + + public static function getLanding() + { + $availableAdmins = User::whereIsAdmin(true)->get(); + $currentAdmin = config_cache('instance.admin.pid'); + + return [ + 'admins' => $availableAdmins, + 'current_admin' => $currentAdmin, + 'show_directory' => (bool) config_cache('instance.landing.show_directory'), + 'show_explore' => (bool) config_cache('instance.landing.show_explore'), + ]; + } + + public static function getBranding() + { + return [ + 'name' => config_cache('app.name'), + 'short_description' => config_cache('app.short_description'), + 'long_description' => config_cache('app.description'), + ]; + } + + public static function getMedia() + { + return [ + 'max_photo_size' => config_cache('pixelfed.max_photo_size'), + 'max_album_length' => config_cache('pixelfed.max_album_length'), + 'image_quality' => config_cache('pixelfed.image_quality'), + 'media_types' => config_cache('pixelfed.media_types'), + 'optimize_image' => (bool) config_cache('pixelfed.optimize_image'), + 'optimize_video' => (bool) config_cache('pixelfed.optimize_video'), + ]; + } + + public static function getRules() + { + return config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : []; + } + + public static function getSuggestedRules() + { + return BeagleService::getDefaultRules(); + } + + public static function getUsers() + { + $autoFollow = config_cache('account.autofollow_usernames'); + if (strlen($autoFollow) >= 2) { + $autoFollow = explode(',', $autoFollow); + } else { + $autoFollow = []; + } + + return [ + 'require_email_verification' => (bool) config_cache('pixelfed.enforce_email_verification'), + 'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'), + 'max_account_size' => config_cache('pixelfed.max_account_size'), + 'admin_autofollow' => (bool) config_cache('account.autofollow'), + 'admin_autofollow_accounts' => $autoFollow, + 'max_user_blocks' => (int) config_cache('instance.user_filters.max_user_blocks'), + 'max_user_mutes' => (int) config_cache('instance.user_filters.max_user_mutes'), + 'max_domain_blocks' => (int) config_cache('instance.user_filters.max_domain_blocks'), + ]; + } + + public static function getPosts() + { + return [ + 'max_caption_length' => config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => config_cache('pixelfed.max_altext_length'), + ]; + } + + public static function getPlatform() + { + return [ + 'allow_app_registration' => (bool) config_cache('pixelfed.allow_app_registration'), + 'app_registration_rate_limit_attempts' => config_cache('pixelfed.app_registration_rate_limit_attempts'), + 'app_registration_rate_limit_decay' => config_cache('pixelfed.app_registration_rate_limit_decay'), + 'app_registration_confirm_rate_limit_attempts' => config_cache('pixelfed.app_registration_confirm_rate_limit_attempts'), + 'app_registration_confirm_rate_limit_decay' => config_cache('pixelfed.app_registration_confirm_rate_limit_decay'), + 'allow_post_embeds' => (bool) config_cache('instance.embed.post'), + 'allow_profile_embeds' => (bool) config_cache('instance.embed.profile'), + 'captcha_enabled' => (bool) config_cache('captcha.enabled'), + 'captcha_on_login' => (bool) config_cache('captcha.active.login'), + 'captcha_on_register' => (bool) config_cache('captcha.active.register'), + 'captcha_secret' => Str::of(config_cache('captcha.secret'))->mask('*', 4, -4), + 'captcha_sitekey' => Str::of(config_cache('captcha.sitekey'))->mask('*', 4, -4), + 'custom_emoji_enabled' => (bool) config_cache('federation.custom_emoji.enabled'), + ]; + } + + public static function getStorage() + { + $cloud_storage = (bool) config_cache('pixelfed.cloud_storage'); + $cloud_disk = config('filesystems.cloud'); + $cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret')); + $primaryDisk = (bool) $cloud_ready && $cloud_storage; + $pkey = 'filesystems.disks.'.$cloud_disk.'.'; + $disk = [ + 'driver' => $cloud_disk, + 'key' => Str::of(config_cache($pkey.'key'))->mask('*', 0, -2), + 'secret' => Str::of(config_cache($pkey.'secret'))->mask('*', 0, -2), + 'region' => config_cache($pkey.'region'), + 'bucket' => config_cache($pkey.'bucket'), + 'visibility' => config_cache($pkey.'visibility'), + 'endpoint' => config_cache($pkey.'endpoint'), + 'url' => config_cache($pkey.'url'), + 'use_path_style_endpoint' => config_cache($pkey.'use_path_style_endpoint'), + ]; + + return [ + 'primary_disk' => $primaryDisk ? 'cloud' : 'local', + 'cloud_ready' => (bool) $cloud_ready, + 'cloud_disk' => $cloud_disk, + 'disk_config' => $disk, + ]; + } +} diff --git a/app/Services/AdminStatsService.php b/app/Services/AdminStatsService.php index 9e345355a..9acbb3c6e 100644 --- a/app/Services/AdminStatsService.php +++ b/app/Services/AdminStatsService.php @@ -2,47 +2,43 @@ namespace App\Services; -use Cache; -use DB; +use App\Avatar; +use App\Contact; +use App\FailedJob; +use App\Instance; +use App\Media; +use App\Profile; +use App\Report; +use App\Status; +use App\User; use App\Util\Lexer\PrettyNumber; -use App\{ - Avatar, - Contact, - FailedJob, - Hashtag, - Instance, - Media, - Like, - Profile, - Report, - Status, - User -}; -use \DateInterval; -use \DatePeriod; +use Cache; +use DateInterval; +use DatePeriod; +use DB; class AdminStatsService { - public static function get() - { - return array_merge( - self::recentData(), - self::additionalData(), - self::postsGraph() - ); - } - - public static function summary() - { - return array_merge( - self::recentData(), - self::additionalDataSummary(), - ); - } - - public static function storage() + public static function get() { - return Cache::remember('admin:dashboard:storage:stats', 120000, function() { + return array_merge( + self::recentData(), + self::additionalData(), + self::postsGraph() + ); + } + + public static function summary() + { + return array_merge( + self::recentData(), + self::additionalDataSummary(), + ); + } + + public static function storage() + { + return Cache::remember('admin:dashboard:storage:stats', 120000, function () { $res = []; $res['last_updated'] = str_replace('+00:00', 'Z', now()->format(DATE_RFC3339_EXTENDED)); @@ -53,7 +49,7 @@ class AdminStatsService 'count' => $avatars, 'local_count' => $avatarsLocal, 'cloud_count' => ($avatars - $avatarsLocal), - 'total_sum' => Avatar::sum('size') + 'total_sum' => Avatar::sum('size'), ]; $media = Media::count(); @@ -77,97 +73,100 @@ class AdminStatsService }); } - protected static function recentData() - { - $day = config('database.default') == 'pgsql' ? 'DATE_PART(\'day\',' : 'day('; - $ttl = now()->addMinutes(15); - return Cache::remember('admin:dashboard:home:data:v0:15min', $ttl, function() use ($day) { - return [ - 'contact' => PrettyNumber::convert(Contact::whereNull('read_at')->count()), - 'contact_monthly' => PrettyNumber::convert(Contact::whereNull('read_at')->where('created_at', '>', now()->subMonth())->count()), - 'reports' => PrettyNumber::convert(Report::whereNull('admin_seen')->count()), - 'reports_monthly' => PrettyNumber::convert(Report::whereNull('admin_seen')->where('created_at', '>', now()->subMonth())->count()), - ]; - }); - } + protected static function recentData() + { + $day = config('database.default') == 'pgsql' ? 'DATE_PART(\'day\',' : 'day('; + $ttl = now()->addMinutes(15); - protected static function additionalData() - { - $day = config('database.default') == 'pgsql' ? 'DATE_PART(\'day\',' : 'day('; - $ttl = now()->addHours(24); - return Cache::remember('admin:dashboard:home:data:v0:24hr', $ttl, function() use ($day) { - return [ - 'failedjobs' => PrettyNumber::convert(FailedJob::where('failed_at', '>=', \Carbon\Carbon::now()->subDay())->count()), - 'statuses' => PrettyNumber::convert(Status::count()), - 'statuses_monthly' => PrettyNumber::convert(Status::where('created_at', '>', now()->subMonth())->count()), - 'profiles' => PrettyNumber::convert(Profile::count()), - 'users' => PrettyNumber::convert(User::count()), - 'users_monthly' => PrettyNumber::convert(User::where('created_at', '>', now()->subMonth())->count()), - 'instances' => PrettyNumber::convert(Instance::count()), - 'media' => PrettyNumber::convert(Media::count()), - 'storage' => Media::sum('size'), - ]; - }); - } + return Cache::remember('admin:dashboard:home:data:v0:15min', $ttl, function () { + return [ + 'contact' => PrettyNumber::convert(Contact::whereNull('read_at')->count()), + 'contact_monthly' => PrettyNumber::convert(Contact::whereNull('read_at')->where('created_at', '>', now()->subMonth())->count()), + 'reports' => PrettyNumber::convert(Report::whereNull('admin_seen')->count()), + 'reports_monthly' => PrettyNumber::convert(Report::whereNull('admin_seen')->where('created_at', '>', now()->subMonth())->count()), + ]; + }); + } - protected static function additionalDataSummary() - { - $ttl = now()->addHours(24); - return Cache::remember('admin:dashboard:home:data-summary:v0:24hr', $ttl, function() { - return [ - 'statuses' => PrettyNumber::convert(Status::count()), - 'profiles' => PrettyNumber::convert(Profile::count()), - 'users' => PrettyNumber::convert(User::count()), - 'instances' => PrettyNumber::convert(Instance::count()), - ]; - }); - } + protected static function additionalData() + { + $day = config('database.default') == 'pgsql' ? 'DATE_PART(\'day\',' : 'day('; + $ttl = now()->addHours(24); - protected static function postsGraph() - { - $ttl = now()->addHours(12); - return Cache::remember('admin:dashboard:home:data-postsGraph:v0.1:24hr', $ttl, function() { - $gb = config('database.default') == 'pgsql' ? ['statuses.id', 'created_at'] : DB::raw('Date(created_at)'); - $s = Status::selectRaw('Date(created_at) as date, count(statuses.id) as count') - ->where('created_at', '>=', now()->subWeek()) - ->groupBy($gb) - ->orderBy('created_at', 'DESC') - ->pluck('count', 'date'); + return Cache::remember('admin:dashboard:home:data:v0:24hr', $ttl, function () { + return [ + 'failedjobs' => PrettyNumber::convert(FailedJob::where('failed_at', '>=', \Carbon\Carbon::now()->subDay())->count()), + 'statuses' => PrettyNumber::convert(intval(StatusService::totalLocalStatuses())), + 'statuses_monthly' => PrettyNumber::convert(Status::where('created_at', '>', now()->subMonth())->count()), + 'profiles' => PrettyNumber::convert(Profile::count()), + 'users' => PrettyNumber::convert(User::count()), + 'users_monthly' => PrettyNumber::convert(User::where('created_at', '>', now()->subMonth())->count()), + 'instances' => PrettyNumber::convert(Instance::count()), + 'media' => PrettyNumber::convert(Media::count()), + 'storage' => Media::sum('size'), + ]; + }); + } - $begin = now()->subWeek(); - $end = now(); - $interval = new DateInterval('P1D'); - $daterange = new DatePeriod($begin, $interval ,$end); - $dates = []; - foreach($daterange as $date){ - $dates[$date->format("Y-m-d")] = 0; - } + protected static function additionalDataSummary() + { + $ttl = now()->addHours(24); - $dates = collect($dates)->merge($s); + return Cache::remember('admin:dashboard:home:data-summary:v0:24hr', $ttl, function () { + return [ + 'statuses' => PrettyNumber::convert(intval(StatusService::totalLocalStatuses())), + 'profiles' => PrettyNumber::convert(Profile::count()), + 'users' => PrettyNumber::convert(User::count()), + 'instances' => PrettyNumber::convert(Instance::count()), + ]; + }); + } - $s = Status::selectRaw('Date(created_at) as date, count(statuses.id) as count') - ->where('created_at', '>=', now()->subWeeks(2)) - ->where('created_at', '<=', now()->subWeeks(1)) - ->groupBy($gb) - ->orderBy('created_at', 'DESC') - ->pluck('count', 'date'); + protected static function postsGraph() + { + $ttl = now()->addHours(12); - $begin = now()->subWeeks(2); - $end = now()->subWeeks(1); - $interval = new DateInterval('P1D'); - $daterange = new DatePeriod($begin, $interval ,$end); - $lw = []; - foreach($daterange as $date){ - $lw[$date->format("Y-m-d")] = 0; - } + return Cache::remember('admin:dashboard:home:data-postsGraph:v0.1:24hr', $ttl, function () { + $gb = config('database.default') == 'pgsql' ? ['statuses.id', 'created_at'] : DB::raw('Date(created_at)'); + $s = Status::selectRaw('Date(created_at) as date, count(statuses.id) as count') + ->where('created_at', '>=', now()->subWeek()) + ->groupBy($gb) + ->orderBy('created_at', 'DESC') + ->pluck('count', 'date'); - $lw = collect($lw)->merge($s); + $begin = now()->subWeek(); + $end = now(); + $interval = new DateInterval('P1D'); + $daterange = new DatePeriod($begin, $interval, $end); + $dates = []; + foreach ($daterange as $date) { + $dates[$date->format('Y-m-d')] = 0; + } - return [ - 'posts_this_week' => $dates->values(), - 'posts_last_week' => $lw->values(), - ]; - }); - } + $dates = collect($dates)->merge($s); + $s = Status::selectRaw('Date(created_at) as date, count(statuses.id) as count') + ->where('created_at', '>=', now()->subWeeks(2)) + ->where('created_at', '<=', now()->subWeeks(1)) + ->groupBy($gb) + ->orderBy('created_at', 'DESC') + ->pluck('count', 'date'); + + $begin = now()->subWeeks(2); + $end = now()->subWeeks(1); + $interval = new DateInterval('P1D'); + $daterange = new DatePeriod($begin, $interval, $end); + $lw = []; + foreach ($daterange as $date) { + $lw[$date->format('Y-m-d')] = 0; + } + + $lw = collect($lw)->merge($s); + + return [ + 'posts_this_week' => $dates->values(), + 'posts_last_week' => $lw->values(), + ]; + }); + } } diff --git a/app/Services/AutolinkService.php b/app/Services/AutolinkService.php index f0f3278ff..c494ab151 100644 --- a/app/Services/AutolinkService.php +++ b/app/Services/AutolinkService.php @@ -2,53 +2,25 @@ namespace App\Services; -use Cache; use App\Profile; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Http; -use App\Util\Webfinger\WebfingerUrl; +use Cache; +use Purify; class AutolinkService { - const CACHE_KEY = 'pf:services:autolink:'; + const CACHE_KEY = 'pf:services:autolink:mue:'; - public static function mentionedUsernameExists($username) - { - $key = 'pf:services:autolink:userexists:' . hash('sha256', $username); + public static function mentionedUsernameExists($username) + { + if (str_starts_with($username, '@')) { + if (substr_count($username, '@') === 1) { + $username = substr($username, 1); + } + } + $name = Purify::clean(strtolower($username)); - return Cache::remember($key, 3600, function() use($username) { - $remote = Str::of($username)->contains('@'); - $profile = Profile::whereUsername($username)->first(); - if($profile) { - if($profile->domain != null) { - $instance = InstanceService::getByDomain($profile->domain); - if($instance && $instance->banned == true) { - return false; - } - } - return true; - } else { - if($remote) { - $parts = explode('@', $username); - $domain = last($parts); - $instance = InstanceService::getByDomain($domain); - - if($instance) { - if($instance->banned == true) { - return false; - } else { - $wf = WebfingerUrl::generateWebfingerUrl($username); - $res = Http::head($wf); - return $res->ok(); - } - } else { - $wf = WebfingerUrl::generateWebfingerUrl($username); - $res = Http::head($wf); - return $res->ok(); - } - } - } - return false; - }); - } + return Cache::remember(self::CACHE_KEY.base64_encode($name), 7200, function () use ($name) { + return Profile::where('username', $name)->exists(); + }); + } } diff --git a/app/Services/AutospamService.php b/app/Services/AutospamService.php index 6986e81e4..3164d14d0 100644 --- a/app/Services/AutospamService.php +++ b/app/Services/AutospamService.php @@ -2,77 +2,82 @@ namespace App\Services; +use App\Util\Lexer\Classifier; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Storage; -use App\Util\Lexer\Classifier; class AutospamService { - const CHCKD_CACHE_KEY = 'pf:services:autospam:nlp:checked'; - const MODEL_CACHE_KEY = 'pf:services:autospam:nlp:model-cache'; - const MODEL_FILE_PATH = 'nlp/active-training-data.json'; - const MODEL_SPAM_PATH = 'nlp/spam.json'; - const MODEL_HAM_PATH = 'nlp/ham.json'; + const CHCKD_CACHE_KEY = 'pf:services:autospam:nlp:checked'; - public static function check($text) - { - if(!$text || strlen($text) == 0) { - false; - } - if(!self::active()) { - return null; - } - $model = self::getCachedModel(); - $classifier = new Classifier; - $classifier->import($model['documents'], $model['words']); - return $classifier->most($text) === 'spam'; - } + const MODEL_CACHE_KEY = 'pf:services:autospam:nlp:model-cache'; - public static function eligible() - { - return Cache::remember(self::CHCKD_CACHE_KEY, 86400, function() { - if(!config_cache('pixelfed.bouncer.enabled') || !config('autospam.enabled')) { - return false; - } + const MODEL_FILE_PATH = 'nlp/active-training-data.json'; - if(!Storage::exists(self::MODEL_SPAM_PATH)) { - return false; - } + const MODEL_SPAM_PATH = 'nlp/spam.json'; - if(!Storage::exists(self::MODEL_HAM_PATH)) { - return false; - } + const MODEL_HAM_PATH = 'nlp/ham.json'; - if(!Storage::exists(self::MODEL_FILE_PATH)) { - return false; - } else { - if(Storage::size(self::MODEL_FILE_PATH) < 1000) { - return false; - } - } + public static function check($text) + { + if (! $text || strlen($text) == 0) { - return true; - }); - } + } + if (! self::active()) { + return null; + } + $model = self::getCachedModel(); + $classifier = new Classifier; + $classifier->import($model['documents'], $model['words']); - public static function active() - { - return config_cache('autospam.nlp.enabled') && self::eligible(); - } + return $classifier->most($text) === 'spam'; + } - public static function getCachedModel() - { - if(!self::active()) { - return null; - } + public static function eligible() + { + return Cache::remember(self::CHCKD_CACHE_KEY, 86400, function () { + if (! (bool) config_cache('pixelfed.bouncer.enabled') || ! (bool) config_cache('autospam.enabled')) { + return false; + } - return Cache::remember(self::MODEL_CACHE_KEY, 86400, function() { - $res = Storage::get(self::MODEL_FILE_PATH); - if(!$res || empty($res)) { - return null; - } + if (! Storage::exists(self::MODEL_SPAM_PATH)) { + return false; + } - return json_decode($res, true); - }); - } + if (! Storage::exists(self::MODEL_HAM_PATH)) { + return false; + } + + if (! Storage::exists(self::MODEL_FILE_PATH)) { + return false; + } else { + if (Storage::size(self::MODEL_FILE_PATH) < 1000) { + return false; + } + } + + return true; + }); + } + + public static function active() + { + return config_cache('autospam.nlp.enabled') && self::eligible(); + } + + public static function getCachedModel() + { + if (! self::active()) { + return null; + } + + return Cache::remember(self::MODEL_CACHE_KEY, 86400, function () { + $res = Storage::get(self::MODEL_FILE_PATH); + if (! $res || empty($res)) { + return null; + } + + return json_decode($res, true); + }); + } } diff --git a/app/Services/ConfigCacheService.php b/app/Services/ConfigCacheService.php index 9da5c8adc..527c86026 100644 --- a/app/Services/ConfigCacheService.php +++ b/app/Services/ConfigCacheService.php @@ -2,125 +2,215 @@ namespace App\Services; -use Cache; -use Config; use App\Models\ConfigCache as ConfigCacheModel; +use Cache; +use Illuminate\Database\QueryException; class ConfigCacheService { - const CACHE_KEY = 'config_cache:_v0-key:'; + const CACHE_KEY = 'config_cache:_v0-key:'; - public static function get($key) - { - $cacheKey = self::CACHE_KEY . $key; - $ttl = now()->addHours(12); - if(!config('instance.enable_cc')) { - return config($key); - } + const PROTECTED_KEYS = [ + 'filesystems.disks.s3.key', + 'filesystems.disks.s3.secret', + 'filesystems.disks.spaces.key', + 'filesystems.disks.spaces.secret', + 'captcha.secret', + 'captcha.sitekey', + ]; - return Cache::remember($cacheKey, $ttl, function() use($key) { + public static function get($key) + { + $cacheKey = self::CACHE_KEY.$key; + $ttl = now()->addHours(12); + if (! config('instance.enable_cc')) { + return config($key); + } - $allowed = [ - 'app.name', - 'app.short_description', - 'app.description', - 'app.rules', + try { + return Cache::remember($cacheKey, $ttl, function () use ($key) { + $allowed = [ + 'app.name', + 'app.short_description', + 'app.description', + 'app.rules', - 'pixelfed.max_photo_size', - 'pixelfed.max_album_length', - 'pixelfed.image_quality', - 'pixelfed.media_types', + 'pixelfed.max_photo_size', + 'pixelfed.max_album_length', + 'pixelfed.image_quality', + 'pixelfed.media_types', - 'pixelfed.open_registration', - 'federation.activitypub.enabled', - 'instance.stories.enabled', - 'pixelfed.oauth_enabled', - 'pixelfed.import.instagram.enabled', - 'pixelfed.bouncer.enabled', + 'pixelfed.open_registration', + 'federation.activitypub.enabled', + 'instance.stories.enabled', + 'pixelfed.oauth_enabled', + 'pixelfed.import.instagram.enabled', + 'pixelfed.bouncer.enabled', + 'federation.activitypub.authorized_fetch', - 'pixelfed.enforce_email_verification', - 'pixelfed.max_account_size', - 'pixelfed.enforce_account_limit', + 'pixelfed.enforce_email_verification', + 'pixelfed.max_account_size', + 'pixelfed.enforce_account_limit', - 'uikit.custom.css', - 'uikit.custom.js', - 'uikit.show_custom.css', - 'uikit.show_custom.js', - 'about.title', + 'uikit.custom.css', + 'uikit.custom.js', + 'uikit.show_custom.css', + 'uikit.show_custom.js', + 'about.title', - 'pixelfed.cloud_storage', + 'pixelfed.cloud_storage', - 'account.autofollow', - 'account.autofollow_usernames', - 'config.discover.features', + 'account.autofollow', + 'account.autofollow_usernames', + 'config.discover.features', - 'instance.has_legal_notice', - 'instance.avatar.local_to_cloud', + 'instance.has_legal_notice', + 'instance.avatar.local_to_cloud', - 'pixelfed.directory', - 'app.banner_image', - 'pixelfed.directory.submission-key', - 'pixelfed.directory.submission-ts', - 'pixelfed.directory.has_submitted', - 'pixelfed.directory.latest_response', - 'pixelfed.directory.is_synced', - 'pixelfed.directory.testimonials', + 'pixelfed.directory', + 'app.banner_image', + 'pixelfed.directory.submission-key', + 'pixelfed.directory.submission-ts', + 'pixelfed.directory.has_submitted', + 'pixelfed.directory.latest_response', + 'pixelfed.directory.is_synced', + 'pixelfed.directory.testimonials', - 'instance.landing.show_directory', - 'instance.landing.show_explore', - 'instance.admin.pid', - 'instance.banner.blurhash', + 'instance.landing.show_directory', + 'instance.landing.show_explore', + 'instance.admin.pid', + 'instance.banner.blurhash', - 'autospam.nlp.enabled', - // 'system.user_mode' - ]; + 'autospam.nlp.enabled', - if(!config('instance.enable_cc')) { - return config($key); - } + 'instance.curated_registration.enabled', - if(!in_array($key, $allowed)) { - return config($key); - } + 'federation.migration', - $v = config($key); - $c = ConfigCacheModel::where('k', $key)->first(); + 'pixelfed.max_caption_length', + 'pixelfed.max_bio_length', + 'pixelfed.max_name_length', + 'pixelfed.min_password_length', + 'pixelfed.max_avatar_size', + 'pixelfed.max_altext_length', + 'pixelfed.allow_app_registration', + 'pixelfed.app_registration_rate_limit_attempts', + 'pixelfed.app_registration_rate_limit_decay', + 'pixelfed.app_registration_confirm_rate_limit_attempts', + 'pixelfed.app_registration_confirm_rate_limit_decay', + 'instance.embed.profile', + 'instance.embed.post', - if($c) { - return $c->v ?? config($key); - } + 'captcha.enabled', + 'captcha.secret', + 'captcha.sitekey', + 'captcha.active.login', + 'captcha.active.register', + 'captcha.triggers.login.enabled', + 'captcha.triggers.login.attempts', + 'federation.custom_emoji.enabled', - if(!$v) { - return; - } + 'pixelfed.optimize_image', + 'pixelfed.optimize_video', + 'pixelfed.max_collection_length', + 'media.delete_local_after_cloud', + 'instance.user_filters.max_user_blocks', + 'instance.user_filters.max_user_mutes', + 'instance.user_filters.max_domain_blocks', - $cc = new ConfigCacheModel; - $cc->k = $key; - $cc->v = $v; - $cc->save(); + 'filesystems.disks.s3.key', + 'filesystems.disks.s3.secret', + 'filesystems.disks.s3.region', + 'filesystems.disks.s3.bucket', + 'filesystems.disks.s3.visibility', + 'filesystems.disks.s3.url', + 'filesystems.disks.s3.endpoint', + 'filesystems.disks.s3.use_path_style_endpoint', - return $v; - }); - } + 'filesystems.disks.spaces.key', + 'filesystems.disks.spaces.secret', + 'filesystems.disks.spaces.region', + 'filesystems.disks.spaces.bucket', + 'filesystems.disks.spaces.visibility', + 'filesystems.disks.spaces.url', + 'filesystems.disks.spaces.endpoint', + 'filesystems.disks.spaces.use_path_style_endpoint', - public static function put($key, $val) - { - $exists = ConfigCacheModel::whereK($key)->first(); + 'instance.stats.total_local_posts', + // 'system.user_mode' + ]; - if($exists) { - $exists->v = $val; - $exists->save(); - Cache::put(self::CACHE_KEY . $key, $val, now()->addHours(12)); - return self::get($key); - } + if (! config('instance.enable_cc')) { + return config($key); + } - $cc = new ConfigCacheModel; - $cc->k = $key; - $cc->v = $val; - $cc->save(); + if (! in_array($key, $allowed)) { + return config($key); + } - Cache::put(self::CACHE_KEY . $key, $val, now()->addHours(12)); + $protect = false; + $protected = null; + if (in_array($key, self::PROTECTED_KEYS)) { + $protect = true; + } - return self::get($key); - } + $v = config($key); + $c = ConfigCacheModel::where('k', $key)->first(); + + if ($c) { + if ($protect) { + return decrypt($c->v) ?? config($key); + } else { + return $c->v ?? config($key); + } + } + + if (! $v) { + return; + } + + if ($protect && $v) { + $protected = encrypt($v); + } + + $cc = new ConfigCacheModel; + $cc->k = $key; + $cc->v = $protect ? $protected : $v; + $cc->save(); + + return $v; + }); + } catch (Exception|QueryException $e) { + return config($key); + } + } + + public static function put($key, $val) + { + $exists = ConfigCacheModel::whereK($key)->first(); + + $protect = false; + $protected = null; + if (in_array($key, self::PROTECTED_KEYS)) { + $protect = true; + $protected = encrypt($val); + } + + if ($exists) { + $exists->v = $protect ? $protected : $val; + $exists->save(); + Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12)); + + return self::get($key); + } + + $cc = new ConfigCacheModel; + $cc->k = $key; + $cc->v = $protect ? $protected : $val; + $cc->save(); + + Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12)); + + return self::get($key); + } } diff --git a/app/Services/CustomEmojiService.php b/app/Services/CustomEmojiService.php index a95c93a2a..f9f267174 100644 --- a/app/Services/CustomEmojiService.php +++ b/app/Services/CustomEmojiService.php @@ -13,7 +13,7 @@ class CustomEmojiService { public static function get($shortcode) { - if(config('federation.custom_emoji.enabled') == false) { + if((bool) config_cache('federation.custom_emoji.enabled') == false) { return; } @@ -22,7 +22,7 @@ class CustomEmojiService public static function import($url, $id = false) { - if(config('federation.custom_emoji.enabled') == false) { + if((bool) config_cache('federation.custom_emoji.enabled') == false) { return; } @@ -133,6 +133,7 @@ class CustomEmojiService return CustomEmoji::when(!$pgsql, function($q, $pgsql) { return $q->groupBy('shortcode'); }) + ->whereNull('uri') ->get() ->map(function($emojo) { $url = url('storage/' . $emojo->media_path); diff --git a/app/Services/DomainService.php b/app/Services/DomainService.php index 01f050ca0..a55cd1dcc 100644 --- a/app/Services/DomainService.php +++ b/app/Services/DomainService.php @@ -3,25 +3,30 @@ namespace App\Services; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Redis; class DomainService { - const CACHE_KEY = 'pf:services:domains:'; + const CACHE_KEY = 'pf:services:domains:'; public static function hasValidDns($domain) { - if(!$domain || !strlen($domain) || strpos($domain, '.') == -1) { + if (! $domain || ! strlen($domain) || strpos($domain, '.') == -1) { return false; } - if(config('security.url.trusted_domains')) { - if(in_array($domain, explode(',', config('security.url.trusted_domains')))) { + if (config('security.url.trusted_domains')) { + if (in_array($domain, explode(',', config('security.url.trusted_domains')))) { return true; } } - return Cache::remember(self::CACHE_KEY . 'valid-dns:' . $domain, 14400, function() use($domain) { + $valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); + + if (! $valid) { + return false; + } + + return Cache::remember(self::CACHE_KEY.'valid-dns:'.$domain, 1800, function () use ($domain) { return count(dns_get_record($domain, DNS_A | DNS_AAAA)) > 0; }); } diff --git a/app/Services/Federation/ActiveSharedInboxService.php b/app/Services/Federation/ActiveSharedInboxService.php new file mode 100644 index 000000000..4f7020a8f --- /dev/null +++ b/app/Services/Federation/ActiveSharedInboxService.php @@ -0,0 +1,244 @@ +parse($res['updated'])->lt(now()->subMonths(6))) { + $res = self::getFromDatabase(); + } else { + if ($res['version'] === self::CACHE_FILE_VERSION) { + $res = $res['data']; + } else { + $res = self::getFromDatabase(); + } + } + } else { + $res = self::getFromDatabase(); + } + } + } else { + $res = self::getFromDatabase(); + } + + if (! $res) { + return []; + } + + $filteredList = []; + + foreach ($res as $value) { + if (! $value || ! str_starts_with($value, 'https://')) { + continue; + } + $passed = self::add($value); + if ($passed) { + $filteredList[] = $value; + } + } + + self::saveCacheToDisk($filteredList); + Cache::remember(self::CACHE_KEY_CHECK, 86400, function () { + return true; + }); + + return $res; + } + + public static function parseCacheFileData() + { + if (Storage::has(self::CACHE_FILE_NAME)) { + $res = Storage::get(self::CACHE_FILE_NAME); + if (! $res) { + return false; + } else { + $res = json_decode($res, true); + if (! $res || isset($res['version'], $res['data'], $res['created'], $res['updated'])) { + if (now()->parse($res['updated'])->lt(now()->subMonths(6))) { + return false; + } else { + if ($res['version'] === self::CACHE_FILE_VERSION) { + return $res; + } else { + return false; + } + } + } else { + return false; + } + } + } + + return false; + } + + public static function transformCacheFileData($res) + { + return [ + 'id' => 'pixelfed/storage/app/'.self::CACHE_FILE_NAME, + 'version' => self::CACHE_FILE_VERSION, + 'created' => now()->format('c'), + 'updated' => now()->format('c'), + 'length' => count($res), + 'data' => $res, + ]; + } + + public static function updateCacheFileData() + { + $res = self::parseCacheFileData(); + if (! $res) { + return false; + } + + $diff = []; + $nodes = $res['data']; + $latest = self::getFromDatabase(); + $merge = array_merge($nodes, $latest); + + foreach ($merge as $val) { + if (! in_array($val, $nodes)) { + if (self::add($val)) { + $nodes[] = $val; + } else { + unset($nodes[$val]); + } + } + } + + $data = [ + 'id' => 'pixelfed/storage/app/'.self::CACHE_FILE_NAME, + 'version' => self::CACHE_FILE_VERSION, + 'created' => $res['created'], + 'updated' => now()->format('c'), + 'length' => count($nodes), + 'data' => $nodes, + ]; + + $contents = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + Storage::put(self::CACHE_FILE_NAME, $contents); + + return 1; + } + + public static function saveCacheToDisk($res = false) + { + if (! $res) { + return; + } + + $contents = json_encode(self::transformCacheFileData($res), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + Storage::put(self::CACHE_FILE_NAME, $contents); + } + + public static function getFromDatabase() + { + return Profile::whereNotNull('sharedInbox')->groupBy('sharedInbox')->pluck('sharedInbox')->toArray(); + } + + public static function scanForUpdates() + { + $res = self::getFromDatabase(); + $filteredList = []; + + foreach ($res as $value) { + if (! $value || ! str_starts_with($value, 'https://')) { + continue; + } + Redis::sadd(self::CACHE_KEY, $value); + $filteredList[] = $value; + } + + self::saveCacheToDisk($filteredList); + + return 1; + } +} diff --git a/app/Services/FetchCacheService.php b/app/Services/FetchCacheService.php new file mode 100644 index 000000000..2e23fb009 --- /dev/null +++ b/app/Services/FetchCacheService.php @@ -0,0 +1,79 @@ + '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')', + ]; + + if ($allowRedirects) { + $options = [ + 'allow_redirects' => [ + 'max' => 2, + 'strict' => true, + ], + ]; + } else { + $options = [ + 'allow_redirects' => false, + ]; + } + try { + $res = Http::withOptions($options) + ->retry(3, function (int $attempt, $exception) { + return $attempt * 500; + }) + ->acceptJson() + ->withHeaders($headers) + ->timeout(40) + ->get($url); + } catch (RequestException $e) { + Cache::put($key, 1, $ttl); + + return false; + } catch (ConnectionException $e) { + Cache::put($key, 1, $ttl); + + return false; + } catch (Exception $e) { + Cache::put($key, 1, $ttl); + + return false; + } + + if (! $res->ok()) { + Cache::put($key, 1, $ttl); + + return false; + } + + return $res->json(); + } +} diff --git a/app/Services/FilesystemService.php b/app/Services/FilesystemService.php new file mode 100644 index 000000000..b52f002f4 --- /dev/null +++ b/app/Services/FilesystemService.php @@ -0,0 +1,82 @@ + 'latest', + 'region' => $region, + 'endpoint' => $endpoint, + 'credentials' => [ + 'key' => $key, + 'secret' => $secret, + ] + ]); + + $adapter = new AwsS3V3Adapter( + $client, + $bucket, + ); + + $throw = false; + $filesystem = new Filesystem($adapter); + + $writable = false; + try { + $filesystem->write(self::VERIFY_FILE_NAME, 'ok', []); + $writable = true; + } catch (FilesystemException | UnableToWriteFile $exception) { + $writable = false; + } + + if(!$writable) { + return false; + } + + try { + $response = $filesystem->read(self::VERIFY_FILE_NAME); + if($response === 'ok') { + $writable = true; + $res[] = self::VERIFY_FILE_NAME; + } else { + $writable = false; + } + } catch (FilesystemException | UnableToReadFile $exception) { + $writable = false; + } + + if(in_array(self::VERIFY_FILE_NAME, $res)) { + try { + $filesystem->delete(self::VERIFY_FILE_NAME); + } catch (FilesystemException | UnableToDeleteFile $exception) { + $writable = false; + } + } + + if(!$writable) { + return false; + } + + if(in_array(self::VERIFY_FILE_NAME, $res)) { + return true; + } + + return false; + } +} diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php index 1c00a6f49..ff2a191f2 100644 --- a/app/Services/FollowerService.php +++ b/app/Services/FollowerService.php @@ -6,210 +6,263 @@ use Illuminate\Support\Facades\Redis; use Cache; use DB; use App\{ - Follower, - Profile, - User + Follower, + Profile, + User }; use App\Jobs\FollowPipeline\FollowServiceWarmCache; class FollowerService { - const CACHE_KEY = 'pf:services:followers:'; - const FOLLOWERS_SYNC_KEY = 'pf:services:followers:sync-followers:'; - const FOLLOWING_SYNC_KEY = 'pf:services:followers:sync-following:'; - const FOLLOWING_KEY = 'pf:services:follow:following:id:'; - const FOLLOWERS_KEY = 'pf:services:follow:followers:id:'; + const CACHE_KEY = 'pf:services:followers:'; + const FOLLOWERS_SYNC_KEY = 'pf:services:followers:sync-followers:'; + const FOLLOWING_SYNC_KEY = 'pf:services:followers:sync-following:'; + const FOLLOWING_KEY = 'pf:services:follow:following:id:'; + const FOLLOWERS_KEY = 'pf:services:follow:followers:id:'; + const FOLLOWERS_LOCAL_KEY = 'pf:services:follow:local-follower-ids:v1:'; + const FOLLOWERS_INTER_KEY = 'pf:services:follow:followers:inter:id:'; - public static function add($actor, $target, $refresh = true) - { - $ts = (int) microtime(true); + public static function add($actor, $target, $refresh = true) + { + $ts = (int) microtime(true); if($refresh) { RelationshipService::refresh($actor, $target); } else { - RelationshipService::forget($actor, $target); + RelationshipService::forget($actor, $target); } - Redis::zadd(self::FOLLOWING_KEY . $actor, $ts, $target); - Redis::zadd(self::FOLLOWERS_KEY . $target, $ts, $actor); - Cache::forget('profile:following:' . $actor); - } + Redis::zadd(self::FOLLOWING_KEY . $actor, $ts, $target); + Redis::zadd(self::FOLLOWERS_KEY . $target, $ts, $actor); + Cache::forget('profile:following:' . $actor); + Cache::forget(self::FOLLOWERS_LOCAL_KEY . $actor); + Cache::forget(self::FOLLOWERS_LOCAL_KEY . $target); + } - public static function remove($actor, $target) - { - Redis::zrem(self::FOLLOWING_KEY . $actor, $target); - Redis::zrem(self::FOLLOWERS_KEY . $target, $actor); - Cache::forget('pf:services:follower:audience:' . $actor); - Cache::forget('pf:services:follower:audience:' . $target); - AccountService::del($actor); - AccountService::del($target); - RelationshipService::refresh($actor, $target); - Cache::forget('profile:following:' . $actor); - } + public static function remove($actor, $target, $silent = false) + { + Redis::zrem(self::FOLLOWING_KEY . $actor, $target); + Redis::zrem(self::FOLLOWERS_KEY . $target, $actor); + Cache::forget(self::FOLLOWERS_LOCAL_KEY . $actor); + Cache::forget(self::FOLLOWERS_LOCAL_KEY . $target); + if($silent !== true) { + AccountService::del($actor); + AccountService::del($target); + RelationshipService::refresh($actor, $target); + Cache::forget('profile:following:' . $actor); + } else { + RelationshipService::forget($actor, $target); + } + } - public static function followers($id, $start = 0, $stop = 10) - { - self::cacheSyncCheck($id, 'followers'); - return Redis::zrevrange(self::FOLLOWERS_KEY . $id, $start, $stop); - } + public static function followers($id, $start = 0, $stop = 10) + { + self::cacheSyncCheck($id, 'followers'); + return Redis::zrevrange(self::FOLLOWERS_KEY . $id, $start, $stop); + } - public static function following($id, $start = 0, $stop = 10) - { - self::cacheSyncCheck($id, 'following'); - return Redis::zrevrange(self::FOLLOWING_KEY . $id, $start, $stop); - } + public static function following($id, $start = 0, $stop = 10) + { + self::cacheSyncCheck($id, 'following'); + return Redis::zrevrange(self::FOLLOWING_KEY . $id, $start, $stop); + } - public static function followersPaginate($id, $page = 1, $limit = 10) - { - $start = $page == 1 ? 0 : $page * $limit - $limit; - $end = $start + ($limit - 1); - return self::followers($id, $start, $end); - } + public static function followersPaginate($id, $page = 1, $limit = 10) + { + $start = $page == 1 ? 0 : $page * $limit - $limit; + $end = $start + ($limit - 1); + return self::followers($id, $start, $end); + } - public static function followingPaginate($id, $page = 1, $limit = 10) - { - $start = $page == 1 ? 0 : $page * $limit - $limit; - $end = $start + ($limit - 1); - return self::following($id, $start, $end); - } + public static function followingPaginate($id, $page = 1, $limit = 10) + { + $start = $page == 1 ? 0 : $page * $limit - $limit; + $end = $start + ($limit - 1); + return self::following($id, $start, $end); + } - public static function followerCount($id, $warmCache = true) - { - if($warmCache) { - self::cacheSyncCheck($id, 'followers'); - } - return Redis::zCard(self::FOLLOWERS_KEY . $id); - } + public static function followerCount($id, $warmCache = true) + { + if($warmCache) { + self::cacheSyncCheck($id, 'followers'); + } + return Redis::zCard(self::FOLLOWERS_KEY . $id); + } - public static function followingCount($id, $warmCache = true) - { - if($warmCache) { - self::cacheSyncCheck($id, 'following'); - } - return Redis::zCard(self::FOLLOWING_KEY . $id); - } + public static function followingCount($id, $warmCache = true) + { + if($warmCache) { + self::cacheSyncCheck($id, 'following'); + } + return Redis::zCard(self::FOLLOWING_KEY . $id); + } - public static function follows(string $actor, string $target) - { - if($actor == $target) { - return false; - } + public static function follows(string $actor, string $target, $quickCheck = false) + { + if($actor == $target) { + return false; + } - if(self::followerCount($target, false) && self::followingCount($actor, false)) { - self::cacheSyncCheck($target, 'followers'); - return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor); - } else { - self::cacheSyncCheck($target, 'followers'); - self::cacheSyncCheck($actor, 'following'); - return Follower::whereProfileId($actor)->whereFollowingId($target)->exists(); - } - } + if($quickCheck) { + return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor); + } - public static function cacheSyncCheck($id, $scope = 'followers') - { - if($scope === 'followers') { - if(Cache::get(self::FOLLOWERS_SYNC_KEY . $id) != null) { - return; - } - FollowServiceWarmCache::dispatch($id)->onQueue('low'); - } - if($scope === 'following') { - if(Cache::get(self::FOLLOWING_SYNC_KEY . $id) != null) { - return; - } - FollowServiceWarmCache::dispatch($id)->onQueue('low'); - } - return; - } + if(self::followerCount($target, false) && self::followingCount($actor, false)) { + self::cacheSyncCheck($target, 'followers'); + return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor); + } else { + self::cacheSyncCheck($target, 'followers'); + self::cacheSyncCheck($actor, 'following'); + return Follower::whereProfileId($actor)->whereFollowingId($target)->exists(); + } + } - public static function audience($profile, $scope = null) - { - return (new self)->getAudienceInboxes($profile, $scope); - } + public static function cacheSyncCheck($id, $scope = 'followers') + { + if($scope === 'followers') { + if(Cache::get(self::FOLLOWERS_SYNC_KEY . $id) != null) { + return; + } + FollowServiceWarmCache::dispatch($id)->onQueue('low'); + } + if($scope === 'following') { + if(Cache::get(self::FOLLOWING_SYNC_KEY . $id) != null) { + return; + } + FollowServiceWarmCache::dispatch($id)->onQueue('low'); + } + return; + } - public static function softwareAudience($profile, $software = 'pixelfed') - { - return collect(self::audience($profile)) - ->filter(function($inbox) use($software) { - $domain = parse_url($inbox, PHP_URL_HOST); - if(!$domain) { - return false; - } - return InstanceService::software($domain) === strtolower($software); - }) - ->unique() - ->values() - ->toArray(); - } + public static function audience($profile, $scope = null) + { + return (new self)->getAudienceInboxes($profile, $scope); + } - protected function getAudienceInboxes($pid, $scope = null) - { - $key = 'pf:services:follower:audience:' . $pid; - $domains = Cache::remember($key, 432000, function() use($pid) { - $profile = Profile::whereNull(['status', 'domain'])->find($pid); - if(!$profile) { - return []; - } - return $profile - ->followers() - ->get() - ->map(function($follow) { - return $follow->sharedInbox ?? $follow->inbox_url; - }) - ->filter() - ->unique() - ->values(); - }); + public static function softwareAudience($profile, $software = 'pixelfed') + { + return collect(self::audience($profile)) + ->filter(function($inbox) use($software) { + $domain = parse_url($inbox, PHP_URL_HOST); + if(!$domain) { + return false; + } + return InstanceService::software($domain) === strtolower($software); + }) + ->unique() + ->values() + ->toArray(); + } - if(!$domains || !$domains->count()) { - return []; - } + protected function getAudienceInboxes($pid, $scope = null) + { + $key = 'pf:services:follower:audience:' . $pid; + $bannedDomains = InstanceService::getBannedDomains(); + $domains = Cache::remember($key, 432000, function() use($pid, $bannedDomains) { + $profile = Profile::whereNull(['status', 'domain'])->find($pid); + if(!$profile) { + return []; + } + return DB::table('followers') + ->join('profiles', 'followers.profile_id', '=', 'profiles.id') + ->where('followers.following_id', $pid) + ->whereNotNull('profiles.inbox_url') + ->whereNull('profiles.deleted_at') + ->select('followers.profile_id', 'followers.following_id', 'profiles.id', 'profiles.user_id', 'profiles.deleted_at', 'profiles.sharedInbox', 'profiles.inbox_url') + ->get() + ->map(function($r) { + return $r->sharedInbox ?? $r->inbox_url; + }) + ->filter(function($r) use($bannedDomains) { + $domain = parse_url($r, PHP_URL_HOST); + return $r && !in_array($domain, $bannedDomains); + }) + ->unique() + ->values(); + }); - $banned = InstanceService::getBannedDomains(); + if(!$domains || !$domains->count()) { + return []; + } - if(!$banned || count($banned) === 0) { - return $domains->toArray(); - } + $banned = InstanceService::getBannedDomains(); - $res = $domains->filter(function($domain) use($banned) { - $parsed = parse_url($domain, PHP_URL_HOST); - return !in_array($parsed, $banned); - }) - ->values() - ->toArray(); + if(!$banned || count($banned) === 0) { + return $domains->toArray(); + } - return $res; - } + $res = $domains->filter(function($domain) use($banned) { + $parsed = parse_url($domain, PHP_URL_HOST); + return !in_array($parsed, $banned); + }) + ->values() + ->toArray(); - public static function mutualCount($pid, $mid) - { - return Cache::remember(self::CACHE_KEY . ':mutualcount:' . $pid . ':' . $mid, 3600, function() use($pid, $mid) { - return DB::table('followers as u') - ->join('followers as s', 'u.following_id', '=', 's.following_id') - ->where('s.profile_id', $mid) - ->where('u.profile_id', $pid) - ->count(); - }); - } + return $res; + } - public static function mutualIds($pid, $mid, $limit = 3) - { - $key = self::CACHE_KEY . ':mutualids:' . $pid . ':' . $mid . ':limit_' . $limit; - return Cache::remember($key, 3600, function() use($pid, $mid, $limit) { - return DB::table('followers as u') - ->join('followers as s', 'u.following_id', '=', 's.following_id') - ->where('s.profile_id', $mid) - ->where('u.profile_id', $pid) - ->limit($limit) - ->pluck('s.following_id') - ->toArray(); - }); - } + public static function mutualCount($pid, $mid) + { + return Cache::remember(self::CACHE_KEY . ':mutualcount:' . $pid . ':' . $mid, 3600, function() use($pid, $mid) { + return DB::table('followers as u') + ->join('followers as s', 'u.following_id', '=', 's.following_id') + ->where('s.profile_id', $mid) + ->where('u.profile_id', $pid) + ->count(); + }); + } - public static function delCache($id) - { - Redis::del(self::CACHE_KEY . $id); - Redis::del(self::FOLLOWING_KEY . $id); - Redis::del(self::FOLLOWERS_KEY . $id); - Cache::forget(self::FOLLOWERS_SYNC_KEY . $id); - Cache::forget(self::FOLLOWING_SYNC_KEY . $id); - } + public static function mutualIds($pid, $mid, $limit = 3) + { + $key = self::CACHE_KEY . ':mutualids:' . $pid . ':' . $mid . ':limit_' . $limit; + return Cache::remember($key, 3600, function() use($pid, $mid, $limit) { + return DB::table('followers as u') + ->join('followers as s', 'u.following_id', '=', 's.following_id') + ->where('s.profile_id', $mid) + ->where('u.profile_id', $pid) + ->limit($limit) + ->pluck('s.following_id') + ->toArray(); + }); + } + + public static function mutualAccounts($actorId, $profileId) + { + if($actorId == $profileId) { + return []; + } + $actorKey = self::FOLLOWING_KEY . $actorId; + $profileKey = self::FOLLOWERS_KEY . $profileId; + $key = self::FOLLOWERS_INTER_KEY . $actorId . ':' . $profileId; + $res = Redis::zinterstore($key, [$actorKey, $profileKey]); + if($res) { + return Redis::zrange($key, 0, -1); + } else { + return []; + } + } + + public static function delCache($id) + { + Redis::del(self::CACHE_KEY . $id); + Redis::del(self::FOLLOWING_KEY . $id); + Redis::del(self::FOLLOWERS_KEY . $id); + Cache::forget(self::FOLLOWERS_SYNC_KEY . $id); + Cache::forget(self::FOLLOWING_SYNC_KEY . $id); + } + + public static function localFollowerIds($pid, $limit = 0) + { + $key = self::FOLLOWERS_LOCAL_KEY . $pid; + $res = Cache::remember($key, 7200, function() use($pid) { + return DB::table('followers') + ->join('profiles', 'followers.profile_id', '=', 'profiles.id') + ->where('followers.following_id', $pid) + ->whereNotNull('profiles.user_id') + ->whereNull('profiles.deleted_at') + ->select('followers.profile_id', 'followers.following_id', 'profiles.id', 'profiles.user_id', 'profiles.deleted_at') + ->pluck('followers.profile_id'); + }); + return $limit ? + $res->take($limit)->values()->toArray() : + $res->values()->toArray(); + } } diff --git a/app/Services/GroupFeedService.php b/app/Services/GroupFeedService.php new file mode 100644 index 000000000..bf28b470e --- /dev/null +++ b/app/Services/GroupFeedService.php @@ -0,0 +1,88 @@ + 100) { + $stop = 100; + } + + return Redis::zrevrange(self::CACHE_KEY.$gid, $start, $stop); + } + + public static function getRankedMaxId($gid, $start = null, $limit = 10) + { + if (! $start) { + return []; + } + + return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$gid, $start, '-inf', [ + 'withscores' => true, + 'limit' => [1, $limit], + ])); + } + + public static function getRankedMinId($gid, $end = null, $limit = 10) + { + if (! $end) { + return []; + } + + return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$gid, '+inf', $end, [ + 'withscores' => true, + 'limit' => [0, $limit], + ])); + } + + public static function add($gid, $val) + { + if (self::count($gid) > self::FEED_LIMIT) { + if (config('database.redis.client') === 'phpredis') { + Redis::zpopmin(self::CACHE_KEY.$gid); + } + } + + return Redis::zadd(self::CACHE_KEY.$gid, $val, $val); + } + + public static function rem($gid, $val) + { + return Redis::zrem(self::CACHE_KEY.$gid, $val); + } + + public static function del($gid, $val) + { + return self::rem($gid, $val); + } + + public static function count($gid) + { + return Redis::zcard(self::CACHE_KEY.$gid); + } + + public static function warmCache($gid, $force = false, $limit = 100) + { + if (self::count($gid) == 0 || $force == true) { + Redis::del(self::CACHE_KEY.$gid); + $ids = GroupPost::whereGroupId($gid) + ->orderByDesc('id') + ->limit($limit) + ->pluck('id'); + foreach ($ids as $id) { + self::add($gid, $id); + } + + return 1; + } + } +} diff --git a/app/Services/GroupPostService.php b/app/Services/GroupPostService.php new file mode 100644 index 000000000..7295bda40 --- /dev/null +++ b/app/Services/GroupPostService.php @@ -0,0 +1,49 @@ +find($pid); + + if (! $gp) { + return null; + } + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($gp, new GroupPostTransformer()); + $res = $fractal->createData($resource)->toArray(); + + $res['pf_type'] = $gp['type']; + $res['url'] = $gp->url(); + + // if($gp['type'] == 'poll') { + // $status['poll'] = PollService::get($status['id']); + // } + //$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}"); + return $res; + }); + } + + public static function del($gid, $pid) + { + return Cache::forget(self::key($gid, $pid)); + } +} diff --git a/app/Services/GroupService.php b/app/Services/GroupService.php new file mode 100644 index 000000000..ac1a1a1c6 --- /dev/null +++ b/app/Services/GroupService.php @@ -0,0 +1,366 @@ +withoutRelations()->whereNull('status')->find($id); + + if(!$group) { + return null; + } + + $admin = $group->profile_id ? AccountService::get($group->profile_id) : null; + + return [ + 'id' => (string) $group->id, + 'name' => $group->name, + 'description' => $group->description, + 'short_description' => str_limit(strip_tags($group->description), 120), + 'category' => self::categoryById($group->category_id), + 'local' => (bool) $group->local, + 'url' => $group->url(), + 'shorturl' => url('/g/'.HashidService::encode($group->id)), + 'membership' => $group->getMembershipType(), + 'member_count' => $group->members()->whereJoinRequest(false)->count(), + 'verified' => false, + 'self' => null, + 'admin' => $admin, + 'config' => [ + 'recommended' => (bool) $group->recommended, + 'discoverable' => (bool) $group->discoverable, + 'activitypub' => (bool) $group->activitypub, + 'is_nsfw' => (bool) $group->is_nsfw, + 'dms' => (bool) $group->dms + ], + 'metadata' => $group->metadata, + 'created_at' => $group->created_at->toAtomString(), + ]; + } + ); + + if($pid) { + $res['self'] = self::getSelf($id, $pid); + } + + return $res; + } + + public static function del($id) + { + Cache::forget('ap:groups:object:' . $id); + return Cache::forget(self::key($id)); + } + + public static function getSelf($gid, $pid) + { + return Cache::remember( + self::key('self:gid-' . $gid . ':pid-' . $pid), + 3600, + function() use($gid, $pid) { + $group = Group::find($gid); + + if(!$gid || !$pid) { + return [ + 'is_member' => false, + 'role' => null, + 'is_requested' => null + ]; + } + + return [ + 'is_member' => $group->isMember($pid), + 'role' => $group->selfRole($pid), + 'is_requested' => optional($group->members()->whereProfileId($pid)->first())->join_request ?? false + ]; + } + ); + } + + public static function delSelf($gid, $pid) + { + Cache::forget(self::key("is_member:{$gid}:{$pid}")); + return Cache::forget(self::key('self:gid-' . $gid . ':pid-' . $pid)); + } + + public static function sidToGid($gid, $pid) + { + return Cache::remember(self::key('s2gid:' . $gid . ':' . $pid), 3600, function() use($gid, $pid) { + return optional(GroupPost::whereGroupId($gid)->whereStatusId($pid)->first())->id; + }); + } + + public static function membershipsByPid($pid) + { + return Cache::remember(self::key("mbpid:{$pid}"), 3600, function() use($pid) { + return GroupMember::whereProfileId($pid)->pluck('group_id'); + }); + } + + public static function config() + { + return [ + 'enabled' => config('exp.gps') ?? false, + 'limits' => [ + 'group' => [ + 'max' => 999, + 'federation' => false, + ], + + 'user' => [ + 'create' => [ + 'new' => true, + 'max' => 10 + ], + 'join' => [ + 'max' => 10 + ], + 'invite' => [ + 'max' => 20 + ] + ] + ], + 'guest' => [ + 'public' => false + ] + ]; + } + + public static function fetchRemote($url) + { + // todo: refactor this demo + $res = Helpers::fetchFromUrl($url); + + if(!$res || !isset($res['type']) || $res['type'] != 'Group') { + return false; + } + + $group = Group::whereRemoteUrl($url)->first(); + + if($group) { + return $group; + } + + $group = new Group; + $group->remote_url = $res['url']; + $group->name = $res['name']; + $group->inbox_url = $res['inbox']; + $group->metadata = [ + 'header' => [ + 'url' => $res['icon']['image']['url'] + ] + ]; + $group->description = Purify::clean($res['summary']); + $group->local = false; + $group->save(); + + return $group->url(); + } + + public static function log( + string $groupId, + string $profileId, + string $type = null, + array $meta = null, + string $itemType = null, + string $itemId = null + ) + { + // todo: truncate (some) metadata after XX days in cron/queue + $log = new GroupInteraction; + $log->group_id = $groupId; + $log->profile_id = $profileId; + $log->type = $type; + $log->item_type = $itemType; + $log->item_id = $itemId; + $log->metadata = $meta; + $log->save(); + } + + public static function getRejoinTimeout($gid, $pid) + { + $key = self::key('rejoin-timeout:gid-' . $gid . ':pid-' . $pid); + return Cache::has($key); + } + + public static function setRejoinTimeout($gid, $pid) + { + // todo: allow group admins to manually remove timeout + $key = self::key('rejoin-timeout:gid-' . $gid . ':pid-' . $pid); + return Cache::put($key, 1, 86400); + } + + public static function getMemberInboxes($id) + { + // todo: cache this, maybe add join/leave methods to this service to handle cache invalidation + $group = (new Group)->withoutRelations()->findOrFail($id); + if(!$group->local) { + return []; + } + $members = GroupMember::whereGroupId($id)->whereLocalProfile(false)->pluck('profile_id'); + return Profile::find($members)->map(function($u) { + return $u->sharedInbox ?? $u->inbox_url; + })->toArray(); + } + + public static function getInteractionLimits($gid, $pid) + { + return Cache::remember(self::key(":il:{$gid}:{$pid}"), 3600, function() use($gid, $pid) { + $limit = GroupLimit::whereGroupId($gid)->whereProfileId($pid)->first(); + if(!$limit) { + return [ + 'limits' => [ + 'can_post' => true, + 'can_comment' => true, + 'can_like' => true + ], + 'updated_at' => null + ]; + } + + return [ + 'limits' => $limit->limits, + 'updated_at' => $limit->updated_at->format('c') + ]; + }); + } + + public static function clearInteractionLimits($gid, $pid) + { + return Cache::forget(self::key(":il:{$gid}:{$pid}")); + } + + public static function canPost($gid, $pid) + { + $limits = self::getInteractionLimits($gid, $pid); + if($limits) { + return (bool) $limits['limits']['can_post']; + } else { + return true; + } + } + + public static function canComment($gid, $pid) + { + $limits = self::getInteractionLimits($gid, $pid); + if($limits) { + return (bool) $limits['limits']['can_comment']; + } else { + return true; + } + } + + public static function canLike($gid, $pid) + { + $limits = self::getInteractionLimits($gid, $pid); + if($limits) { + return (bool) $limits['limits']['can_like']; + } else { + return true; + } + } + + public static function categories($onlyActive = true) + { + return Cache::remember(self::key(':categories'), 2678400, function() use($onlyActive) { + return GroupCategory::when($onlyActive, function($q, $onlyActive) { + return $q->whereActive(true); + }) + ->orderBy('order') + ->pluck('name') + ->toArray(); + }); + } + + public static function categoryById($id) + { + return Cache::remember(self::key(':categorybyid:'.$id), 2678400, function() use($id) { + $category = GroupCategory::find($id); + if($category) { + return [ + 'name' => $category->name, + 'url' => url("/groups/explore/category/{$category->slug}") + ]; + } + return false; + }); + } + + public static function isMember($gid = false, $pid = false) + { + if(!$gid || !$pid) { + return false; + } + + $key = self::key("is_member:{$gid}:{$pid}"); + return Cache::remember($key, 3600, function() use($gid, $pid) { + return GroupMember::whereGroupId($gid) + ->whereProfileId($pid) + ->whereJoinRequest(false) + ->exists(); + }); + } + + public static function mutualGroups($cid = false, $pid = false, $exclude = []) + { + if(!$cid || !$pid) { + return [ + 'count' => 0, + 'groups' => [] + ]; + } + + $self = self::membershipsByPid($cid); + $user = self::membershipsByPid($pid); + + if(!$self->count() || !$user->count()) { + return [ + 'count' => 0, + 'groups' => [] + ]; + } + + $intersect = $self->intersect($user); + $count = $intersect->count(); + $groups = $intersect + ->values() + ->filter(function($id) use($exclude) { + return !in_array($id, $exclude); + }) + ->shuffle() + ->take(1) + ->map(function($id) { + return self::get($id); + }); + + return [ + 'count' => $count, + 'groups' => $groups + ]; + } +} diff --git a/app/Services/Groups/GroupAccountService.php b/app/Services/Groups/GroupAccountService.php new file mode 100644 index 000000000..2d86e4f43 --- /dev/null +++ b/app/Services/Groups/GroupAccountService.php @@ -0,0 +1,51 @@ +whereProfileId($pid)->first(); + if(!$membership) { + return []; + } + + return [ + 'joined' => $membership->created_at->format('c'), + 'role' => $membership->role, + 'local_group' => (bool) $membership->local_group, + 'local_profile' => (bool) $membership->local_profile, + ]; + }); + return $account; + } + + public static function del($gid, $pid) + { + $key = self::CACHE_KEY . $gid . ':' . $pid; + return Cache::forget($key); + } +} diff --git a/app/Services/Groups/GroupActivityPubService.php b/app/Services/Groups/GroupActivityPubService.php new file mode 100644 index 000000000..4c48b22c4 --- /dev/null +++ b/app/Services/Groups/GroupActivityPubService.php @@ -0,0 +1,311 @@ +first(); + if($group) { + return $group; + } + + $res = ActivityPubFetchService::get($url); + if(!$res) { + return $res; + } + $json = json_decode($res, true); + $group = self::validateGroup($json); + if(!$group) { + return false; + } + if($saveOnFetch) { + return self::storeGroup($group); + } + return $group; + } + + public static function fetchGroupPost($url, $saveOnFetch = true) + { + $group = GroupPost::where('remote_url', $url)->first(); + + if($group) { + return $group; + } + + $res = ActivityPubFetchService::get($url); + if(!$res) { + return 'invalid res'; + } + $json = json_decode($res, true); + if(!$json) { + return 'invalid json'; + } + if(isset($json['inReplyTo'])) { + $comment = self::validateGroupComment($json); + return self::storeGroupComment($comment); + } + + $group = self::validateGroupPost($json); + if($saveOnFetch) { + return self::storeGroupPost($group); + } + return $group; + } + + public static function validateGroup($obj) + { + $validator = Validator::make($obj, [ + '@context' => 'required', + 'id' => ['required', 'url', new ValidUrl], + 'type' => 'required|in:Group', + 'preferredUsername' => 'required', + 'name' => 'required', + 'url' => ['sometimes', 'url', new ValidUrl], + 'inbox' => ['required', 'url', new ValidUrl], + 'outbox' => ['required', 'url', new ValidUrl], + 'followers' => ['required', 'url', new ValidUrl], + 'attributedTo' => 'required', + 'summary' => 'sometimes', + 'publicKey' => 'required', + 'publicKey.id' => 'required', + 'publicKey.owner' => ['required', 'url', 'same:id', new ValidUrl], + 'publicKey.publicKeyPem' => 'required', + ]); + + if($validator->fails()) { + return false; + } + + return $validator->validated(); + } + + public static function validateGroupPost($obj) + { + $validator = Validator::make($obj, [ + '@context' => 'required', + 'id' => ['required', 'url', new ValidUrl], + 'type' => 'required|in:Page,Note', + 'to' => 'required|array', + 'to.*' => ['required', 'url', new ValidUrl], + 'cc' => 'sometimes|array', + 'cc.*' => ['sometimes', 'url', new ValidUrl], + 'url' => ['sometimes', 'url', new ValidUrl], + 'attributedTo' => 'required', + 'name' => 'sometimes', + 'target' => 'sometimes', + 'audience' => 'sometimes', + 'inReplyTo' => 'sometimes', + 'content' => 'sometimes', + 'mediaType' => 'sometimes', + 'sensitive' => 'sometimes', + 'attachment' => 'sometimes', + 'published' => 'required', + ]); + + if($validator->fails()) { + return false; + } + + return $validator->validated(); + } + + public static function validateGroupComment($obj) + { + $validator = Validator::make($obj, [ + '@context' => 'required', + 'id' => ['required', 'url', new ValidUrl], + 'type' => 'required|in:Note', + 'to' => 'required|array', + 'to.*' => ['required', 'url', new ValidUrl], + 'cc' => 'sometimes|array', + 'cc.*' => ['sometimes', 'url', new ValidUrl], + 'url' => ['sometimes', 'url', new ValidUrl], + 'attributedTo' => 'required', + 'name' => 'sometimes', + 'target' => 'sometimes', + 'audience' => 'sometimes', + 'inReplyTo' => 'sometimes', + 'content' => 'sometimes', + 'mediaType' => 'sometimes', + 'sensitive' => 'sometimes', + 'published' => 'required', + ]); + + if($validator->fails()) { + return $validator->errors(); + return false; + } + + return $validator->validated(); + } + + public static function getGroupFromPostActivity($groupPost) + { + if(isset($groupPost['audience']) && is_string($groupPost['audience'])) { + return $groupPost['audience']; + } + + if( + isset( + $groupPost['target'], + $groupPost['target']['type'], + $groupPost['target']['attributedTo'] + ) && $groupPost['target']['type'] == 'Collection' + ) { + return $groupPost['target']['attributedTo']; + } + + return false; + } + + public static function getActorFromPostActivity($groupPost) + { + if(!isset($groupPost['attributedTo'])) { + return false; + } + + $field = $groupPost['attributedTo']; + + if(is_string($field)) { + return $field; + } + + if(is_array($field) && count($field) === 1) { + if( + isset( + $field[0]['id'], + $field[0]['type'] + ) && + $field[0]['type'] === 'Person' && + is_string($field[0]['id']) + ) { + return $field[0]['id']; + } + } + + return false; + } + + public static function getCaptionFromPostActivity($groupPost) + { + if(!isset($groupPost['name']) && isset($groupPost['content'])) { + return Purify::clean(strip_tags($groupPost['content'])); + } + + if(isset($groupPost['name'], $groupPost['content'])) { + return Purify::clean(strip_tags($groupPost['name'])) . Purify::clean(strip_tags($groupPost['content'])); + } + } + + public static function getSensitiveFromPostActivity($groupPost) + { + if(!isset($groupPost['sensitive'])) { + return true; + } + + if(isset($groupPost['sensitive']) && !is_bool($groupPost['sensitive'])) { + return true; + } + + return boolval($groupPost['sensitive']); + } + + public static function storeGroup($activity) + { + $group = new Group; + $group->profile_id = null; + $group->category_id = 1; + $group->name = $activity['name'] ?? 'Untitled Group'; + $group->description = isset($activity['summary']) ? Purify::clean($activity['summary']) : null; + $group->is_private = false; + $group->local_only = false; + $group->metadata = []; + $group->local = false; + $group->remote_url = $activity['id']; + $group->inbox_url = $activity['inbox']; + $group->activitypub = true; + $group->save(); + + return $group; + } + + public static function storeGroupPost($groupPost) + { + $groupUrl = self::getGroupFromPostActivity($groupPost); + if(!$groupUrl) { + return; + } + $group = self::fetchGroup($groupUrl, true); + if(!$group) { + return; + } + $actorUrl = self::getActorFromPostActivity($groupPost); + $actor = Helpers::profileFetch($actorUrl); + $caption = self::getCaptionFromPostActivity($groupPost); + $sensitive = self::getSensitiveFromPostActivity($groupPost); + $model = GroupPost::firstOrCreate( + [ + 'remote_url' => $groupPost['id'], + ], [ + 'group_id' => $group->id, + 'profile_id' => $actor->id, + 'type' => 'text', + 'caption' => $caption, + 'visibility' => 'public', + 'is_nsfw' => $sensitive, + ] + ); + return $model; + } + + public static function storeGroupComment($groupPost) + { + $groupUrl = self::getGroupFromPostActivity($groupPost); + if(!$groupUrl) { + return; + } + $group = self::fetchGroup($groupUrl, true); + if(!$group) { + return; + } + $actorUrl = self::getActorFromPostActivity($groupPost); + $actor = Helpers::profileFetch($actorUrl); + $caption = self::getCaptionFromPostActivity($groupPost); + $sensitive = self::getSensitiveFromPostActivity($groupPost); + $parentPost = self::fetchGroupPost($groupPost['inReplyTo']); + $model = GroupComment::firstOrCreate( + [ + 'remote_url' => $groupPost['id'], + ], [ + 'group_id' => $group->id, + 'profile_id' => $actor->id, + 'status_id' => $parentPost->id, + 'type' => 'text', + 'caption' => $caption, + 'visibility' => 'public', + 'is_nsfw' => $sensitive, + 'local' => $actor->private_key != null + ] + ); + return $model; + } +} diff --git a/app/Services/Groups/GroupCommentService.php b/app/Services/Groups/GroupCommentService.php new file mode 100644 index 000000000..52eeee533 --- /dev/null +++ b/app/Services/Groups/GroupCommentService.php @@ -0,0 +1,50 @@ +find($pid); + + if(!$gp) { + return null; + } + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($gp, new GroupPostTransformer()); + $res = $fractal->createData($resource)->toArray(); + + $res['pf_type'] = 'group:post:comment'; + $res['url'] = $gp->url(); + // if($gp['type'] == 'poll') { + // $status['poll'] = PollService::get($status['id']); + // } + //$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}"); + return $res; + }); + } + + public static function del($gid, $pid) + { + return Cache::forget(self::key($gid, $pid)); + } +} diff --git a/app/Services/Groups/GroupFeedService.php b/app/Services/Groups/GroupFeedService.php new file mode 100644 index 000000000..a2a87be1d --- /dev/null +++ b/app/Services/Groups/GroupFeedService.php @@ -0,0 +1,95 @@ + 100) { + $stop = 100; + } + + return Redis::zrevrange(self::CACHE_KEY . $gid, $start, $stop); + } + + public static function getRankedMaxId($gid, $start = null, $limit = 10) + { + if(!$start) { + return []; + } + + return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY . $gid, $start, '-inf', [ + 'withscores' => true, + 'limit' => [1, $limit] + ])); + } + + public static function getRankedMinId($gid, $end = null, $limit = 10) + { + if(!$end) { + return []; + } + + return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY . $gid, '+inf', $end, [ + 'withscores' => true, + 'limit' => [0, $limit] + ])); + } + + public static function add($gid, $val) + { + if(self::count($gid) > self::FEED_LIMIT) { + if(config('database.redis.client') === 'phpredis') { + Redis::zpopmin(self::CACHE_KEY . $gid); + } + } + + return Redis::zadd(self::CACHE_KEY . $gid, $val, $val); + } + + public static function rem($gid, $val) + { + return Redis::zrem(self::CACHE_KEY . $gid, $val); + } + + public static function del($gid, $val) + { + return self::rem($gid, $val); + } + + public static function count($gid) + { + return Redis::zcard(self::CACHE_KEY . $gid); + } + + public static function warmCache($gid, $force = false, $limit = 100) + { + if(self::count($gid) == 0 || $force == true) { + Redis::del(self::CACHE_KEY . $gid); + $ids = GroupPost::whereGroupId($gid) + ->orderByDesc('id') + ->limit($limit) + ->pluck('id'); + foreach($ids as $id) { + self::add($gid, $id); + } + return 1; + } + } +} diff --git a/app/Services/Groups/GroupHashtagService.php b/app/Services/Groups/GroupHashtagService.php new file mode 100644 index 000000000..6553850f0 --- /dev/null +++ b/app/Services/Groups/GroupHashtagService.php @@ -0,0 +1,28 @@ + $tag->name, + 'slug' => Str::slug($tag->name), + ]; + }); + } +} diff --git a/app/Services/Groups/GroupMediaService.php b/app/Services/Groups/GroupMediaService.php new file mode 100644 index 000000000..0200e3a56 --- /dev/null +++ b/app/Services/Groups/GroupMediaService.php @@ -0,0 +1,114 @@ +orderBy('order')->get(); + if(!$media) { + return []; + } + $medias = $media->map(function($media) { + return [ + 'id' => (string) $media->id, + 'type' => 'Document', + 'url' => $media->url(), + 'preview_url' => $media->url(), + 'remote_url' => $media->url, + 'description' => $media->cw_summary, + 'blurhash' => $media->blurhash ?? 'U4Rfzst8?bt7ogayj[j[~pfQ9Goe%Mj[WBay' + ]; + }); + return $medias->toArray(); + }); + } + + public static function getMastodon($id) + { + $media = self::get($id); + if(!$media) { + return []; + } + $medias = collect($media) + ->map(function($media) { + $mime = $media['mime'] ? explode('/', $media['mime']) : false; + unset( + $media['optimized_url'], + $media['license'], + $media['is_nsfw'], + $media['orientation'], + $media['filter_name'], + $media['filter_class'], + $media['mime'], + $media['hls_manifest'] + ); + + $media['type'] = $mime ? strtolower($mime[0]) : 'unknown'; + return $media; + }) + ->filter(function($m) { + return $m && isset($m['url']); + }) + ->values(); + + return $medias->toArray(); + } + + public static function del($statusId) + { + return Cache::forget(self::CACHE_KEY . $statusId); + } + + public static function activitypub($statusId) + { + $status = self::get($statusId); + if(!$status) { + return []; + } + + return collect($status)->map(function($s) { + $license = isset($s['license']) && $s['license']['title'] ? $s['license']['title'] : null; + return [ + 'type' => 'Document', + 'mediaType' => $s['mime'], + 'url' => $s['url'], + 'name' => $s['description'], + 'summary' => $s['description'], + 'blurhash' => $s['blurhash'], + 'license' => $license + ]; + }); + } +} diff --git a/app/Services/Groups/GroupPostService.php b/app/Services/Groups/GroupPostService.php new file mode 100644 index 000000000..a043be134 --- /dev/null +++ b/app/Services/Groups/GroupPostService.php @@ -0,0 +1,83 @@ +find($pid); + + if(!$gp) { + return null; + } + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($gp, new GroupPostTransformer()); + $res = $fractal->createData($resource)->toArray(); + + $res['pf_type'] = $gp['type']; + $res['url'] = $gp->url(); + // if($gp['type'] == 'poll') { + // $status['poll'] = PollService::get($status['id']); + // } + //$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}"); + return $res; + }); + } + + public static function del($gid, $pid) + { + return Cache::forget(self::key($gid, $pid)); + } + + public function getStatus(Request $request) + { + $gid = $request->input('gid'); + $sid = $request->input('sid'); + $pid = optional($request->user())->profile_id ?? false; + + $group = Group::findOrFail($gid); + + if($group->is_private) { + abort_if(!$group->isMember($pid), 404); + } + + $gp = GroupPost::whereGroupId($group->id)->whereId($sid)->firstOrFail(); + + $status = GroupPostService::get($gp['group_id'], $gp['id']); + if(!$status) { + return false; + } + $status['reply_count'] = $gp['reply_count']; + $status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']); + $status['favourites_count'] = GroupsLikeService::count($gp['id']); + $status['pf_type'] = $gp['type']; + $status['visibility'] = 'public'; + $status['url'] = $gp->url(); + $status['account']['url'] = url("/groups/{$gp->group_id}/user/{$gp->profile_id}"); + + // if($gp['type'] == 'poll') { + // $status['poll'] = PollService::get($status['id']); + // } + + return $status; + } +} diff --git a/app/Services/Groups/GroupsLikeService.php b/app/Services/Groups/GroupsLikeService.php new file mode 100644 index 000000000..e2daa1e71 --- /dev/null +++ b/app/Services/Groups/GroupsLikeService.php @@ -0,0 +1,85 @@ + 400) { + Redis::zpopmin(self::CACHE_SET_KEY . $profileId); + } + + return Redis::zadd(self::CACHE_SET_KEY . $profileId, $statusId, $statusId); + } + + public static function setCount($id) + { + return Redis::zcard(self::CACHE_SET_KEY . $id); + } + + public static function setRem($profileId, $val) + { + return Redis::zrem(self::CACHE_SET_KEY . $profileId, $val); + } + + public static function get($profileId, $start = 0, $stop = 10) + { + if($stop > 100) { + $stop = 100; + } + + return Redis::zrevrange(self::CACHE_SET_KEY . $profileId, $start, $stop); + } + + public static function remove($profileId, $statusId) + { + $key = self::CACHE_KEY . $profileId . ':' . $statusId; + Cache::decrement(self::CACHE_POST_KEY . $statusId); + //Cache::forget('pf:services:likes:liked_by:'.$statusId); + self::setRem($profileId, $statusId); + return Cache::put($key, false, 86400); + } + + public static function liked($profileId, $statusId) + { + $key = self::CACHE_KEY . $profileId . ':' . $statusId; + return Cache::remember($key, 900, function() use($profileId, $statusId) { + return GroupLike::whereProfileId($profileId)->whereStatusId($statusId)->exists(); + }); + } + + public static function likedBy($status) + { + $empty = [ + 'username' => null, + 'others' => false + ]; + + return $empty; + } + + public static function count($id) + { + return Cache::get(self::CACHE_POST_KEY . $id, 0); + } + +} diff --git a/app/Services/HashidService.php b/app/Services/HashidService.php index 914d24321..e12c10599 100644 --- a/app/Services/HashidService.php +++ b/app/Services/HashidService.php @@ -2,54 +2,38 @@ namespace App\Services; -use Cache; +class HashidService +{ + public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; -class HashidService { + public static function encode($id, $minLimit = true) + { + if (! is_numeric($id) || $id > PHP_INT_MAX) { + return null; + } - public const MIN_LIMIT = 15; - public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + $cmap = self::CMAP; + $base = strlen($cmap); + $shortcode = ''; + while ($id) { + $id = ($id - ($r = $id % $base)) / $base; + $shortcode = $cmap[$r].$shortcode; + } - public static function encode($id, $minLimit = true) - { - if(!is_numeric($id) || $id > PHP_INT_MAX) { - return null; - } + return $shortcode; + } - if($minLimit && strlen($id) < self::MIN_LIMIT) { - return null; - } - - $key = "hashids:{$id}"; - return Cache::remember($key, now()->hours(48), function() use($id) { - $cmap = self::CMAP; - $base = strlen($cmap); - $shortcode = ''; - while($id) { - $id = ($id - ($r = $id % $base)) / $base; - $shortcode = $cmap[$r] . $shortcode; - } - return $shortcode; - }); - } - - public static function decode($short) - { - $len = strlen($short); - if($len < 3 || $len > 11) { - return null; - } - $id = 0; - foreach(str_split($short) as $needle) { - $pos = strpos(self::CMAP, $needle); - // if(!$pos) { - // return null; - // } - $id = ($id*64) + $pos; - } - if(strlen($id) < self::MIN_LIMIT) { - return null; - } - return $id; - } + public static function decode($short = false) + { + if (! $short) { + return; + } + $id = 0; + foreach (str_split($short) as $needle) { + $pos = strpos(self::CMAP, $needle); + $id = ($id * 64) + $pos; + } + return $id; + } } diff --git a/app/Services/HashtagFollowService.php b/app/Services/HashtagFollowService.php new file mode 100644 index 000000000..d4f93f404 --- /dev/null +++ b/app/Services/HashtagFollowService.php @@ -0,0 +1,72 @@ +lazyById(20, 'id') as $h) { + if($h) { + self::add($h->hashtag_id, $h->profile_id); + } + } + + self::setWarm($hid); + + return self::get($hid); + } + + public static function isWarm($hid) + { + return Redis::zcount(self::CACHE_KEY . $hid, 0, -1) ?? Redis::zscore(self::CACHE_WARMED, $hid) != null; + } + + public static function setWarm($hid) + { + return Redis::zadd(self::CACHE_WARMED, $hid, $hid); + } +} diff --git a/app/Services/HashtagRelatedService.php b/app/Services/HashtagRelatedService.php new file mode 100644 index 000000000..b96483987 --- /dev/null +++ b/app/Services/HashtagRelatedService.php @@ -0,0 +1,38 @@ +first(); + if(!$tag) { + return []; + } + return $tag->related_tags; + } + + public static function fetchRelatedTags($tag) + { + $res = StatusHashtag::query() + ->select('h2.name', DB::raw('COUNT(*) as related_count')) + ->join('status_hashtags as hs2', function ($join) { + $join->on('status_hashtags.status_id', '=', 'hs2.status_id') + ->whereRaw('status_hashtags.hashtag_id != hs2.hashtag_id'); + }) + ->join('hashtags as h1', 'status_hashtags.hashtag_id', '=', 'h1.id') + ->join('hashtags as h2', 'hs2.hashtag_id', '=', 'h2.id') + ->where('h1.name', '=', $tag) + ->groupBy('h2.name') + ->orderBy('related_count', 'desc') + ->limit(30) + ->get(); + + return $res; + } +} diff --git a/app/Services/HashtagService.php b/app/Services/HashtagService.php index 87f895a65..84fd1986b 100644 --- a/app/Services/HashtagService.php +++ b/app/Services/HashtagService.php @@ -8,65 +8,80 @@ use App\Hashtag; use App\StatusHashtag; use App\HashtagFollow; -class HashtagService { +class HashtagService +{ + const FOLLOW_KEY = 'pf:services:hashtag:following:v1:'; + const FOLLOW_PIDS_KEY = 'pf:services:hashtag-follows:v1:'; - const FOLLOW_KEY = 'pf:services:hashtag:following:'; + public static function get($id) + { + return Cache::remember('services:hashtag:by_id:' . $id, 3600, function() use($id) { + $tag = Hashtag::find($id); + if(!$tag) { + return []; + } + return [ + 'name' => $tag->name, + 'slug' => $tag->slug, + ]; + }); + } - public static function get($id) - { - return Cache::remember('services:hashtag:by_id:' . $id, 3600, function() use($id) { - $tag = Hashtag::find($id); - if(!$tag) { - return []; - } - return [ - 'name' => $tag->name, - 'slug' => $tag->slug, - ]; - }); - } + public static function count($id) + { + return Cache::remember('services:hashtag:total-count:by_id:' . $id, 300, function() use($id) { + $tag = Hashtag::find($id); + return $tag ? $tag->cached_count ?? 0 : 0; + }); + } - public static function count($id) - { - return Cache::remember('services:hashtag:public-count:by_id:' . $id, 86400, function() use($id) { - return StatusHashtag::whereHashtagId($id)->whereStatusVisibility('public')->count(); - }); - } + public static function isFollowing($pid, $hid) + { + $res = Redis::zscore(self::FOLLOW_KEY . $hid, $pid); + if($res) { + return true; + } - public static function isFollowing($pid, $hid) - { - $res = Redis::zscore(self::FOLLOW_KEY . $pid, $hid); - if($res) { - return true; - } + $synced = Cache::get(self::FOLLOW_KEY . 'acct:' . $pid . ':synced'); + if(!$synced) { + $tags = HashtagFollow::whereProfileId($pid) + ->get() + ->each(function($tag) use($pid) { + self::follow($pid, $tag->hashtag_id); + }); + Cache::set(self::FOLLOW_KEY . 'acct:' . $pid . ':synced', true, 1209600); - $synced = Cache::get(self::FOLLOW_KEY . $pid . ':synced'); - if(!$synced) { - $tags = HashtagFollow::whereProfileId($pid) - ->get() - ->each(function($tag) use($pid) { - self::follow($pid, $tag->hashtag_id); - }); - Cache::set(self::FOLLOW_KEY . $pid . ':synced', true, 1209600); + return (bool) Redis::zscore(self::FOLLOW_KEY . $hid, $pid) >= 1; + } - return (bool) Redis::zscore(self::FOLLOW_KEY . $pid, $hid) > 1; - } + return false; + } - return false; - } + public static function follow($pid, $hid) + { + Cache::forget(self::FOLLOW_PIDS_KEY . $hid); + return Redis::zadd(self::FOLLOW_KEY . $hid, $pid, $pid); + } - public static function follow($pid, $hid) - { - return Redis::zadd(self::FOLLOW_KEY . $pid, $hid, $hid); - } + public static function unfollow($pid, $hid) + { + Cache::forget(self::FOLLOW_PIDS_KEY . $hid); + return Redis::zrem(self::FOLLOW_KEY . $hid, $pid); + } - public static function unfollow($pid, $hid) - { - return Redis::zrem(self::FOLLOW_KEY . $pid, $hid); - } + public static function following($hid, $start = 0, $limit = 10) + { + $synced = Cache::get(self::FOLLOW_KEY . 'acct-following:' . $hid . ':synced'); + if(!$synced) { + $tags = HashtagFollow::whereHashtagId($hid) + ->get() + ->each(function($tag) use($hid) { + self::follow($tag->profile_id, $hid); + }); + Cache::set(self::FOLLOW_KEY . 'acct-following:' . $hid . ':synced', true, 1209600); - public static function following($pid, $start = 0, $limit = 10) - { - return Redis::zrevrange(self::FOLLOW_KEY . $pid, $start, $limit); - } + return Redis::zrevrange(self::FOLLOW_KEY . $hid, $start, $limit); + } + return Redis::zrevrange(self::FOLLOW_KEY . $hid, $start, $limit); + } } diff --git a/app/Services/HomeTimelineService.php b/app/Services/HomeTimelineService.php new file mode 100644 index 000000000..08d990591 --- /dev/null +++ b/app/Services/HomeTimelineService.php @@ -0,0 +1,114 @@ + 100) { + $stop = 100; + } + + return Redis::zrevrange(self::CACHE_KEY . $id, $start, $stop); + } + + public static function getRankedMaxId($id, $start = null, $limit = 10) + { + if(!$start) { + return []; + } + + return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY . $id, $start, '-inf', [ + 'withscores' => true, + 'limit' => [1, $limit - 1] + ])); + } + + public static function getRankedMinId($id, $end = null, $limit = 10) + { + if(!$end) { + return []; + } + + return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY . $id, '+inf', $end, [ + 'withscores' => true, + 'limit' => [0, $limit] + ])); + } + + public static function add($id, $val) + { + if(self::count($id) >= 400) { + Redis::zpopmin(self::CACHE_KEY . $id); + } + + return Redis::zadd(self::CACHE_KEY .$id, $val, $val); + } + + public static function rem($id, $val) + { + return Redis::zrem(self::CACHE_KEY . $id, $val); + } + + public static function count($id) + { + return Redis::zcard(self::CACHE_KEY . $id); + } + + public static function warmCache($id, $force = false, $limit = 100, $returnIds = false) + { + if(self::count($id) == 0 || $force == true) { + Redis::del(self::CACHE_KEY . $id); + $following = Cache::remember('profile:following:'.$id, 1209600, function() use($id) { + $following = Follower::whereProfileId($id)->pluck('following_id'); + return $following->push($id)->toArray(); + }); + + $minId = SnowflakeService::byDate(now()->subMonths(6)); + + $filters = UserFilterService::filters($id); + + if($filters && count($filters)) { + $following = array_diff($following, $filters); + } + + $domainBlocks = UserDomainBlock::whereProfileId($id)->pluck('domain')->toArray(); + + $ids = Status::where('id', '>', $minId) + ->whereIn('profile_id', $following) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereIn('visibility',['public', 'unlisted', 'private']) + ->orderByDesc('id') + ->limit($limit) + ->pluck('id'); + + foreach($ids as $pid) { + $status = StatusService::get($pid, false); + if(!$status || !isset($status['account'], $status['url'])) { + continue; + } + if($domainBlocks && count($domainBlocks)) { + $domain = strtolower(parse_url($status['url'], PHP_URL_HOST)); + if(in_array($domain, $domainBlocks)) { + continue; + } + } + self::add($id, $pid); + } + + return $returnIds ? $ids : 1; + } + return 0; + } +} diff --git a/app/Services/InstanceService.php b/app/Services/InstanceService.php index 2ad991063..0a6255ad2 100644 --- a/app/Services/InstanceService.php +++ b/app/Services/InstanceService.php @@ -2,76 +2,86 @@ namespace App\Services; -use Cache; use App\Instance; use App\Util\Blurhash\Blurhash; -use App\Services\ConfigCacheService; +use Cache; class InstanceService { const CACHE_KEY_BY_DOMAIN = 'pf:services:instance:by_domain:'; - const CACHE_KEY_BANNED_DOMAINS = 'instances:banned:domains'; - const CACHE_KEY_UNLISTED_DOMAINS = 'instances:unlisted:domains'; - const CACHE_KEY_NSFW_DOMAINS = 'instances:auto_cw:domains'; - const CACHE_KEY_STATS = 'pf:services:instances:stats'; - const CACHE_KEY_BANNER_BLURHASH = 'pf:services:instance:header-blurhash:v1'; - public function __construct() - { - ini_set('memory_limit', config('pixelfed.memory_limit', '1024M')); - } + const CACHE_KEY_BANNED_DOMAINS = 'instances:banned:domains'; - public static function getByDomain($domain) - { - return Cache::remember(self::CACHE_KEY_BY_DOMAIN.$domain, 3600, function() use($domain) { - return Instance::whereDomain($domain)->first(); - }); - } + const CACHE_KEY_UNLISTED_DOMAINS = 'instances:unlisted:domains'; - public static function getBannedDomains() - { - return Cache::remember(self::CACHE_KEY_BANNED_DOMAINS, 1209600, function() { - return Instance::whereBanned(true)->pluck('domain')->toArray(); - }); - } + const CACHE_KEY_NSFW_DOMAINS = 'instances:auto_cw:domains'; - public static function getUnlistedDomains() - { - return Cache::remember(self::CACHE_KEY_UNLISTED_DOMAINS, 1209600, function() { - return Instance::whereUnlisted(true)->pluck('domain')->toArray(); - }); - } + const CACHE_KEY_STATS = 'pf:services:instances:stats'; - public static function getNsfwDomains() - { - return Cache::remember(self::CACHE_KEY_NSFW_DOMAINS, 1209600, function() { - return Instance::whereAutoCw(true)->pluck('domain')->toArray(); - }); - } + const CACHE_KEY_TOTAL_POSTS = 'pf:services:instances:self:total-posts'; - public static function software($domain) - { - $key = 'instances:software:' . strtolower($domain); - return Cache::remember($key, 86400, function() use($domain) { - $instance = Instance::whereDomain($domain)->first(); - if(!$instance) { - return; - } - return $instance->software; - }); - } + const CACHE_KEY_BANNER_BLURHASH = 'pf:services:instance:header-blurhash:v1'; - public static function stats() - { - return Cache::remember(self::CACHE_KEY_STATS, 86400, function() { - return [ - 'total_count' => Instance::count(), - 'new_count' => Instance::where('created_at', '>', now()->subDays(14))->count(), - 'banned_count' => Instance::whereBanned(true)->count(), - 'nsfw_count' => Instance::whereAutoCw(true)->count() - ]; - }); - } + const CACHE_KEY_API_PEERS_LIST = 'pf:services:instance:api:peers:list:v0'; + + public function __construct() + { + ini_set('memory_limit', config('pixelfed.memory_limit', '1024M')); + } + + public static function getByDomain($domain) + { + return Cache::remember(self::CACHE_KEY_BY_DOMAIN.$domain, 3600, function () use ($domain) { + return Instance::whereDomain($domain)->first(); + }); + } + + public static function getBannedDomains() + { + return Cache::remember(self::CACHE_KEY_BANNED_DOMAINS, 1209600, function () { + return Instance::whereBanned(true)->pluck('domain')->toArray(); + }); + } + + public static function getUnlistedDomains() + { + return Cache::remember(self::CACHE_KEY_UNLISTED_DOMAINS, 1209600, function () { + return Instance::whereUnlisted(true)->pluck('domain')->toArray(); + }); + } + + public static function getNsfwDomains() + { + return Cache::remember(self::CACHE_KEY_NSFW_DOMAINS, 1209600, function () { + return Instance::whereAutoCw(true)->pluck('domain')->toArray(); + }); + } + + public static function software($domain) + { + $key = 'instances:software:'.strtolower($domain); + + return Cache::remember($key, 86400, function () use ($domain) { + $instance = Instance::whereDomain($domain)->first(); + if (! $instance) { + return; + } + + return $instance->software; + }); + } + + public static function stats() + { + return Cache::remember(self::CACHE_KEY_STATS, 86400, function () { + return [ + 'total_count' => Instance::count(), + 'new_count' => Instance::where('created_at', '>', now()->subDays(14))->count(), + 'banned_count' => Instance::whereBanned(true)->count(), + 'nsfw_count' => Instance::whereAutoCw(true)->count(), + ]; + }); + } public static function refresh() { @@ -79,6 +89,7 @@ class InstanceService Cache::forget(self::CACHE_KEY_UNLISTED_DOMAINS); Cache::forget(self::CACHE_KEY_NSFW_DOMAINS); Cache::forget(self::CACHE_KEY_STATS); + Cache::forget(self::CACHE_KEY_API_PEERS_LIST); self::getBannedDomains(); self::getUnlistedDomains(); @@ -87,52 +98,57 @@ class InstanceService return true; } + public static function totalLocalStatuses() + { + return config_cache('instance.stats.total_local_posts'); + } + public static function headerBlurhash() { - return Cache::rememberForever(self::CACHE_KEY_BANNER_BLURHASH, function() { - if(str_ends_with(config_cache('app.banner_image'), 'headers/default.jpg')) { - return 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt'; - } - $cached = config_cache('instance.banner.blurhash'); + return Cache::rememberForever(self::CACHE_KEY_BANNER_BLURHASH, function () { + if (str_ends_with(config_cache('app.banner_image'), 'headers/default.jpg')) { + return 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt'; + } + $cached = config_cache('instance.banner.blurhash'); - if($cached) { - return $cached; - } + if ($cached) { + return $cached; + } - $file = config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')); + $file = config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')); - $image = imagecreatefromstring(file_get_contents($file)); - if(!$image) { - return 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt'; - } - $width = imagesx($image); - $height = imagesy($image); + $image = imagecreatefromstring(file_get_contents($file)); + if (! $image) { + return 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt'; + } + $width = imagesx($image); + $height = imagesy($image); - $pixels = []; - for ($y = 0; $y < $height; ++$y) { - $row = []; - for ($x = 0; $x < $width; ++$x) { - $index = imagecolorat($image, $x, $y); - $colors = imagecolorsforindex($image, $index); + $pixels = []; + for ($y = 0; $y < $height; $y++) { + $row = []; + for ($x = 0; $x < $width; $x++) { + $index = imagecolorat($image, $x, $y); + $colors = imagecolorsforindex($image, $index); - $row[] = [$colors['red'], $colors['green'], $colors['blue']]; - } - $pixels[] = $row; - } + $row[] = [$colors['red'], $colors['green'], $colors['blue']]; + } + $pixels[] = $row; + } - // Free the allocated GdImage object from memory: - imagedestroy($image); + // Free the allocated GdImage object from memory: + imagedestroy($image); - $components_x = 4; - $components_y = 4; - $blurhash = Blurhash::encode($pixels, $components_x, $components_y); - if(strlen($blurhash) > 191) { - return 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt'; - } + $components_x = 4; + $components_y = 4; + $blurhash = Blurhash::encode($pixels, $components_x, $components_y); + if (strlen($blurhash) > 191) { + return 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt'; + } - ConfigCacheService::put('instance.banner.blurhash', $blurhash); + ConfigCacheService::put('instance.banner.blurhash', $blurhash); - return $blurhash; - }); + return $blurhash; + }); } } diff --git a/app/Services/Internal/BeagleService.php b/app/Services/Internal/BeagleService.php new file mode 100644 index 000000000..0f284e93c --- /dev/null +++ b/app/Services/Internal/BeagleService.php @@ -0,0 +1,135 @@ +addDays(7), function () { + try { + $res = Http::withOptions(['allow_redirects' => false]) + ->timeout(5) + ->connectTimeout(5) + ->retry(2, 500) + ->get('https://beagle.pixelfed.net/api/v1/common/suggestions/rules'); + } catch (RequestException $e) { + return; + } catch (ConnectionException $e) { + return; + } catch (Exception $e) { + return; + } + + if (! $res->ok()) { + return; + } + + $json = $res->json(); + + if (! isset($json['rule_suggestions']) || ! count($json['rule_suggestions'])) { + return []; + } + + return $json['rule_suggestions']; + }); + } + + public static function getDiscover() + { + if ((bool) config_cache('federation.activitypub.enabled') == false) { + return []; + } + + if ((bool) config('instance.discover.beagle_api') == false) { + return []; + } + + return Cache::remember(self::DISCOVER_CACHE_KEY, now()->addHours(6), function () { + try { + $res = Http::withOptions(['allow_redirects' => false]) + ->withHeaders([ + 'X-Pixelfed-Api' => 1, + ])->timeout(5) + ->connectTimeout(5) + ->retry(2, 500) + ->get('https://beagle.pixelfed.net/api/v1/discover'); + } catch (RequestException $e) { + return; + } catch (ConnectionException $e) { + return; + } catch (Exception $e) { + return; + } + + if (! $res->ok()) { + return; + } + + $json = $res->json(); + + if (! isset($json['statuses']) || ! count($json['statuses'])) { + return []; + } + + return $json['statuses']; + }); + } + + public static function getDiscoverPosts() + { + if ((bool) config_cache('federation.activitypub.enabled') == false) { + return []; + } + + if ((bool) config('instance.discover.beagle_api') == false) { + return []; + } + + return Cache::remember(self::DISCOVER_POSTS_CACHE_KEY, now()->addHours(1), function () { + $posts = collect(self::getDiscover()) + ->filter(function ($post) { + $bannedInstances = InstanceService::getBannedDomains(); + $domain = parse_url($post['id'], PHP_URL_HOST); + + return ! in_array($domain, $bannedInstances); + }) + ->map(function ($post) { + $domain = parse_url($post['id'], PHP_URL_HOST); + if ($domain === config_cache('pixelfed.domain.app')) { + $parts = explode('/', $post['id']); + $id = array_last($parts); + + return StatusService::get($id); + } + + $post = Helpers::statusFetch($post['id']); + if (! $post) { + return; + } + $id = $post->id; + + return StatusService::get($id); + }) + ->filter() + ->values() + ->toArray(); + + return $posts; + }); + } +} diff --git a/app/Services/Internal/SoftwareUpdateService.php b/app/Services/Internal/SoftwareUpdateService.php new file mode 100644 index 000000000..492596bf7 --- /dev/null +++ b/app/Services/Internal/SoftwareUpdateService.php @@ -0,0 +1,73 @@ + $curVersion, + 'latest' => [ + 'version' => null, + 'published_at' => null, + 'url' => null, + ], + 'running_latest' => $hideWarning ? true : null + ]; + } + + return [ + 'current' => $curVersion, + 'latest' => [ + 'version' => $versions['latest']['version'], + 'published_at' => $versions['latest']['published_at'], + 'url' => $versions['latest']['url'], + ], + 'running_latest' => strval($versions['latest']['version']) === strval($curVersion) + ]; + } + + public static function fetchLatest() + { + try { + $res = Http::withOptions(['allow_redirects' => false]) + ->timeout(5) + ->connectTimeout(5) + ->retry(2, 500) + ->get('https://versions.pixelfed.org/versions.json'); + } catch (RequestException $e) { + return; + } catch (ConnectionException $e) { + return; + } catch (Exception $e) { + return; + } + + if(!$res->ok()) { + return; + } + + return $res->json(); + } +} diff --git a/app/Services/LandingService.php b/app/Services/LandingService.php index ba16af5b6..d6180771d 100644 --- a/app/Services/LandingService.php +++ b/app/Services/LandingService.php @@ -2,102 +2,101 @@ namespace App\Services; -use App\Util\ActivityPub\Helpers; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Redis; -use App\Status; use App\User; -use App\Services\AccountService; use App\Util\Site\Nodeinfo; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Str; class LandingService { - public static function get($json = true) - { - $activeMonth = Nodeinfo::activeUsersMonthly(); + public static function get($json = true) + { + $activeMonth = Nodeinfo::activeUsersMonthly(); - $totalUsers = Cache::remember('api:nodeinfo:users', 43200, function() { - return User::count(); - }); + $totalUsers = Cache::remember('api:nodeinfo:users', 43200, function () { + return User::count(); + }); - $postCount = Cache::remember('api:nodeinfo:statuses', 21600, function() { - return Status::whereLocal(true)->count(); - }); + $postCount = InstanceService::totalLocalStatuses(); - $contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () { - if(config_cache('instance.admin.pid')) { - return AccountService::getMastodon(config_cache('instance.admin.pid'), true); - } - $admin = User::whereIsAdmin(true)->first(); - return $admin && isset($admin->profile_id) ? - AccountService::getMastodon($admin->profile_id, true) : - null; - }); + $contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () { + if (config_cache('instance.admin.pid')) { + return AccountService::getMastodon(config_cache('instance.admin.pid'), true); + } + $admin = User::whereIsAdmin(true)->first(); - $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { - return config_cache('app.rules') ? - collect(json_decode(config_cache('app.rules'), true)) - ->map(function($rule, $key) { - $id = $key + 1; - return [ - 'id' => "{$id}", - 'text' => $rule - ]; - }) - ->toArray() : []; - }); + return $admin && isset($admin->profile_id) ? + AccountService::getMastodon($admin->profile_id, true) : + null; + }); - $res = [ - 'name' => config_cache('app.name'), - 'url' => config_cache('app.url'), - 'domain' => config('pixelfed.domain.app'), - 'show_directory' => config_cache('instance.landing.show_directory'), - 'show_explore_feed' => config_cache('instance.landing.show_explore'), - 'open_registration' => config_cache('pixelfed.open_registration') == 1, - 'version' => config('pixelfed.version'), - 'about' => [ - 'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'), - 'short_description' => config_cache('app.short_description'), - 'description' => config_cache('app.description'), - ], - 'stats' => [ - 'active_users' => (int) $activeMonth, - 'posts_count' => (int) $postCount, - 'total_users' => (int) $totalUsers - ], - 'contact' => [ - 'account' => $contactAccount, - 'email' => config('instance.email') - ], - 'rules' => $rules, - 'uploader' => [ - 'max_photo_size' => (int) (config('pixelfed.max_photo_size') * 1024), - 'max_caption_length' => (int) config('pixelfed.max_caption_length'), - 'max_altext_length' => (int) config('pixelfed.max_altext_length', 150), - 'album_limit' => (int) config_cache('pixelfed.max_album_length'), - 'image_quality' => (int) config_cache('pixelfed.image_quality'), - 'max_collection_length' => (int) config('pixelfed.max_collection_length', 18), - 'optimize_image' => (bool) config('pixelfed.optimize_image'), - 'optimize_video' => (bool) config('pixelfed.optimize_video'), - 'media_types' => config_cache('pixelfed.media_types'), - ], - 'features' => [ - 'federation' => config_cache('federation.activitypub.enabled'), - 'timelines' => [ - 'local' => true, - 'network' => (bool) config('federation.network_timeline'), - ], - 'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'), - 'stories' => (bool) config_cache('instance.stories.enabled'), - 'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'), - ] - ]; + $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { + return config_cache('app.rules') ? + collect(json_decode(config_cache('app.rules'), true)) + ->map(function ($rule, $key) { + $id = $key + 1; - if($json) { - return json_encode($res); - } + return [ + 'id' => "{$id}", + 'text' => $rule, + ]; + }) + ->toArray() : []; + }); - return $res; - } + $openReg = (bool) config_cache('pixelfed.open_registration'); + + $res = [ + 'name' => config_cache('app.name'), + 'url' => config_cache('app.url'), + 'domain' => config('pixelfed.domain.app'), + 'show_directory' => (bool) config_cache('instance.landing.show_directory'), + 'show_explore_feed' => (bool) config_cache('instance.landing.show_explore'), + 'open_registration' => (bool) $openReg, + 'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'), + 'version' => config('pixelfed.version'), + 'about' => [ + 'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'), + 'short_description' => config_cache('app.short_description'), + 'description' => config_cache('app.description'), + ], + 'stats' => [ + 'active_users' => (int) $activeMonth, + 'posts_count' => (int) $postCount, + 'total_users' => (int) $totalUsers, + ], + 'contact' => [ + 'account' => $contactAccount, + 'email' => config('instance.email'), + ], + 'rules' => $rules, + 'uploader' => [ + 'max_photo_size' => (int) (config_cache('pixelfed.max_photo_size') * 1024), + 'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150), + 'album_limit' => (int) config_cache('pixelfed.max_album_length'), + 'image_quality' => (int) config_cache('pixelfed.image_quality'), + 'max_collection_length' => (int) config('pixelfed.max_collection_length', 18), + 'optimize_image' => (bool) config_cache('pixelfed.optimize_image'), + 'optimize_video' => (bool) config_cache('pixelfed.optimize_video'), + 'media_types' => config_cache('pixelfed.media_types'), + ], + 'features' => [ + 'federation' => (bool) config_cache('federation.activitypub.enabled'), + 'timelines' => [ + 'local' => true, + 'network' => (bool) config_cache('federation.network_timeline'), + ], + 'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'), + 'stories' => (bool) config_cache('instance.stories.enabled'), + 'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'), + ], + ]; + + if ($json) { + return json_encode($res); + } + + return $res; + } } diff --git a/app/Services/MarkerService.php b/app/Services/MarkerService.php index 6b407b567..130f0b017 100644 --- a/app/Services/MarkerService.php +++ b/app/Services/MarkerService.php @@ -13,7 +13,7 @@ class MarkerService return Cache::get(self::CACHE_KEY . $timeline . ':' . $profileId); } - public static function set($profileId, $timeline = 'home', $entityId) + public static function set($profileId, $timeline = 'home', $entityId = false) { $existing = self::get($profileId, $timeline); $key = self::CACHE_KEY . $timeline . ':' . $profileId; diff --git a/app/Services/MediaStorageService.php b/app/Services/MediaStorageService.php index 128001de2..87bb9a586 100644 --- a/app/Services/MediaStorageService.php +++ b/app/Services/MediaStorageService.php @@ -2,277 +2,321 @@ namespace App\Services; +use App\Jobs\AvatarPipeline\AvatarStorageCleanup; +use App\Jobs\MediaPipeline\MediaDeletePipeline; +use App\Media; use App\Util\ActivityPub\Helpers; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; use Illuminate\Http\File; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -use App\Media; -use App\Profile; -use App\User; -use GuzzleHttp\Client; -use App\Services\AccountService; -use App\Http\Controllers\AvatarController; -use GuzzleHttp\Exception\RequestException; -use App\Jobs\MediaPipeline\MediaDeletePipeline; -use Illuminate\Support\Arr; -use App\Jobs\AvatarPipeline\AvatarStorageCleanup; -class MediaStorageService { +class MediaStorageService +{ + public static function store(Media $media) + { + if ((bool) config_cache('pixelfed.cloud_storage') == true) { + (new self())->cloudStore($media); + } - public static function store(Media $media) - { - if(config_cache('pixelfed.cloud_storage') == true) { - (new self())->cloudStore($media); - } + } - return; - } + public static function move(Media $media) + { + if ($media->remote_media) { + return; + } - public static function avatar($avatar, $local = false, $skipRecentCheck = false) - { - return (new self())->fetchAvatar($avatar, $local, $skipRecentCheck); - } + if ((bool) config_cache('pixelfed.cloud_storage') == true) { + return (new self())->cloudMove($media); + } - public static function head($url) - { - $c = new Client(); - try { - $r = $c->request('HEAD', $url); - } catch (RequestException $e) { - return false; - } + } - $h = Arr::mapWithKeys($r->getHeaders(), function($item, $key) { + public static function avatar($avatar, $local = false, $skipRecentCheck = false) + { + return (new self())->fetchAvatar($avatar, $local, $skipRecentCheck); + } + + public static function head($url) + { + $c = new Client(); + try { + $r = $c->request('HEAD', $url); + } catch (RequestException $e) { + return false; + } + + $h = Arr::mapWithKeys($r->getHeaders(), function ($item, $key) { return [strtolower($key) => last($item)]; }); - if(!isset($h['content-length'], $h['content-type'])) { + if (! isset($h['content-length'], $h['content-type'])) { return false; } $len = (int) $h['content-length']; $mime = $h['content-type']; - if($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) { - return false; - } + if ($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) { + return false; + } - return [ - 'length' => $len, - 'mime' => $mime - ]; - } + return [ + 'length' => $len, + 'mime' => $mime, + ]; + } - protected function cloudStore($media) - { - if($media->remote_media == true) { - if(config('media.storage.remote.cloud')) { - (new self())->remoteToCloud($media); - } - } else { - (new self())->localToCloud($media); - } - } + protected function cloudStore($media) + { + if ($media->remote_media == true) { + if (config('media.storage.remote.cloud')) { + (new self())->remoteToCloud($media); + } + } else { + (new self())->localToCloud($media); + } + } - protected function localToCloud($media) - { - $path = storage_path('app/'.$media->media_path); - $thumb = storage_path('app/'.$media->thumbnail_path); + protected function localToCloud($media) + { + $path = storage_path('app/'.$media->media_path); + $thumb = storage_path('app/'.$media->thumbnail_path); - $p = explode('/', $media->media_path); - $name = array_pop($p); - $pt = explode('/', $media->thumbnail_path); - $thumbname = array_pop($pt); - $storagePath = implode('/', $p); + $p = explode('/', $media->media_path); + $name = array_pop($p); + $pt = explode('/', $media->thumbnail_path); + $thumbname = array_pop($pt); + $storagePath = implode('/', $p); - $url = ResilientMediaStorageService::store($storagePath, $path, $name); - if($thumb) { - $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); - $media->thumbnail_url = $thumbUrl; - } - $media->cdn_url = $url; - $media->optimized_url = $url; - $media->replicated_at = now(); - $media->save(); - if($media->status_id) { - Cache::forget('status:transformer:media:attachments:' . $media->status_id); - MediaService::del($media->status_id); - StatusService::del($media->status_id, false); - } - } + $url = ResilientMediaStorageService::store($storagePath, $path, $name); + if ($thumb) { + $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); + $media->thumbnail_url = $thumbUrl; + } + $media->cdn_url = $url; + $media->optimized_url = $url; + $media->replicated_at = now(); + $media->save(); + if ($media->status_id) { + Cache::forget('status:transformer:media:attachments:'.$media->status_id); + MediaService::del($media->status_id); + StatusService::del($media->status_id, false); + } + } - protected function remoteToCloud($media) - { - $url = $media->remote_url; + protected function remoteToCloud($media) + { + $url = $media->remote_url; - if(!Helpers::validateUrl($url)) { - return; - } + if (! Helpers::validateUrl($url)) { + return; + } - $head = $this->head($media->remote_url); + $head = $this->head($media->remote_url); - if(!$head) { - return; - } + if (! $head) { + return; + } - $mimes = [ - 'image/jpeg', - 'image/png', - 'video/mp4' - ]; + $mimes = [ + 'image/jpeg', + 'image/png', + 'video/mp4', + ]; - $mime = $head['mime']; - $max_size = (int) config_cache('pixelfed.max_photo_size') * 1000; - $media->size = $head['length']; - $media->remote_media = true; - $media->save(); + $mime = $head['mime']; + $max_size = (int) config_cache('pixelfed.max_photo_size') * 1000; + $media->size = $head['length']; + $media->remote_media = true; + $media->save(); - if(!in_array($mime, $mimes)) { - return; - } + if (! in_array($mime, $mimes)) { + return; + } - if($head['length'] >= $max_size) { - return; - } + if ($head['length'] >= $max_size) { + return; + } - switch ($mime) { - case 'image/png': - $ext = '.png'; - break; + switch ($mime) { + case 'image/png': + $ext = '.png'; + break; - case 'image/gif': - $ext = '.gif'; - break; + case 'image/gif': + $ext = '.gif'; + break; - case 'image/jpeg': - $ext = '.jpg'; - break; + case 'image/jpeg': + $ext = '.jpg'; + break; - case 'video/mp4': - $ext = '.mp4'; - break; - } + case 'video/mp4': + $ext = '.mp4'; + break; + } - $base = MediaPathService::get($media->profile); - $path = Str::random(40) . $ext; - $tmpBase = storage_path('app/remcache/'); - $tmpPath = $media->profile_id . '-' . $path; - $tmpName = $tmpBase . $tmpPath; - $data = file_get_contents($url, false, null, 0, $head['length']); - file_put_contents($tmpName, $data); - $hash = hash_file('sha256', $tmpName); + $base = MediaPathService::get($media->profile); + $path = Str::random(40).$ext; + $tmpBase = storage_path('app/remcache/'); + $tmpPath = $media->profile_id.'-'.$path; + $tmpName = $tmpBase.$tmpPath; + $data = file_get_contents($url, false, null, 0, $head['length']); + file_put_contents($tmpName, $data); + $hash = hash_file('sha256', $tmpName); - $disk = Storage::disk(config('filesystems.cloud')); - $file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); - $permalink = $disk->url($file); + $disk = Storage::disk(config('filesystems.cloud')); + $file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); + $permalink = $disk->url($file); - $media->media_path = $file; - $media->cdn_url = $permalink; - $media->original_sha256 = $hash; - $media->replicated_at = now(); - $media->save(); + $media->media_path = $file; + $media->cdn_url = $permalink; + $media->original_sha256 = $hash; + $media->replicated_at = now(); + $media->save(); - if($media->status_id) { - Cache::forget('status:transformer:media:attachments:' . $media->status_id); - } + if ($media->status_id) { + Cache::forget('status:transformer:media:attachments:'.$media->status_id); + } - unlink($tmpName); - } + unlink($tmpName); + } - protected function fetchAvatar($avatar, $local = false, $skipRecentCheck = false) - { - $queue = random_int(1, 15) > 5 ? 'mmo' : 'low'; - $url = $avatar->remote_url; - $driver = $local ? 'local' : config('filesystems.cloud'); + protected function fetchAvatar($avatar, $local = false, $skipRecentCheck = false) + { + $queue = random_int(1, 15) > 5 ? 'mmo' : 'low'; + $url = $avatar->remote_url; + $driver = $local ? 'local' : config('filesystems.cloud'); - if(empty($url) || Helpers::validateUrl($url) == false) { - return; - } + if (empty($url) || Helpers::validateUrl($url) == false) { + return; + } - $head = $this->head($url); + $head = $this->head($url); - if($head == false) { - return; - } + if ($head == false) { + return; + } - $mimes = [ - 'application/octet-stream', - 'image/jpeg', - 'image/png', - ]; + $mimes = [ + 'application/octet-stream', + 'image/jpeg', + 'image/png', + ]; - $mime = $head['mime']; - $max_size = (int) config('pixelfed.max_avatar_size') * 1000; + $mime = $head['mime']; + $max_size = (int) config('pixelfed.max_avatar_size') * 1000; - if(!$skipRecentCheck) { - if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) { - return; - } - } + if (! $skipRecentCheck) { + if ($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) { + return; + } + } - Cache::forget('avatar:' . $avatar->profile_id); - AccountService::del($avatar->profile_id); + Cache::forget('avatar:'.$avatar->profile_id); + AccountService::del($avatar->profile_id); - // handle pleroma edge case - if(Str::endsWith($mime, '; charset=utf-8')) { - $mime = str_replace('; charset=utf-8', '', $mime); - } + // handle pleroma edge case + if (Str::endsWith($mime, '; charset=utf-8')) { + $mime = str_replace('; charset=utf-8', '', $mime); + } - if(!in_array($mime, $mimes)) { - return; - } + if (! in_array($mime, $mimes)) { + return; + } - if($head['length'] >= $max_size) { - return; - } + if ($head['length'] >= $max_size) { + return; + } - $base = ($local ? 'public/cache/' : 'cache/') . 'avatars/' . $avatar->profile_id; - $ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png'; - $path = 'avatar_' . strtolower(Str::random(random_int(3,6))) . '.' . $ext; - $tmpBase = storage_path('app/remcache/'); - $tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path; - $tmpName = $tmpBase . $tmpPath; - $data = @file_get_contents($url, false, null, 0, $head['length']); - if(!$data) { - return; - } - file_put_contents($tmpName, $data); + $base = ($local ? 'public/cache/' : 'cache/').'avatars/'.$avatar->profile_id; + $ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png'; + $path = 'avatar_'.strtolower(Str::random(random_int(3, 6))).'.'.$ext; + $tmpBase = storage_path('app/remcache/'); + $tmpPath = 'avatar_'.$avatar->profile_id.'-'.$path; + $tmpName = $tmpBase.$tmpPath; + $data = @file_get_contents($url, false, null, 0, $head['length']); + if (! $data) { + return; + } + file_put_contents($tmpName, $data); - $mimeCheck = Storage::mimeType('remcache/' . $tmpPath); + $mimeCheck = Storage::mimeType('remcache/'.$tmpPath); - if(!$mimeCheck || !in_array($mimeCheck, ['image/png', 'image/jpeg'])) { - $avatar->last_fetched_at = now(); - $avatar->save(); - unlink($tmpName); - return; - } + if (! $mimeCheck || ! in_array($mimeCheck, ['image/png', 'image/jpeg'])) { + $avatar->last_fetched_at = now(); + $avatar->save(); + unlink($tmpName); - $disk = Storage::disk($driver); - $file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); - $permalink = $disk->url($file); + return; + } - $avatar->media_path = $base . '/' . $path; - $avatar->is_remote = true; - $avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink; - $avatar->size = $head['length']; - $avatar->change_count = $avatar->change_count + 1; - $avatar->last_fetched_at = now(); - $avatar->save(); + $disk = Storage::disk($driver); + $file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); + $permalink = $disk->url($file); - Cache::forget('avatar:' . $avatar->profile_id); - AccountService::del($avatar->profile_id); - AvatarStorageCleanup::dispatch($avatar)->onQueue($queue)->delay(now()->addMinutes(random_int(3, 15))); + $avatar->media_path = $base.'/'.$path; + $avatar->is_remote = true; + $avatar->cdn_url = $local ? config('app.url').$permalink : $permalink; + $avatar->size = $head['length']; + $avatar->change_count = $avatar->change_count + 1; + $avatar->last_fetched_at = now(); + $avatar->save(); - unlink($tmpName); - } + Cache::forget('avatar:'.$avatar->profile_id); + AccountService::del($avatar->profile_id); + AvatarStorageCleanup::dispatch($avatar)->onQueue($queue)->delay(now()->addMinutes(random_int(3, 15))); - public static function delete(Media $media, $confirm = false) - { - if(!$confirm) { - return; - } - MediaDeletePipeline::dispatch($media)->onQueue('mmo'); - } + unlink($tmpName); + } + + public static function delete(Media $media, $confirm = false) + { + if (! $confirm) { + return; + } + MediaDeletePipeline::dispatch($media)->onQueue('mmo'); + } + + protected function cloudMove($media) + { + if (! Storage::exists($media->media_path)) { + return 'invalid file'; + } + + $path = storage_path('app/'.$media->media_path); + $thumb = false; + if ($media->thumbnail_path) { + $thumb = storage_path('app/'.$media->thumbnail_path); + $pt = explode('/', $media->thumbnail_path); + $thumbname = array_pop($pt); + } + + $p = explode('/', $media->media_path); + $name = array_pop($p); + $storagePath = implode('/', $p); + + $url = ResilientMediaStorageService::store($storagePath, $path, $name); + if ($thumb) { + $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); + $media->thumbnail_url = $thumbUrl; + } + $media->cdn_url = $url; + $media->optimized_url = $url; + $media->replicated_at = now(); + $media->save(); + + if ($media->status_id) { + Cache::forget('status:transformer:media:attachments:'.$media->status_id); + MediaService::del($media->status_id); + StatusService::del($media->status_id, false); + } + + return 'success'; + } } diff --git a/app/Services/NodeinfoService.php b/app/Services/NodeinfoService.php index 10575ff9f..d0de127e6 100644 --- a/app/Services/NodeinfoService.php +++ b/app/Services/NodeinfoService.php @@ -2,29 +2,31 @@ namespace App\Services; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Http; -use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\ConnectionException; +use Illuminate\Http\Client\RequestException; +use Illuminate\Support\Facades\Http; class NodeinfoService { public static function get($domain) { - $version = config('pixelfed.version'); - $appUrl = config('app.url'); - $headers = [ - 'Accept' => 'application/json', - 'User-Agent' => "(Pixelfed/{$version}; +{$appUrl})", - ]; + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $headers = [ + 'Accept' => 'application/json', + 'User-Agent' => "(Pixelfed/{$version}; +{$appUrl})", + ]; - $url = 'https://' . $domain; - $wk = $url . '/.well-known/nodeinfo'; + $url = 'https://'.$domain; + $wk = $url.'/.well-known/nodeinfo'; try { - $res = Http::withHeaders($headers) - ->timeout(5) - ->get($wk); + $res = Http::withOptions([ + 'allow_redirects' => false, + ]) + ->withHeaders($headers) + ->timeout(5) + ->get($wk); } catch (RequestException $e) { return false; } catch (ConnectionException $e) { @@ -33,18 +35,18 @@ class NodeinfoService return false; } - if(!$res) { + if (! $res) { return false; } $json = $res->json(); - if( !isset($json['links'])) { + if (! isset($json['links'])) { return false; } - if(is_array($json['links'])) { - if(isset($json['links']['href'])) { + if (is_array($json['links'])) { + if (isset($json['links']['href'])) { $href = $json['links']['href']; } else { $href = $json['links'][0]['href']; @@ -56,14 +58,17 @@ class NodeinfoService $domain = parse_url($url, PHP_URL_HOST); $hrefDomain = parse_url($href, PHP_URL_HOST); - if($domain !== $hrefDomain) { - return 60; + if ($domain !== $hrefDomain) { + return false; } try { - $res = Http::withHeaders($headers) - ->timeout(5) - ->get($href); + $res = Http::withOptions([ + 'allow_redirects' => false, + ]) + ->withHeaders($headers) + ->timeout(5) + ->get($href); } catch (RequestException $e) { return false; } catch (ConnectionException $e) { @@ -71,6 +76,7 @@ class NodeinfoService } catch (\Exception $e) { return false; } + return $res->json(); } } diff --git a/app/Services/NotificationAppGatewayService.php b/app/Services/NotificationAppGatewayService.php new file mode 100644 index 000000000..c22d2c2e2 --- /dev/null +++ b/app/Services/NotificationAppGatewayService.php @@ -0,0 +1,125 @@ + 1]) + ->retry(3, 500) + ->throw() + ->get($endpoint); + + $data = $res->json(); + } catch (RequestException $e) { + return false; + } catch (Exception $e) { + return false; + } + + if ($res->successful() && isset($data['active']) && $data['active'] === true) { + return true; + } + + return false; + } + + public static function forceSupportRecheck() + { + Cache::forget(self::GATEWAY_SUPPORT_CHECK); + + return self::enabled(); + } + + public static function isValidExpoPushToken($token) + { + if (! $token || empty($token)) { + return false; + } + + if (str_starts_with($token, 'ExponentPushToken[') && mb_strlen($token) < 26) { + return false; + } + + if (! str_starts_with($token, 'ExponentPushToken[') && ! str_starts_with($token, 'ExpoPushToken[')) { + return false; + } + + if (! str_ends_with($token, ']')) { + return false; + } + + return true; + } + + public static function send($userToken, $type, $actor = '') + { + if (! self::enabled()) { + return false; + } + + if (! $userToken || empty($userToken) || ! self::isValidExpoPushToken($userToken)) { + return false; + } + + $types = PushNotificationService::NOTIFY_TYPES; + + if (! $type || empty($type) || ! in_array($type, $types)) { + return false; + } + + $apiKey = config('instance.notifications.nag.api_key'); + + if (! $apiKey || empty($apiKey)) { + return false; + } + $url = 'https://'.config('instance.notifications.nag.endpoint').'/api/v1/relay/deliver'; + + try { + $response = Http::withToken($apiKey) + ->withHeaders(['X-PIXELFED-API' => 1]) + ->post($url, [ + 'token' => $userToken, + 'type' => $type, + 'actor' => $actor, + ]); + + $response->throw(); + } catch (RequestException $e) { + return; + } catch (Exception $e) { + return; + } + } +} diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php index d088c2015..9ee0d77fc 100644 --- a/app/Services/NotificationService.php +++ b/app/Services/NotificationService.php @@ -2,297 +2,324 @@ namespace App\Services; +use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline; +use App\Notification; +use App\Transformer\Api\NotificationTransformer; use Cache; use Illuminate\Support\Facades\Redis; -use App\{ - Notification, - Profile -}; -use App\Transformer\Api\NotificationTransformer; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; -class NotificationService { +class NotificationService +{ + const CACHE_KEY = 'pf:services:notifications:ids:'; - const CACHE_KEY = 'pf:services:notifications:ids:'; - const EPOCH_CACHE_KEY = 'pf:services:notifications:epoch-id:by-months:'; - const ITEM_CACHE_TTL = 86400; - const MASTODON_TYPES = [ - 'follow', - 'follow_request', - 'mention', - 'reblog', - 'favourite', - 'poll', - 'status' - ]; + const EPOCH_CACHE_KEY = 'pf:services:notifications:epoch-id:by-months:'; - public static function get($id, $start = 0, $stop = 400) - { - $res = collect([]); - $key = self::CACHE_KEY . $id; - $stop = $stop > 400 ? 400 : $stop; - $ids = Redis::zrangebyscore($key, $start, $stop); - if(empty($ids)) { - $ids = self::coldGet($id, $start, $stop); - } - foreach($ids as $id) { - $n = self::getNotification($id); - if($n != null) { - $res->push($n); - } - } - return $res; - } + const ITEM_CACHE_TTL = 86400; - public static function getEpochId($months = 6) - { - return Cache::remember(self::EPOCH_CACHE_KEY . $months, 1209600, function() use($months) { - if(Notification::count() === 0) { - return 0; + const MASTODON_TYPES = [ + 'follow', + 'follow_request', + 'mention', + 'reblog', + 'favourite', + 'poll', + 'status', + ]; + + public static function get($id, $start = 0, $stop = 400) + { + $res = collect([]); + $key = self::CACHE_KEY.$id; + $stop = $stop > 400 ? 400 : $stop; + $ids = Redis::zrangebyscore($key, $start, $stop); + if (empty($ids)) { + $ids = self::coldGet($id, $start, $stop); + } + foreach ($ids as $id) { + $n = self::getNotification($id); + if ($n != null) { + $res->push($n); } - return Notification::where('created_at', '>', now()->subMonths($months))->first()->id; - }); - } + } - public static function coldGet($id, $start = 0, $stop = 400) - { - $stop = $stop > 400 ? 400 : $stop; - $ids = Notification::where('id', '>', self::getEpochId()) - ->where('profile_id', $id) - ->orderByDesc('id') - ->skip($start) - ->take($stop) - ->pluck('id'); - foreach($ids as $key) { - self::set($id, $key); - } - return $ids; - } + return $res; + } - public static function getMax($id = false, $start = 0, $limit = 10) - { - $ids = self::getRankedMaxId($id, $start, $limit); + public static function getEpochId($months = 6) + { + $epoch = Cache::get(self::EPOCH_CACHE_KEY.$months); + if (! $epoch) { + NotificationEpochUpdatePipeline::dispatch(); - if(empty($ids)) { - return []; - } + return 1; + } - $res = collect([]); - foreach($ids as $id) { - $n = self::getNotification($id); - if($n != null) { - $res->push($n); - } - } - return $res->toArray(); - } + return $epoch; + } - public static function getMin($id = false, $start = 0, $limit = 10) - { - $ids = self::getRankedMinId($id, $start, $limit); + public static function coldGet($id, $start = 0, $stop = 400) + { + $stop = $stop > 400 ? 400 : $stop; + $ids = Notification::where('id', '>', self::getEpochId()) + ->where('profile_id', $id) + ->orderByDesc('id') + ->skip($start) + ->take($stop) + ->pluck('id'); + foreach ($ids as $key) { + self::set($id, $key); + } - if(empty($ids)) { - return []; - } + return $ids; + } - $res = collect([]); - foreach($ids as $id) { - $n = self::getNotification($id); - if($n != null) { - $res->push($n); - } - } - return $res->toArray(); - } + public static function getMax($id = false, $start = 0, $limit = 10) + { + $ids = self::getRankedMaxId($id, $start, $limit); + if (empty($ids)) { + return []; + } - public static function getMaxMastodon($id = false, $start = 0, $limit = 10) - { - $ids = self::getRankedMaxId($id, $start, $limit); + $res = collect([]); + foreach ($ids as $id) { + $n = self::getNotification($id); + if ($n != null) { + $res->push($n); + } + } - if(empty($ids)) { - return []; - } + return $res->toArray(); + } - $res = collect([]); - foreach($ids as $id) { - $n = self::rewriteMastodonTypes(self::getNotification($id)); - if($n != null && in_array($n['type'], self::MASTODON_TYPES)) { - if(isset($n['account'])) { - $n['account'] = AccountService::getMastodon($n['account']['id']); - } + public static function getMin($id = false, $start = 0, $limit = 10) + { + $ids = self::getRankedMinId($id, $start, $limit); - if(isset($n['relationship'])) { - unset($n['relationship']); - } + if (empty($ids)) { + return []; + } - if(isset($n['status'])) { - $n['status'] = StatusService::getMastodon($n['status']['id'], false); - } + $res = collect([]); + foreach ($ids as $id) { + $n = self::getNotification($id); + if ($n != null) { + $res->push($n); + } + } - $res->push($n); - } - } - return $res->toArray(); - } + return $res->toArray(); + } - public static function getMinMastodon($id = false, $start = 0, $limit = 10) - { - $ids = self::getRankedMinId($id, $start, $limit); + public static function getMaxMastodon($id = false, $start = 0, $limit = 10) + { + $ids = self::getRankedMaxId($id, $start, $limit); - if(empty($ids)) { - return []; - } + if (empty($ids)) { + return []; + } - $res = collect([]); - foreach($ids as $id) { - $n = self::rewriteMastodonTypes(self::getNotification($id)); - if($n != null && in_array($n['type'], self::MASTODON_TYPES)) { - if(isset($n['account'])) { - $n['account'] = AccountService::getMastodon($n['account']['id']); - } + $res = collect([]); + foreach ($ids as $id) { + $n = self::rewriteMastodonTypes(self::getNotification($id)); + if ($n != null && in_array($n['type'], self::MASTODON_TYPES)) { + if (isset($n['account'])) { + $n['account'] = AccountService::getMastodon($n['account']['id']); + } - if(isset($n['relationship'])) { - unset($n['relationship']); - } + if (isset($n['relationship'])) { + unset($n['relationship']); + } - if(isset($n['status'])) { - $n['status'] = StatusService::getMastodon($n['status']['id'], false); - } + if ($n['type'] === 'mention' && isset($n['tagged'], $n['tagged']['status_id'])) { + $n['status'] = StatusService::getMastodon($n['tagged']['status_id'], false); + unset($n['tagged']); + } - $res->push($n); - } - } - return $res->toArray(); - } + if (isset($n['status'])) { + $n['status'] = StatusService::getMastodon($n['status']['id'], false); + } - public static function getRankedMaxId($id = false, $start = null, $limit = 10) - { - if(!$start || !$id) { - return []; - } + $res->push($n); + } + } - return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, $start, '-inf', [ - 'withscores' => true, - 'limit' => [1, $limit] - ])); - } + return $res->toArray(); + } - public static function getRankedMinId($id = false, $end = null, $limit = 10) - { - if(!$end || !$id) { - return []; - } + public static function getMinMastodon($id = false, $start = 0, $limit = 10) + { + $ids = self::getRankedMinId($id, $start, $limit); - return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, '+inf', $end, [ - 'withscores' => true, - 'limit' => [0, $limit] - ])); - } + if (empty($ids)) { + return []; + } - public static function rewriteMastodonTypes($notification) - { - if(!$notification || !isset($notification['type'])) { - return $notification; - } + $res = collect([]); + foreach ($ids as $id) { + $n = self::rewriteMastodonTypes(self::getNotification($id)); + if ($n != null && in_array($n['type'], self::MASTODON_TYPES)) { + if (isset($n['account'])) { + $n['account'] = AccountService::getMastodon($n['account']['id']); + } - if($notification['type'] === 'comment') { - $notification['type'] = 'mention'; - } + if (isset($n['relationship'])) { + unset($n['relationship']); + } - if($notification['type'] === 'share') { - $notification['type'] = 'reblog'; - } + if ($n['type'] === 'mention' && isset($n['tagged'], $n['tagged']['status_id'])) { + $n['status'] = StatusService::getMastodon($n['tagged']['status_id'], false); + unset($n['tagged']); + } - return $notification; - } + if (isset($n['status'])) { + $n['status'] = StatusService::getMastodon($n['status']['id'], false); + } - public static function set($id, $val) - { - if(self::count($id) > 400) { - Redis::zpopmin(self::CACHE_KEY . $id); - } - return Redis::zadd(self::CACHE_KEY . $id, $val, $val); - } + $res->push($n); + } + } - public static function del($id, $val) - { - Cache::forget('service:notification:' . $val); - return Redis::zrem(self::CACHE_KEY . $id, $val); - } + return $res->toArray(); + } - public static function add($id, $val) - { - return self::set($id, $val); - } + public static function getRankedMaxId($id = false, $start = null, $limit = 10) + { + if (! $start || ! $id) { + return []; + } - public static function rem($id, $val) - { - return self::del($id, $val); - } + return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, $start, '-inf', [ + 'withscores' => true, + 'limit' => [1, $limit], + ])); + } - public static function count($id) - { - return Redis::zcount(self::CACHE_KEY . $id, '-inf', '+inf'); - } + public static function getRankedMinId($id = false, $end = null, $limit = 10) + { + if (! $end || ! $id) { + return []; + } - public static function getNotification($id) - { - $notification = Cache::remember('service:notification:'.$id, self::ITEM_CACHE_TTL, function() use($id) { - $n = Notification::with('item')->find($id); + return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, '+inf', $end, [ + 'withscores' => true, + 'limit' => [0, $limit], + ])); + } - if(!$n) { - return null; - } + public static function rewriteMastodonTypes($notification) + { + if (! $notification || ! isset($notification['type'])) { + return $notification; + } - $account = AccountService::get($n->actor_id, true); + if ($notification['type'] === 'comment') { + $notification['type'] = 'mention'; + } - if(!$account) { - return null; - } + if ($notification['type'] === 'share') { + $notification['type'] = 'reblog'; + } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($n, new NotificationTransformer()); - return $fractal->createData($resource)->toArray(); - }); + if ($notification['type'] === 'tagged') { + $notification['type'] = 'mention'; + } - if(!$notification) { - return; - } + return $notification; + } - if(isset($notification['account'])) { - $notification['account'] = AccountService::get($notification['account']['id'], true); - } + public static function set($id, $val) + { + if (self::count($id) > 400) { + Redis::zpopmin(self::CACHE_KEY.$id); + } - return $notification; - } + return Redis::zadd(self::CACHE_KEY.$id, $val, $val); + } - public static function setNotification(Notification $notification) - { - return Cache::remember('service:notification:'.$notification->id, self::ITEM_CACHE_TTL, function() use($notification) { - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($notification, new NotificationTransformer()); - return $fractal->createData($resource)->toArray(); - }); - } + public static function del($id, $val) + { + Cache::forget('service:notification:'.$val); - public static function warmCache($id, $stop = 400, $force = false) - { - if(self::count($id) == 0 || $force == true) { - $ids = Notification::where('profile_id', $id) - ->where('id', '>', self::getEpochId()) - ->orderByDesc('id') - ->limit($stop) - ->pluck('id'); - foreach($ids as $key) { - self::set($id, $key); - } - return 1; - } - return 0; - } + return Redis::zrem(self::CACHE_KEY.$id, $val); + } + + public static function add($id, $val) + { + return self::set($id, $val); + } + + public static function rem($id, $val) + { + return self::del($id, $val); + } + + public static function count($id) + { + return Redis::zcount(self::CACHE_KEY.$id, '-inf', '+inf'); + } + + public static function getNotification($id) + { + $notification = Cache::remember('service:notification:'.$id, self::ITEM_CACHE_TTL, function () use ($id) { + $n = Notification::with('item')->find($id); + + if (! $n) { + return null; + } + + $account = AccountService::get($n->actor_id, true); + + if (! $account) { + return null; + } + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($n, new NotificationTransformer()); + + return $fractal->createData($resource)->toArray(); + }); + + if (! $notification) { + return; + } + + if (isset($notification['account'])) { + $notification['account'] = AccountService::get($notification['account']['id'], true); + } + + return $notification; + } + + public static function setNotification(Notification $notification) + { + return Cache::remember('service:notification:'.$notification->id, self::ITEM_CACHE_TTL, function () use ($notification) { + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($notification, new NotificationTransformer()); + + return $fractal->createData($resource)->toArray(); + }); + } + + public static function warmCache($id, $stop = 400, $force = false) + { + if (self::count($id) == 0 || $force == true) { + $ids = Notification::where('profile_id', $id) + ->where('id', '>', self::getEpochId()) + ->orderByDesc('id') + ->limit($stop) + ->pluck('id'); + foreach ($ids as $key) { + self::set($id, $key); + } + + return 1; + } + + return 0; + } } diff --git a/app/Services/PronounService.php b/app/Services/PronounService.php index dce1d591a..a33ac0296 100644 --- a/app/Services/PronounService.php +++ b/app/Services/PronounService.php @@ -70,6 +70,8 @@ class PronounService { 'her', 'hers', 'hir', + 'it', + 'its', 'mer', 'mers', 'ne', diff --git a/app/Services/PushNotificationService.php b/app/Services/PushNotificationService.php new file mode 100644 index 000000000..8acb07acc --- /dev/null +++ b/app/Services/PushNotificationService.php @@ -0,0 +1,123 @@ +first(); + if (! $user || $user->status || $user->deleted_at) { + return false; + } + + return Redis::sadd(self::ACTIVE_LIST_KEY.$listId, $memberId); + } + + public static function check($listId, $memberId) + { + return random_int(1, self::LOTTERY_ODDS) === 1 + ? self::isMemberDeepCheck($listId, $memberId) + : self::isMember($listId, $memberId); + } + + public static function isMember($listId, $memberId) + { + try { + return Redis::sismember(self::ACTIVE_LIST_KEY.$listId, $memberId); + } catch (Exception $e) { + return false; + } + } + + public static function isMemberDeepCheck($listId, $memberId) + { + $lock = Cache::lock(self::DEEP_CHECK_KEY.$listId, self::CACHE_LOCK_SECONDS); + + try { + $lock->block(5); + $actualCount = User::whereNull('status')->where('notify_enabled', true)->where('notify_'.$listId, true)->count(); + $cachedCount = self::count($listId); + if ($actualCount != $cachedCount) { + self::warmList($listId); + $user = User::where('notify_enabled', true)->where('profile_id', $memberId)->first(); + + return $user ? (bool) $user->{"notify_{$listId}"} : false; + } else { + return self::isMember($listId, $memberId); + } + } catch (Exception $e) { + Log::error('Failed during deep membership check: '.$e->getMessage()); + + return false; + } finally { + optional($lock)->release(); + } + } + + public static function removeMember($listId, $memberId) + { + return Redis::srem(self::ACTIVE_LIST_KEY.$listId, $memberId); + } + + public static function removeMemberFromAll($memberId) + { + foreach (self::NOTIFY_TYPES as $type) { + self::removeMember($type, $memberId); + } + + return 1; + } + + public static function count($listId) + { + if (! in_array($listId, self::NOTIFY_TYPES)) { + return false; + } + + return Redis::scard(self::ACTIVE_LIST_KEY.$listId); + } + + public static function warmList($listId) + { + if (! in_array($listId, self::NOTIFY_TYPES)) { + return false; + } + $key = self::ACTIVE_LIST_KEY.$listId; + Redis::del($key); + foreach (User::where('notify_'.$listId, true)->cursor() as $acct) { + if ($acct->status || $acct->deleted_at || ! $acct->profile_id || ! $acct->notify_enabled) { + continue; + } + Redis::sadd($key, $acct->profile_id); + } + + return self::count($listId); + } +} diff --git a/app/Services/ReblogService.php b/app/Services/ReblogService.php index c08ad0089..e2c7b9fdd 100644 --- a/app/Services/ReblogService.php +++ b/app/Services/ReblogService.php @@ -2,67 +2,157 @@ namespace App\Services; +use App\Status; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Redis; -use App\Status; +use Illuminate\Support\Lottery; class ReblogService { - const CACHE_KEY = 'pf:services:reblogs:'; - const REBLOGS_KEY = 'pf:services:reblogs:v1:post:'; - const COLDBOOT_KEY = 'pf:services:reblogs:v1:post_:'; + const CACHE_KEY = 'pf:services:reblogs:'; - public static function get($profileId, $statusId) - { - if (!Redis::zcard(self::CACHE_KEY . $profileId)) { - return false; - } + const REBLOGS_KEY = 'pf:services:reblogs:v1:post:'; - return Redis::zscore(self::CACHE_KEY . $profileId, $statusId) != null; - } + const COLDBOOT_KEY = 'pf:services:reblogs:v1:post_:'; - public static function add($profileId, $statusId) - { - return Redis::zadd(self::CACHE_KEY . $profileId, $statusId, $statusId); - } + const CACHE_SKIP_KEY = 'pf:services:reblogs:skip_empty_check:'; - public static function del($profileId, $statusId) - { - return Redis::zrem(self::CACHE_KEY . $profileId, $statusId); - } + public static function get($profileId, $statusId) + { + return Lottery::odds(1, 20) + ->winner(fn () => self::getFromDatabaseCheck($profileId, $statusId)) + ->loser(fn () => self::getFromRedis($profileId, $statusId)) + ->choose(); + } - public static function getPostReblogs($id, $start = 0, $stop = 10) - { - if(!Redis::zcard(self::REBLOGS_KEY . $id)) { - return Cache::remember(self::COLDBOOT_KEY . $id, 86400, function() use($id) { - return Status::whereReblogOfId($id) - ->pluck('id') - ->each(function($reblog) use($id) { - self::addPostReblog($id, $reblog); - }) - ->map(function($reblog) { - return (string) $reblog; - }); - }); - } - return Redis::zrange(self::REBLOGS_KEY . $id, $start, $stop); - } + public static function getFromDatabaseCheck($profileId, $statusId) + { + if (! Redis::zcard(self::CACHE_KEY.$profileId)) { + if (Cache::has(self::CACHE_SKIP_KEY.$profileId)) { + return false; + } else { + self::warmCache($profileId); + sleep(1); - public static function addPostReblog($parentId, $reblogId) - { - $pid = intval($parentId); - $id = intval($reblogId); - if($pid && $id) { - return Redis::zadd(self::REBLOGS_KEY . $pid, $id, $id); - } - } + return self::getFromRedis($profileId, $statusId); + } + } - public static function removePostReblog($parentId, $reblogId) - { - $pid = intval($parentId); - $id = intval($reblogId); - if($pid && $id) { - return Redis::zrem(self::REBLOGS_KEY . $pid, $id); - } - } + $minId = SnowflakeService::byDate(now()->subMonths(12)); + + if ($minId > $statusId) { + return Redis::zscore(self::CACHE_KEY.$profileId, $statusId) != null; + } + + $cachedRes = (bool) Redis::zscore(self::CACHE_KEY.$profileId, $statusId) != null; + $databaseRes = (bool) self::getFromDatabase($profileId, $statusId); + + if ($cachedRes === $databaseRes) { + return $cachedRes; + } + + self::warmCache($profileId); + sleep(1); + + return self::getFromDatabase($profileId, $statusId); + } + + public static function getFromRedis($profileId, $statusId) + { + if (! Redis::zcard(self::CACHE_KEY.$profileId)) { + if (Cache::has(self::CACHE_SKIP_KEY.$profileId)) { + return false; + } else { + self::warmCache($profileId); + sleep(1); + + return self::getFromDatabase($profileId, $statusId); + } + } + + return Redis::zscore(self::CACHE_KEY.$profileId, $statusId) != null; + } + + public static function getFromDatabase($profileId, $statusId) + { + return Status::whereProfileId($profileId) + ->where('reblog_of_id', $statusId) + ->exists(); + } + + public static function add($profileId, $statusId) + { + return Redis::zadd(self::CACHE_KEY.$profileId, $statusId, $statusId); + } + + public static function count($profileId) + { + return Redis::zcard(self::CACHE_KEY.$profileId); + } + + public static function del($profileId, $statusId) + { + return Redis::zrem(self::CACHE_KEY.$profileId, $statusId); + } + + public static function getWarmCacheCount($profileId) + { + $minId = SnowflakeService::byDate(now()->subMonths(12)); + + return Status::where('id', '>', $minId) + ->whereProfileId($profileId) + ->whereNotNull('reblog_of_id') + ->count(); + } + + public static function warmCache($profileId) + { + Redis::del(self::CACHE_KEY.$profileId); + $minId = SnowflakeService::byDate(now()->subMonths(12)); + foreach ( + Status::where('id', '>', $minId) + ->whereProfileId($profileId) + ->whereNotNull('reblog_of_id') + ->lazy() as $post + ) { + self::add($profileId, $post->reblog_of_id); + } + Cache::put(self::CACHE_SKIP_KEY.$profileId, 1, now()->addHours(24)); + } + + public static function getPostReblogs($id, $start = 0, $stop = 10) + { + if (! Redis::zcard(self::REBLOGS_KEY.$id)) { + return Cache::remember(self::COLDBOOT_KEY.$id, 86400, function () use ($id) { + return Status::whereReblogOfId($id) + ->pluck('id') + ->each(function ($reblog) use ($id) { + self::addPostReblog($id, $reblog); + }) + ->map(function ($reblog) { + return (string) $reblog; + }); + }); + } + + return Redis::zrange(self::REBLOGS_KEY.$id, $start, $stop); + } + + public static function addPostReblog($parentId, $reblogId) + { + $pid = intval($parentId); + $id = intval($reblogId); + if ($pid && $id) { + return Redis::zadd(self::REBLOGS_KEY.$pid, $id, $id); + } + } + + public static function removePostReblog($parentId, $reblogId) + { + $pid = intval($parentId); + $id = intval($reblogId); + if ($pid && $id) { + return Redis::zrem(self::REBLOGS_KEY.$pid, $id); + } + } } diff --git a/app/Services/SearchApiV2Service.php b/app/Services/SearchApiV2Service.php index 90691f0bd..fc87d2c04 100644 --- a/app/Services/SearchApiV2Service.php +++ b/app/Services/SearchApiV2Service.php @@ -2,28 +2,25 @@ namespace App\Services; -use Cache; -use Illuminate\Support\Facades\Redis; -use App\{Hashtag, Profile, Status}; +use App\Hashtag; +use App\Profile; +use App\Status; use App\Transformer\Api\AccountTransformer; -use App\Transformer\Api\StatusTransformer; -use League\Fractal; -use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\Util\ActivityPub\Helpers; use Illuminate\Support\Str; -use App\Services\AccountService; -use App\Services\HashtagService; -use App\Services\StatusService; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; class SearchApiV2Service { private $query; - static $mastodonMode = false; + + public static $mastodonMode = false; public static function query($query, $mastodonMode = false) { self::$mastodonMode = $mastodonMode; + return (new self)->run($query); } @@ -32,51 +29,51 @@ class SearchApiV2Service $this->query = $query; $q = urldecode($query->input('q')); - if($query->has('resolve') && - ( Str::startsWith($q, 'https://') || + if ($query->has('resolve') && + (Str::startsWith($q, 'https://') || Str::substrCount($q, '@') >= 1) ) { return $this->resolveQuery(); } - if($query->has('type')) { + if ($query->has('type')) { switch ($query->input('type')) { case 'accounts': return [ 'accounts' => $this->accounts(), 'hashtags' => [], - 'statuses' => [] + 'statuses' => [], ]; break; case 'hashtags': return [ 'accounts' => [], 'hashtags' => $this->hashtags(), - 'statuses' => [] + 'statuses' => [], ]; break; case 'statuses': return [ 'accounts' => [], 'hashtags' => [], - 'statuses' => $this->statuses() + 'statuses' => $this->statuses(), ]; break; } } - if($query->has('account_id')) { + if ($query->has('account_id')) { return [ 'accounts' => [], 'hashtags' => [], - 'statuses' => $this->statusesById() + 'statuses' => $this->statusesById(), ]; } return [ 'accounts' => $this->accounts(), 'hashtags' => $this->hashtags(), - 'statuses' => $this->statuses() + 'statuses' => $this->statuses(), ]; } @@ -87,15 +84,23 @@ class SearchApiV2Service $limit = $this->query->input('limit') ?? 20; $offset = $this->query->input('offset') ?? 0; $rawQuery = $initalQuery ? $initalQuery : $this->query->input('q'); - $query = $rawQuery . '%'; + $query = $rawQuery.'%'; $webfingerQuery = $query; - if(Str::substrCount($rawQuery, '@') == 1 && substr($rawQuery, 0, 1) !== '@') { - $query = '@' . $query; + if (Str::substrCount($rawQuery, '@') == 1 && substr($rawQuery, 0, 1) !== '@') { + $query = '@'.$query; } - if(substr($webfingerQuery, 0, 1) !== '@') { - $webfingerQuery = '@' . $webfingerQuery; + if (substr($webfingerQuery, 0, 1) !== '@') { + $webfingerQuery = '@'.$webfingerQuery; + } + $banned = InstanceService::getBannedDomains() ?? []; + $domainBlocks = UserFilterService::domainBlocks($user->profile_id); + if ($domainBlocks && count($domainBlocks)) { + $banned = array_unique( + array_values( + array_merge($banned, $domainBlocks) + ) + ); } - $banned = InstanceService::getBannedDomains(); $operator = config('database.default') === 'pgsql' ? 'ilike' : 'like'; $results = Profile::select('username', 'id', 'followers_count', 'domain') ->where('username', $operator, $query) @@ -104,16 +109,16 @@ class SearchApiV2Service ->offset($offset) ->limit($limit) ->get() - ->filter(function($profile) use ($banned) { + ->filter(function ($profile) use ($banned) { return in_array($profile->domain, $banned) == false; }) - ->map(function($res) use($mastodonMode) { + ->map(function ($res) use ($mastodonMode) { return $mastodonMode ? AccountService::getMastodon($res['id']) : AccountService::get($res['id']); }) - ->filter(function($account) { - return $account && isset($account['id']); + ->filter(function ($account) { + return $account && isset($account['id']) && ! isset($account['moved'], $account['moved']['id']); }) ->values(); @@ -126,31 +131,31 @@ class SearchApiV2Service $q = $this->query->input('q'); $limit = $this->query->input('limit') ?? 20; $offset = $this->query->input('offset') ?? 0; - $query = Str::startsWith($q, '#') ? '%' . substr($q, 1) . '%' : '%' . $q . '%'; + $query = Str::startsWith($q, '#') ? substr($q, 1).'%' : $q; $operator = config('database.default') === 'pgsql' ? 'ilike' : 'like'; + return Hashtag::where('name', $operator, $query) - ->orWhere('slug', $operator, $query) - ->where(function($q) { - return $q->where('can_search', true) - ->orWhereNull('can_search'); - }) ->orderByDesc('cached_count') ->offset($offset) ->limit($limit) ->get() - ->map(function($tag) use($mastodonMode) { + ->filter(function ($tag) { + return $tag->can_search != false; + }) + ->map(function ($tag) use ($mastodonMode) { $res = [ 'name' => $tag->name, - 'url' => $tag->url() + 'url' => $tag->url(), ]; - if(!$mastodonMode) { + if (! $mastodonMode) { $res['history'] = []; - $res['count'] = HashtagService::count($tag->id); + $res['count'] = $tag->cached_count ?? 0; } return $res; - }); + }) + ->values(); } protected function statuses() @@ -167,73 +172,98 @@ class SearchApiV2Service protected function resolveQuery() { - $default = [ + $default = [ 'accounts' => [], 'hashtags' => [], 'statuses' => [], ]; + $user = request()->user(); $mastodonMode = self::$mastodonMode; $query = urldecode($this->query->input('q')); - if(substr($query, 0, 1) === '@' && !Str::contains($query, '.')) { + $banned = InstanceService::getBannedDomains(); + $domainBlocks = UserFilterService::domainBlocks($user->profile_id); + if ($domainBlocks && count($domainBlocks)) { + $banned = array_unique( + array_values( + array_merge($banned, $domainBlocks) + ) + ); + } + if (substr($query, 0, 1) === '@' && ! Str::contains($query, '.')) { $default['accounts'] = $this->accounts(substr($query, 1)); + return $default; } - if(Helpers::validateLocalUrl($query)) { - if(Str::contains($query, '/p/') || Str::contains($query, 'i/web/post/')) { + if (Helpers::validateLocalUrl($query)) { + if (Str::contains($query, '/p/') || Str::contains($query, 'i/web/post/')) { return $this->resolveLocalStatus(); - } else if(Str::contains($query, 'i/web/profile/')) { + } elseif (Str::contains($query, 'i/web/profile/')) { return $this->resolveLocalProfileId(); } else { return $this->resolveLocalProfile(); } } else { - if(!Helpers::validateUrl($query) && strpos($query, '@') == -1) { + if (! Helpers::validateUrl($query) && strpos($query, '@') == -1) { return $default; } - if(!Str::startsWith($query, 'http') && Str::substrCount($query, '@') == 1 && strpos($query, '@') !== 0) { + if (! Str::startsWith($query, 'http') && Str::substrCount($query, '@') == 1 && strpos($query, '@') !== 0) { try { - $res = WebfingerService::lookup('@' . $query, $mastodonMode); + $res = WebfingerService::lookup('@'.$query, $mastodonMode); } catch (\Exception $e) { return $default; } - if($res && isset($res['id'])) { + if ($res && isset($res['id'], $res['url'])) { + $domain = strtolower(parse_url($res['url'], PHP_URL_HOST)); + if (in_array($domain, $banned)) { + return $default; + } $default['accounts'][] = $res; + return $default; } else { return $default; } } - if(Str::substrCount($query, '@') == 2) { + if (Str::substrCount($query, '@') == 2) { try { $res = WebfingerService::lookup($query, $mastodonMode); } catch (\Exception $e) { return $default; } - if($res && isset($res['id'])) { + if ($res && isset($res['id'])) { + $domain = strtolower(parse_url($res['url'], PHP_URL_HOST)); + if (in_array($domain, $banned)) { + return $default; + } $default['accounts'][] = $res; + return $default; } else { return $default; } } - if($sid = Status::whereUri($query)->first()) { + if ($sid = Status::whereUri($query)->first()) { $s = StatusService::get($sid->id, false); - if(in_array($s['visibility'], ['public', 'unlisted'])) { + if (! $s || isset($s['account']['moved'], $s['account']['moved']['id'])) { + return $default; + } + if (in_array($s['visibility'], ['public', 'unlisted'])) { $default['statuses'][] = $s; + return $default; } } try { $res = ActivityPubFetchService::get($query); - $banned = InstanceService::getBannedDomains(); - if($res) { + + if ($res) { $json = json_decode($res, true); - if(!$json || !isset($json['@context']) || !isset($json['type']) || !in_array($json['type'], ['Note', 'Person'])) { + if (! $json || ! isset($json['@context']) || ! isset($json['type']) || ! in_array($json['type'], ['Note', 'Person'])) { return [ 'accounts' => [], 'hashtags' => [], @@ -241,38 +271,40 @@ class SearchApiV2Service ]; } - switch($json['type']) { + switch ($json['type']) { case 'Note': $obj = Helpers::statusFetch($query); - if(!$obj || !isset($obj['id'])) { + if (! $obj || ! isset($obj['id'])) { return $default; } $note = $mastodonMode ? StatusService::getMastodon($obj['id'], false) : StatusService::get($obj['id'], false); - if(!$note) { + if (! $note) { return $default; } - if(!isset($note['visibility']) || !in_array($note['visibility'], ['public', 'unlisted'])) { + if (! isset($note['visibility']) || ! in_array($note['visibility'], ['public', 'unlisted'])) { return $default; } $default['statuses'][] = $note; + return $default; - break; + break; case 'Person': $obj = Helpers::profileFetch($query); - if(!$obj) { + if (! $obj) { return $default; } - if(in_array($obj['domain'], $banned)) { + if (in_array($obj['domain'], $banned)) { return $default; } $default['accounts'][] = $mastodonMode ? AccountService::getMastodon($obj['id'], true) : AccountService::get($obj['id'], true); + return $default; - break; + break; default: return [ @@ -280,7 +312,7 @@ class SearchApiV2Service 'hashtags' => [], 'statuses' => [], ]; - break; + break; } } } catch (\Exception $e) { @@ -300,18 +332,18 @@ class SearchApiV2Service $query = urldecode($this->query->input('q')); $query = last(explode('/', parse_url($query, PHP_URL_PATH))); $status = StatusService::getMastodon($query, false); - if(!$status || !in_array($status['visibility'], ['public', 'unlisted'])) { + if (! $status || ! in_array($status['visibility'], ['public', 'unlisted'])) { return [ 'accounts' => [], 'hashtags' => [], - 'statuses' => [] + 'statuses' => [], ]; } $res = [ 'accounts' => [], 'hashtags' => [], - 'statuses' => [$status] + 'statuses' => [$status], ]; return $res; @@ -326,21 +358,22 @@ class SearchApiV2Service ->whereUsername($query) ->first(); - if(!$profile) { + if (! $profile || $profile->moved_to_profile_id) { return [ 'accounts' => [], 'hashtags' => [], - 'statuses' => [] + 'statuses' => [], ]; } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($profile, new AccountTransformer); + return [ 'accounts' => [$fractal->createData($resource)->toArray()], 'hashtags' => [], - 'statuses' => [] + 'statuses' => [], ]; } @@ -351,22 +384,22 @@ class SearchApiV2Service $profile = Profile::whereNull('status') ->find($query); - if(!$profile) { + if (! $profile) { return [ 'accounts' => [], 'hashtags' => [], - 'statuses' => [] + 'statuses' => [], ]; } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($profile, new AccountTransformer); + return [ 'accounts' => [$fractal->createData($resource)->toArray()], 'hashtags' => [], - 'statuses' => [] + 'statuses' => [], ]; } - } diff --git a/app/Services/Status/UpdateStatusService.php b/app/Services/Status/UpdateStatusService.php index d0a69c451..7ef4d440c 100644 --- a/app/Services/Status/UpdateStatusService.php +++ b/app/Services/Status/UpdateStatusService.php @@ -3,135 +3,133 @@ namespace App\Services\Status; use App\Media; -use App\ModLog; -use App\Status; use App\Models\StatusEdit; -use Purify; -use App\Util\Lexer\Autolink; +use App\ModLog; use App\Services\MediaService; use App\Services\MediaStorageService; use App\Services\StatusService; +use App\Status; +use Purify; class UpdateStatusService { - public static function call(Status $status, $attributes) - { - self::createPreviousEdit($status); - self::updateMediaAttachements($status, $attributes); - self::handleImmediateAttributes($status, $attributes); - self::createEdit($status, $attributes); + public static function call(Status $status, $attributes) + { + self::createPreviousEdit($status); + self::updateMediaAttachements($status, $attributes); + self::handleImmediateAttributes($status, $attributes); + self::createEdit($status, $attributes); - return StatusService::get($status->id); - } + return StatusService::get($status->id); + } - public static function updateMediaAttachements(Status $status, $attributes) - { - $count = $status->media()->count(); - if($count === 0 || $count === 1) { - return; - } + public static function updateMediaAttachements(Status $status, $attributes) + { + $count = $status->media()->count(); + if ($count === 0 || $count === 1) { + return; + } - $oids = $status->media()->orderBy('order')->pluck('id')->map(function($m) { return (string) $m; }); - $nids = collect($attributes['media_ids']); + $oids = $status->media()->orderBy('order')->pluck('id')->map(function ($m) { + return (string) $m; + }); + $nids = collect($attributes['media_ids']); - if($oids->toArray() === $nids->toArray()) { - return; - } + if ($oids->toArray() === $nids->toArray()) { + return; + } - foreach($oids->diff($nids)->values()->toArray() as $mid) { - $media = Media::find($mid); - if(!$media) { - continue; - } - $media->status_id = null; - $media->save(); - MediaStorageService::delete($media, true); - } + foreach ($oids->diff($nids)->values()->toArray() as $mid) { + $media = Media::find($mid); + if (! $media) { + continue; + } + $media->status_id = null; + $media->save(); + MediaStorageService::delete($media, true); + } - $nids->each(function($nid, $idx) { - $media = Media::find($nid); - if(!$media) { - return; - } - $media->order = $idx; - $media->save(); - }); - MediaService::del($status->id); - } + $nids->each(function ($nid, $idx) { + $media = Media::find($nid); + if (! $media) { + return; + } + $media->order = $idx; + $media->save(); + }); + MediaService::del($status->id); + } - public static function handleImmediateAttributes(Status $status, $attributes) - { - if(isset($attributes['status'])) { - $cleaned = Purify::clean($attributes['status']); - $status->caption = $cleaned; - $status->rendered = nl2br(Autolink::create()->autolink($cleaned)); - } else { - $status->caption = null; - $status->rendered = null; - } - if(isset($attributes['sensitive'])) { - if($status->is_nsfw != (bool) $attributes['sensitive'] && - (bool) $attributes['sensitive'] == false) - { - $exists = ModLog::whereObjectType('App\Status::class') - ->whereObjectId($status->id) - ->whereAction('admin.status.moderate') - ->exists(); - if(!$exists) { - $status->is_nsfw = (bool) $attributes['sensitive']; - } - } else { - $status->is_nsfw = (bool) $attributes['sensitive']; - } - } - if(isset($attributes['spoiler_text'])) { - $status->cw_summary = Purify::clean($attributes['spoiler_text']); - } else { - $status->cw_summary = null; - } - if(isset($attributes['location'])) { - if (isset($attributes['location']['id'])) { - $status->place_id = $attributes['location']['id']; - } else { - $status->place_id = null; - } - } - if($status->cw_summary && !$status->is_nsfw) { - $status->cw_summary = null; - } - $status->edited_at = now(); - $status->save(); - StatusService::del($status->id); - } + public static function handleImmediateAttributes(Status $status, $attributes) + { + if (isset($attributes['status'])) { + $cleaned = Purify::clean($attributes['status']); + $status->caption = $cleaned; + } else { + $status->caption = null; + } + if (isset($attributes['sensitive'])) { + if ($status->is_nsfw != (bool) $attributes['sensitive'] && + (bool) $attributes['sensitive'] == false) { + $exists = ModLog::whereObjectType('App\Status::class') + ->whereObjectId($status->id) + ->whereAction('admin.status.moderate') + ->exists(); + if (! $exists) { + $status->is_nsfw = (bool) $attributes['sensitive']; + } + } else { + $status->is_nsfw = (bool) $attributes['sensitive']; + } + } + if (isset($attributes['spoiler_text'])) { + $status->cw_summary = Purify::clean($attributes['spoiler_text']); + } else { + $status->cw_summary = null; + } + if (isset($attributes['location'])) { + if (isset($attributes['location']['id'])) { + $status->place_id = $attributes['location']['id']; + } else { + $status->place_id = null; + } + } + if ($status->cw_summary && ! $status->is_nsfw) { + $status->cw_summary = null; + } + $status->edited_at = now(); + $status->save(); + StatusService::del($status->id); + } - public static function createPreviousEdit(Status $status) - { - if(!$status->edits()->count()) { - StatusEdit::create([ - 'status_id' => $status->id, - 'profile_id' => $status->profile_id, - 'caption' => $status->caption, - 'spoiler_text' => $status->cw_summary, - 'is_nsfw' => $status->is_nsfw, - 'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(), - 'created_at' => $status->created_at - ]); - } - } + public static function createPreviousEdit(Status $status) + { + if (! $status->edits()->count()) { + StatusEdit::create([ + 'status_id' => $status->id, + 'profile_id' => $status->profile_id, + 'caption' => $status->caption, + 'spoiler_text' => $status->cw_summary, + 'is_nsfw' => $status->is_nsfw, + 'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(), + 'created_at' => $status->created_at, + ]); + } + } - public static function createEdit(Status $status, $attributes) - { - $cleaned = isset($attributes['status']) ? Purify::clean($attributes['status']) : null; - $spoiler_text = isset($attributes['spoiler_text']) ? Purify::clean($attributes['spoiler_text']) : null; - $sensitive = isset($attributes['sensitive']) ? $attributes['sensitive'] : null; - $mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null; - StatusEdit::create([ - 'status_id' => $status->id, - 'profile_id' => $status->profile_id, - 'caption' => $cleaned, - 'spoiler_text' => $spoiler_text, - 'is_nsfw' => $sensitive, - 'ordered_media_attachment_ids' => $mids - ]); - } + public static function createEdit(Status $status, $attributes) + { + $cleaned = isset($attributes['status']) ? Purify::clean($attributes['status']) : null; + $spoiler_text = isset($attributes['spoiler_text']) ? Purify::clean($attributes['spoiler_text']) : null; + $sensitive = isset($attributes['sensitive']) ? $attributes['sensitive'] : null; + $mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null; + StatusEdit::create([ + 'status_id' => $status->id, + 'profile_id' => $status->profile_id, + 'caption' => $cleaned, + 'spoiler_text' => $spoiler_text, + 'is_nsfw' => $sensitive, + 'ordered_media_attachment_ids' => $mids, + ]); + } } diff --git a/app/Services/StatusHashtagService.php b/app/Services/StatusHashtagService.php index ececca633..1f86b2de8 100644 --- a/app/Services/StatusHashtagService.php +++ b/app/Services/StatusHashtagService.php @@ -2,100 +2,97 @@ namespace App\Services; -use Cache; -use Illuminate\Support\Facades\Redis; -use App\{Status, StatusHashtag}; -use App\Transformer\Api\StatusHashtagTransformer; +use App\Hashtag; +use App\Status; +use App\StatusHashtag; use App\Transformer\Api\HashtagTransformer; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; -class StatusHashtagService { +class StatusHashtagService +{ + const CACHE_KEY = 'pf:services:status-hashtag:collection:'; - const CACHE_KEY = 'pf:services:status-hashtag:collection:'; + public static function get($id, $page = 1, $stop = 9) + { + if ($page > 20) { + return []; + } - public static function get($id, $page = 1, $stop = 9) - { - if($page > 20) { - return []; - } + $pid = request()->user() ? request()->user()->profile_id : false; + $filtered = $pid ? UserFilterService::filters($pid) : []; - $pid = request()->user() ? request()->user()->profile_id : false; - $filtered = $pid ? UserFilterService::filters($pid) : []; + return StatusHashtag::whereHashtagId($id) + ->whereStatusVisibility('public') + ->skip($stop) + ->latest() + ->take(9) + ->pluck('status_id') + ->map(function ($i, $k) use ($id) { + return self::getStatus($i, $id); + }) + ->filter(function ($i) use ($filtered) { + return isset($i['status']) && + ! empty($i['status']) && ! in_array($i['status']['account']['id'], $filtered) && + isset($i['status']['media_attachments']) && + ! empty($i['status']['media_attachments']); + }) + ->values(); + } - return StatusHashtag::whereHashtagId($id) - ->whereStatusVisibility('public') - ->skip($stop) - ->latest() - ->take(9) - ->pluck('status_id') - ->map(function ($i, $k) use ($id) { - return self::getStatus($i, $id); - }) - ->filter(function ($i) use($filtered) { - return isset($i['status']) && - !empty($i['status']) && !in_array($i['status']['account']['id'], $filtered) && - isset($i['status']['media_attachments']) && - !empty($i['status']['media_attachments']); - }) - ->values(); - } + public static function coldGet($id, $start = 0, $stop = 2000) + { + $stop = $stop > 2000 ? 2000 : $stop; + $ids = StatusHashtag::whereHashtagId($id) + ->whereStatusVisibility('public') + ->whereHas('media') + ->latest() + ->skip($start) + ->take($stop) + ->pluck('status_id'); + foreach ($ids as $key) { + self::set($id, $key); + } - public static function coldGet($id, $start = 0, $stop = 2000) - { - $stop = $stop > 2000 ? 2000 : $stop; - $ids = StatusHashtag::whereHashtagId($id) - ->whereStatusVisibility('public') - ->whereHas('media') - ->latest() - ->skip($start) - ->take($stop) - ->pluck('status_id'); - foreach($ids as $key) { - self::set($id, $key); - } - return $ids; - } + return $ids; + } - public static function set($key, $val) - { - return 1; - } + public static function set($key, $val) + { + return 1; + } - public static function del($key) - { - return 1; - } + public static function del($key) + { + return 1; + } - public static function count($id) - { - $key = 'pf:services:status-hashtag:count:' . $id; - $ttl = now()->addMinutes(5); - return Cache::remember($key, $ttl, function() use($id) { - return StatusHashtag::whereHashtagId($id)->has('media')->count(); - }); - } + public static function count($id) + { + $cc = Hashtag::find($id); + if (! $cc) { + return 0; + } - public static function getStatus($statusId, $hashtagId) - { - return ['status' => StatusService::get($statusId)]; - } + return $cc->cached_count ?? 0; + } - public static function statusTags($statusId) - { - $key = 'pf:services:sh:id:' . $statusId; + public static function getStatus($statusId, $hashtagId) + { + return ['status' => StatusService::get($statusId)]; + } - return Cache::remember($key, 604800, function() use($statusId) { - $status = Status::find($statusId); - if(!$status) { - return []; - } + public static function statusTags($statusId) + { + $status = Status::with('hashtags')->find($statusId); + if (! $status) { + return []; + } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Collection($status->hashtags, new HashtagTransformer()); - return $fractal->createData($resource)->toArray(); - }); - } + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Collection($status->hashtags, new HashtagTransformer()); + + return $fractal->createData($resource)->toArray(); + } } diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php index 4051bede4..de2f4d112 100644 --- a/app/Services/StatusService.php +++ b/app/Services/StatusService.php @@ -2,68 +2,67 @@ namespace App\Services; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Redis; -use DB; use App\Status; use App\Transformer\Api\StatusStatelessTransformer; -use App\Transformer\Api\StatusTransformer; +use Illuminate\Support\Facades\Cache; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; class StatusService { - const CACHE_KEY = 'pf:services:status:'; + const CACHE_KEY = 'pf:services:status:v1.1:'; public static function key($id, $publicOnly = true) { $p = $publicOnly ? 'pub:' : 'all:'; - return self::CACHE_KEY . $p . $id; + + return self::CACHE_KEY.$p.$id; } public static function get($id, $publicOnly = true, $mastodonMode = false) { - $res = Cache::remember(self::key($id, $publicOnly), 21600, function() use($id, $publicOnly) { - if($publicOnly) { + $res = Cache::remember(self::key($id, $publicOnly), 21600, function () use ($id, $publicOnly) { + if ($publicOnly) { $status = Status::whereScope('public')->find($id); } else { $status = Status::whereIn('scope', ['public', 'private', 'unlisted', 'group'])->find($id); } - if(!$status) { + if (! $status) { return null; } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer); $res = $fractal->createData($resource)->toArray(); $res['_pid'] = isset($res['account']) && isset($res['account']['id']) ? $res['account']['id'] : null; - if(isset($res['_pid'])) { + if (isset($res['_pid'])) { unset($res['account']); } + return $res; }); - if($res && isset($res['_pid'])) { + if ($res && isset($res['_pid'])) { $res['account'] = $mastodonMode === true ? AccountService::getMastodon($res['_pid'], true) : AccountService::get($res['_pid'], true); unset($res['_pid']); } + return $res; } public static function getMastodon($id, $publicOnly = true) { $status = self::get($id, $publicOnly, true); - if(!$status) { + if (! $status) { return null; } - if(!isset($status['account'])) { + if (! isset($status['account'])) { return null; } $status['replies_count'] = $status['reply_count']; - if(config('exp.emc') == false) { + if (config('exp.emc') == false) { return $status; } @@ -113,28 +112,29 @@ class StatusService { $status = self::get($id, false); - if(!$status) { + if (! $status || ! $pid) { return [ 'liked' => false, 'shared' => false, - 'bookmarked' => false + 'bookmarked' => false, ]; } return [ 'liked' => LikeService::liked($pid, $id), 'shared' => self::isShared($id, $pid), - 'bookmarked' => self::isBookmarked($id, $pid) + 'bookmarked' => self::isBookmarked($id, $pid), ]; } public static function getFull($id, $pid, $publicOnly = true) { $res = self::get($id, $publicOnly); - if(!$res || !isset($res['account']) || !isset($res['account']['id'])) { + if (! $res || ! isset($res['account']) || ! isset($res['account']['id'])) { return $res; } $res['relationship'] = RelationshipService::get($pid, $res['account']['id']); + return $res; } @@ -142,31 +142,33 @@ class StatusService { $status = Status::whereScope('direct')->find($id); - if(!$status) { + if (! $status) { return null; } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer); + return $fractal->createData($resource)->toArray(); } public static function del($id, $purge = false) { - if($purge) { + if ($purge) { $status = self::get($id); - if($status && isset($status['account']) && isset($status['account']['id'])) { - Cache::forget('profile:embed:' . $status['account']['id']); + if ($status && isset($status['account']) && isset($status['account']['id'])) { + Cache::forget('profile:embed:'.$status['account']['id']); } - Cache::forget('status:transformer:media:attachments:' . $id); + Cache::forget('status:transformer:media:attachments:'.$id); MediaService::del($id); - Cache::forget('pf:services:sh:id:' . $id); + Cache::forget('pf:services:sh:id:'.$id); PublicTimelineService::rem($id); NetworkTimelineService::rem($id); } Cache::forget(self::key($id, false)); + return Cache::forget(self::key($id)); } @@ -191,4 +193,9 @@ class StatusService BookmarkService::get($pid, $id) : false; } + + public static function totalLocalStatuses() + { + return InstanceService::totalLocalStatuses(); + } } diff --git a/app/Services/UserFilterService.php b/app/Services/UserFilterService.php index 1dcdc819a..5673db60c 100644 --- a/app/Services/UserFilterService.php +++ b/app/Services/UserFilterService.php @@ -4,145 +4,160 @@ namespace App\Services; use Cache; use App\UserFilter; +use App\Models\UserDomainBlock; use Illuminate\Support\Facades\Redis; class UserFilterService { - const USER_MUTES_KEY = 'pf:services:mutes:ids:'; - const USER_BLOCKS_KEY = 'pf:services:blocks:ids:'; + const USER_MUTES_KEY = 'pf:services:mutes:ids:'; + const USER_BLOCKS_KEY = 'pf:services:blocks:ids:'; + const USER_DOMAIN_KEY = 'pf:services:domain-blocks:ids:'; - public static function mutes(int $profile_id) - { - $key = self::USER_MUTES_KEY . $profile_id; - $warm = Cache::has($key . ':cached-v0'); - if($warm) { - return Redis::zrevrange($key, 0, -1) ?? []; - } else { - if(Redis::zrevrange($key, 0, -1)) { - return Redis::zrevrange($key, 0, -1); - } - $ids = UserFilter::whereFilterType('mute') - ->whereUserId($profile_id) - ->pluck('filterable_id') - ->map(function($id) { - $acct = AccountService::get($id, true); - if(!$acct) { - return false; - } - return $acct['id']; - }) - ->filter(function($res) { - return $res; - }) - ->values() - ->toArray(); - foreach ($ids as $muted_id) { - Redis::zadd($key, (int) $muted_id, (int) $muted_id); - } - Cache::set($key . ':cached-v0', 1, 7776000); - return $ids; - } - } + public static function mutes(int $profile_id) + { + $key = self::USER_MUTES_KEY . $profile_id; + $warm = Cache::has($key . ':cached-v0'); + if($warm) { + return Redis::zrevrange($key, 0, -1) ?? []; + } else { + if(Redis::zrevrange($key, 0, -1)) { + return Redis::zrevrange($key, 0, -1); + } + $ids = UserFilter::whereFilterType('mute') + ->whereUserId($profile_id) + ->pluck('filterable_id') + ->map(function($id) { + $acct = AccountService::get($id, true); + if(!$acct) { + return false; + } + return $acct['id']; + }) + ->filter(function($res) { + return $res; + }) + ->values() + ->toArray(); + foreach ($ids as $muted_id) { + Redis::zadd($key, (int) $muted_id, (int) $muted_id); + } + Cache::set($key . ':cached-v0', 1, 7776000); + return $ids; + } + } - public static function blocks(int $profile_id) - { - $key = self::USER_BLOCKS_KEY . $profile_id; - $warm = Cache::has($key . ':cached-v0'); - if($warm) { - return Redis::zrevrange($key, 0, -1) ?? []; - } else { - if(Redis::zrevrange($key, 0, -1)) { - return Redis::zrevrange($key, 0, -1); - } - $ids = UserFilter::whereFilterType('block') - ->whereUserId($profile_id) - ->pluck('filterable_id') - ->map(function($id) { - $acct = AccountService::get($id, true); - if(!$acct) { - return false; - } - return $acct['id']; - }) - ->filter(function($res) { - return $res; - }) - ->values() - ->toArray(); - foreach ($ids as $blocked_id) { - Redis::zadd($key, (int) $blocked_id, (int) $blocked_id); - } - Cache::set($key . ':cached-v0', 1, 7776000); - return $ids; - } - } + public static function blocks(int $profile_id) + { + $key = self::USER_BLOCKS_KEY . $profile_id; + $warm = Cache::has($key . ':cached-v0'); + if($warm) { + return Redis::zrevrange($key, 0, -1) ?? []; + } else { + if(Redis::zrevrange($key, 0, -1)) { + return Redis::zrevrange($key, 0, -1); + } + $ids = UserFilter::whereFilterType('block') + ->whereUserId($profile_id) + ->pluck('filterable_id') + ->map(function($id) { + $acct = AccountService::get($id, true); + if(!$acct) { + return false; + } + return $acct['id']; + }) + ->filter(function($res) { + return $res; + }) + ->values() + ->toArray(); + foreach ($ids as $blocked_id) { + Redis::zadd($key, (int) $blocked_id, (int) $blocked_id); + } + Cache::set($key . ':cached-v0', 1, 7776000); + return $ids; + } + } - public static function filters(int $profile_id) - { - return array_unique(array_merge(self::mutes($profile_id), self::blocks($profile_id))); - } + public static function filters(int $profile_id) + { + return array_unique(array_merge(self::mutes($profile_id), self::blocks($profile_id))); + } - public static function mute(int $profile_id, int $muted_id) - { - if($profile_id == $muted_id) { - return false; - } - $key = self::USER_MUTES_KEY . $profile_id; - $mutes = self::mutes($profile_id); - $exists = in_array($muted_id, $mutes); - if(!$exists) { - Redis::zadd($key, $muted_id, $muted_id); - } - return true; - } + public static function mute(int $profile_id, int $muted_id) + { + if($profile_id == $muted_id) { + return false; + } + $key = self::USER_MUTES_KEY . $profile_id; + $mutes = self::mutes($profile_id); + $exists = in_array($muted_id, $mutes); + if(!$exists) { + Redis::zadd($key, $muted_id, $muted_id); + } + return true; + } - public static function unmute(int $profile_id, string $muted_id) - { - if($profile_id == $muted_id) { - return false; - } - $key = self::USER_MUTES_KEY . $profile_id; - $mutes = self::mutes($profile_id); - $exists = in_array($muted_id, $mutes); - if($exists) { - Redis::zrem($key, $muted_id); - } - return true; - } + public static function unmute(int $profile_id, string $muted_id) + { + if($profile_id == $muted_id) { + return false; + } + $key = self::USER_MUTES_KEY . $profile_id; + $mutes = self::mutes($profile_id); + $exists = in_array($muted_id, $mutes); + if($exists) { + Redis::zrem($key, $muted_id); + } + return true; + } - public static function block(int $profile_id, int $blocked_id) - { - if($profile_id == $blocked_id) { - return false; - } - $key = self::USER_BLOCKS_KEY . $profile_id; - $exists = in_array($blocked_id, self::blocks($profile_id)); - if(!$exists) { - Redis::zadd($key, $blocked_id, $blocked_id); - } - return true; - } + public static function block(int $profile_id, int $blocked_id) + { + if($profile_id == $blocked_id) { + return false; + } + $key = self::USER_BLOCKS_KEY . $profile_id; + $exists = in_array($blocked_id, self::blocks($profile_id)); + if(!$exists) { + Redis::zadd($key, $blocked_id, $blocked_id); + } + return true; + } - public static function unblock(int $profile_id, string $blocked_id) - { - if($profile_id == $blocked_id) { - return false; - } - $key = self::USER_BLOCKS_KEY . $profile_id; - $exists = in_array($blocked_id, self::blocks($profile_id)); - if($exists) { - Redis::zrem($key, $blocked_id); - } - return $exists; - } + public static function unblock(int $profile_id, string $blocked_id) + { + if($profile_id == $blocked_id) { + return false; + } + $key = self::USER_BLOCKS_KEY . $profile_id; + $exists = in_array($blocked_id, self::blocks($profile_id)); + if($exists) { + Redis::zrem($key, $blocked_id); + } + return $exists; + } - public static function blockCount(int $profile_id) - { - return Redis::zcard(self::USER_BLOCKS_KEY . $profile_id); - } + public static function blockCount(int $profile_id) + { + return Redis::zcard(self::USER_BLOCKS_KEY . $profile_id); + } - public static function muteCount(int $profile_id) - { - return Redis::zcard(self::USER_MUTES_KEY . $profile_id); - } + public static function muteCount(int $profile_id) + { + return Redis::zcard(self::USER_MUTES_KEY . $profile_id); + } + + public static function domainBlocks($pid, $purge = false) + { + if($purge) { + Cache::forget(self::USER_DOMAIN_KEY . $pid); + } + return Cache::remember( + self::USER_DOMAIN_KEY . $pid, + 21600, + function() use($pid) { + return UserDomainBlock::whereProfileId($pid)->pluck('domain')->toArray(); + }); + } } diff --git a/app/Services/UserRoleService.php b/app/Services/UserRoleService.php new file mode 100644 index 000000000..ed765a930 --- /dev/null +++ b/app/Services/UserRoleService.php @@ -0,0 +1,230 @@ +first()) { + return $roles->roles; + } + + return self::defaultRoles(); + } + + public static function roleKeys() + { + return array_keys(self::defaultRoles()); + } + + public static function defaultRoles() + { + return [ + 'account-force-private' => true, + 'account-ignore-follow-requests' => true, + + 'can-view-public-feed' => true, + 'can-view-network-feed' => true, + 'can-view-discover' => true, + 'can-view-hashtag-feed' => false, + + 'can-post' => true, + 'can-comment' => true, + 'can-like' => true, + 'can-share' => true, + + 'can-follow' => false, + 'can-make-public' => false, + + 'can-direct-message' => false, + 'can-use-stories' => false, + 'can-view-sensitive' => false, + 'can-bookmark' => false, + 'can-collections' => false, + 'can-federation' => false, + ]; + } + + public static function getRoles($id) + { + $myRoles = self::get($id); + $roleData = collect(self::roleData()) + ->map(function($role, $k) use($myRoles) { + $role['value'] = $myRoles[$k]; + return $role; + }) + ->toArray(); + return $roleData; + } + + public static function roleData() + { + return [ + 'account-force-private' => [ + 'title' => 'Force Private Account', + 'action' => 'Prevent changing account from private' + ], + 'account-ignore-follow-requests' => [ + 'title' => 'Ignore Follow Requests', + 'action' => 'Hide follow requests and associated notifications' + ], + 'can-view-public-feed' => [ + 'title' => 'Hide Public Feed', + 'action' => 'Hide the public feed timeline' + ], + 'can-view-network-feed' => [ + 'title' => 'Hide Network Feed', + 'action' => 'Hide the network feed timeline' + ], + 'can-view-discover' => [ + 'title' => 'Hide Discover', + 'action' => 'Hide the discover feature' + ], + 'can-post' => [ + 'title' => 'Can post', + 'action' => 'Allows new posts to be shared' + ], + 'can-comment' => [ + 'title' => 'Can comment', + 'action' => 'Allows new comments to be posted' + ], + 'can-like' => [ + 'title' => 'Can Like', + 'action' => 'Allows the ability to like posts and comments' + ], + 'can-share' => [ + 'title' => 'Can Share', + 'action' => 'Allows the ability to share posts and comments' + ], + 'can-follow' => [ + 'title' => 'Can Follow', + 'action' => 'Allows the ability to follow accounts' + ], + 'can-make-public' => [ + 'title' => 'Can make account public', + 'action' => 'Allows the ability to make account public' + ], + + 'can-direct-message' => [ + 'title' => '', + 'action' => '' + ], + 'can-use-stories' => [ + 'title' => '', + 'action' => '' + ], + 'can-view-sensitive' => [ + 'title' => '', + 'action' => '' + ], + 'can-bookmark' => [ + 'title' => '', + 'action' => '' + ], + 'can-collections' => [ + 'title' => '', + 'action' => '' + ], + 'can-federation' => [ + 'title' => '', + 'action' => '' + ], + ]; + } + + public static function mapInvite($id, $data = []) + { + $roles = self::get($id); + + $map = [ + 'account-force-private' => 'private', + 'account-ignore-follow-requests' => 'private', + + 'can-view-public-feed' => 'discovery_feeds', + 'can-view-network-feed' => 'discovery_feeds', + 'can-view-discover' => 'discovery_feeds', + 'can-view-hashtag-feed' => 'discovery_feeds', + + 'can-post' => 'post', + 'can-comment' => 'comment', + 'can-like' => 'like', + 'can-share' => 'share', + + 'can-follow' => 'follow', + 'can-make-public' => '!private', + + 'can-direct-message' => 'dms', + 'can-use-stories' => 'story', + 'can-view-sensitive' => '!hide_cw', + 'can-bookmark' => 'bookmark', + 'can-collections' => 'collection', + 'can-federation' => 'federation', + ]; + + foreach ($map as $key => $value) { + if(!isset($data[$value]) && !isset($data[substr($value, 1)])) { + $map[$key] = false; + continue; + } + $map[$key] = str_starts_with($value, '!') ? !$data[substr($value, 1)] : $data[$value]; + } + + return $map; + } + + public static function mapActions($id, $data = []) + { + $res = []; + $map = [ + 'account-force-private' => 'private', + 'account-ignore-follow-requests' => 'private', + + 'can-view-public-feed' => 'discovery_feeds', + 'can-view-network-feed' => 'discovery_feeds', + 'can-view-discover' => 'discovery_feeds', + 'can-view-hashtag-feed' => 'discovery_feeds', + + 'can-post' => 'post', + 'can-comment' => 'comment', + 'can-like' => 'like', + 'can-share' => 'share', + + 'can-follow' => 'follow', + 'can-make-public' => '!private', + + 'can-direct-message' => 'dms', + 'can-use-stories' => 'story', + 'can-view-sensitive' => '!hide_cw', + 'can-bookmark' => 'bookmark', + 'can-collections' => 'collection', + 'can-federation' => 'federation', + ]; + + foreach ($map as $key => $value) { + if(!isset($data[$value]) && !isset($data[substr($value, 1)])) { + $res[$key] = false; + continue; + } + $res[$key] = str_starts_with($value, '!') ? !$data[substr($value, 1)] : $data[$value]; + } + + return $res; + } +} diff --git a/app/Services/UserStorageService.php b/app/Services/UserStorageService.php new file mode 100644 index 000000000..0fa5c7e0d --- /dev/null +++ b/app/Services/UserStorageService.php @@ -0,0 +1,48 @@ +status) { + return -1; + } + + if ($user->storage_used_updated_at) { + return (int) $user->storage_used; + } + $updatedVal = self::calculateStorageUsed($id); + $user->storage_used = $updatedVal; + $user->storage_used_updated_at = now(); + $user->save(); + + return $user->storage_used; + } + + public static function calculateStorageUsed($id) + { + return (int) floor(Media::whereUserId($id)->sum('size') / 1000); + } + + public static function recalculateUpdateStorageUsed($id) + { + $user = User::find($id); + if (! $user || $user->status) { + return; + } + $updatedVal = (int) floor(Media::whereUserId($id)->sum('size') / 1000); + $user->storage_used = $updatedVal; + $user->storage_used_updated_at = now(); + $user->save(); + + return $updatedVal; + } +} diff --git a/app/Services/WebfingerService.php b/app/Services/WebfingerService.php index 385bff023..7340109f5 100644 --- a/app/Services/WebfingerService.php +++ b/app/Services/WebfingerService.php @@ -2,69 +2,95 @@ namespace App\Services; -use Cache; use App\Profile; +use App\Util\ActivityPub\Helpers; use App\Util\Webfinger\WebfingerUrl; use Illuminate\Support\Facades\Http; -use App\Util\ActivityPub\Helpers; -use App\Services\AccountService; class WebfingerService { - public static function lookup($query, $mastodonMode = false) - { - return (new self)->run($query, $mastodonMode); - } + public static function rawGet($url) + { + $n = WebfingerUrl::get($url); + if (! $n) { + return false; + } + $webfinger = FetchCacheService::getJson($n); + if (! $webfinger) { + return false; + } - protected function run($query, $mastodonMode) - { - if($profile = Profile::whereUsername($query)->first()) { - return $mastodonMode ? - AccountService::getMastodon($profile->id, true) : - AccountService::get($profile->id); - } - $url = WebfingerUrl::generateWebfingerUrl($query); - if(!Helpers::validateUrl($url)) { - return []; - } + if (! isset($webfinger['links']) || ! is_array($webfinger['links']) || empty($webfinger['links'])) { + return false; + } + $link = collect($webfinger['links']) + ->filter(function ($link) { + return $link && + isset($link['rel'], $link['type'], $link['href']) && + $link['rel'] === 'self' && + in_array($link['type'], ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']); + }) + ->pluck('href') + ->first(); - try { - $res = Http::retry(3, 100) - ->acceptJson() - ->withHeaders([ - 'User-Agent' => '(Pixelfed/' . config('pixelfed.version') . '; +' . config('app.url') . ')' - ]) - ->timeout(20) - ->get($url); - } catch (\Illuminate\Http\Client\ConnectionException $e) { - return []; - } + return $link; + } - if(!$res->successful()) { - return []; - } + public static function lookup($query, $mastodonMode = false) + { + return (new self)->run($query, $mastodonMode); + } - $webfinger = $res->json(); - if(!isset($webfinger['links']) || !is_array($webfinger['links']) || empty($webfinger['links'])) { - return []; - } + protected function run($query, $mastodonMode) + { + if ($profile = Profile::whereUsername($query)->first()) { + return $mastodonMode ? + AccountService::getMastodon($profile->id, true) : + AccountService::get($profile->id); + } + $url = WebfingerUrl::generateWebfingerUrl($query); + if (! Helpers::validateUrl($url)) { + return []; + } - $link = collect($webfinger['links']) - ->filter(function($link) { - return $link && - isset($link['rel'], $link['type'], $link['href']) && - $link['rel'] === 'self' && - in_array($link['type'], ['application/activity+json','application/ld+json; profile="https://www.w3.org/ns/activitystreams"']); - }) - ->pluck('href') - ->first(); + try { + $res = Http::retry(3, 100) + ->acceptJson() + ->withHeaders([ + 'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')', + ]) + ->timeout(20) + ->get($url); + } catch (\Illuminate\Http\Client\ConnectionException $e) { + return []; + } - $profile = Helpers::profileFetch($link); - if(!$profile) { - return; - } - return $mastodonMode ? - AccountService::getMastodon($profile->id, true) : - AccountService::get($profile->id); - } + if (! $res->successful()) { + return []; + } + + $webfinger = $res->json(); + if (! isset($webfinger['links']) || ! is_array($webfinger['links']) || empty($webfinger['links'])) { + return []; + } + + $link = collect($webfinger['links']) + ->filter(function ($link) { + return $link && + isset($link['rel'], $link['type'], $link['href']) && + $link['rel'] === 'self' && + in_array($link['type'], ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']); + }) + ->pluck('href') + ->first(); + + $profile = Helpers::profileFetch($link); + if (! $profile) { + return; + } + + return $mastodonMode ? + AccountService::getMastodon($profile->id, true) : + AccountService::get($profile->id); + } } diff --git a/app/Status.php b/app/Status.php index d665464ae..8b69c199c 100644 --- a/app/Status.php +++ b/app/Status.php @@ -308,46 +308,6 @@ class Status extends Model return $this->comments()->orderBy('created_at', 'desc')->take(3); } - public function toActivityPubObject() - { - if($this->local == false) { - return; - } - $profile = $this->profile; - $to = $this->scopeToAudience('to'); - $cc = $this->scopeToAudience('cc'); - return [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $this->permalink(), - 'type' => 'Create', - 'actor' => $profile->permalink(), - 'published' => str_replace('+00:00', 'Z', $this->created_at->format(DATE_RFC3339_EXTENDED)), - 'to' => $to, - 'cc' => $cc, - 'object' => [ - 'id' => $this->url(), - 'type' => 'Note', - 'summary' => null, - 'inReplyTo' => null, - 'published' => str_replace('+00:00', 'Z', $this->created_at->format(DATE_RFC3339_EXTENDED)), - 'url' => $this->url(), - 'attributedTo' => $this->profile->url(), - 'to' => $to, - 'cc' => $cc, - 'sensitive' => (bool) $this->is_nsfw, - 'content' => $this->rendered, - 'attachment' => $this->media->map(function($media) { - return [ - 'type' => 'Document', - 'mediaType' => $media->mime, - 'url' => $media->url(), - 'name' => null - ]; - })->toArray() - ] - ]; - } - public function scopeToAudience($audience) { if(!in_array($audience, ['to', 'cc']) || $this->local == false) { diff --git a/app/Transformer/ActivityPub/ProfileTransformer.php b/app/Transformer/ActivityPub/ProfileTransformer.php index cdd4eb82d..96d129bf7 100644 --- a/app/Transformer/ActivityPub/ProfileTransformer.php +++ b/app/Transformer/ActivityPub/ProfileTransformer.php @@ -3,66 +3,80 @@ namespace App\Transformer\ActivityPub; use App\Profile; -use League\Fractal; use App\Services\AccountService; +use League\Fractal; class ProfileTransformer extends Fractal\TransformerAbstract { public function transform(Profile $profile) { $res = [ - '@context' => [ - 'https://w3id.org/security/v1', - 'https://www.w3.org/ns/activitystreams', - [ - 'toot' => 'http://joinmastodon.org/ns#', - 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', - 'alsoKnownAs' => [ - '@id' => 'as:alsoKnownAs', - '@type' => '@id' - ], - 'movedTo' => [ - '@id' => 'as:movedTo', - '@type' => '@id' - ], - 'indexable' => 'toot:indexable', + '@context' => [ + 'https://w3id.org/security/v1', + 'https://www.w3.org/ns/activitystreams', + [ + 'toot' => 'http://joinmastodon.org/ns#', + 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', + 'alsoKnownAs' => [ + '@id' => 'as:alsoKnownAs', + '@type' => '@id', + ], + 'movedTo' => [ + '@id' => 'as:movedTo', + '@type' => '@id', + ], + 'indexable' => 'toot:indexable', + 'suspended' => 'toot:suspended', + ], ], - ], - 'id' => $profile->permalink(), - 'type' => 'Person', - 'following' => $profile->permalink('/following'), - 'followers' => $profile->permalink('/followers'), - 'inbox' => $profile->permalink('/inbox'), - 'outbox' => $profile->permalink('/outbox'), - 'preferredUsername' => $profile->username, - 'name' => $profile->name, - 'summary' => $profile->bio, - 'url' => $profile->url(), - 'manuallyApprovesFollowers' => (bool) $profile->is_private, - 'indexable' => (bool) $profile->indexable, - 'publicKey' => [ - 'id' => $profile->permalink().'#main-key', - 'owner' => $profile->permalink(), - 'publicKeyPem' => $profile->public_key, - ], - 'icon' => [ - 'type' => 'Image', - 'mediaType' => 'image/jpeg', - 'url' => $profile->avatarUrl(), - ], - 'endpoints' => [ - 'sharedInbox' => config('app.url') . '/f/inbox' - ] - ]; + 'id' => $profile->permalink(), + 'type' => 'Person', + 'following' => $profile->permalink('/following'), + 'followers' => $profile->permalink('/followers'), + 'inbox' => $profile->permalink('/inbox'), + 'outbox' => $profile->permalink('/outbox'), + 'preferredUsername' => $profile->username, + 'name' => $profile->name, + 'summary' => $profile->bio, + 'url' => $profile->url(), + 'manuallyApprovesFollowers' => (bool) $profile->is_private, + 'indexable' => (bool) $profile->indexable, + 'published' => $profile->created_at->format('Y-m-d').'T00:00:00Z', + 'publicKey' => [ + 'id' => $profile->permalink().'#main-key', + 'owner' => $profile->permalink(), + 'publicKeyPem' => $profile->public_key, + ], + 'icon' => [ + 'type' => 'Image', + 'mediaType' => 'image/jpeg', + 'url' => $profile->avatarUrl(), + ], + 'endpoints' => [ + 'sharedInbox' => config('app.url').'/f/inbox', + ], + ]; - if($profile->aliases->count()) { - $res['alsoKnownAs'] = $profile->aliases->map(fn($alias) => $alias->uri); - } + if ($profile->status === 'delete' || $profile->deleted_at != null) { + $res['suspended'] = true; + $res['name'] = ''; + unset($res['icon']); + $res['summary'] = ''; + $res['indexable'] = false; + $res['manuallyApprovesFollowers'] = false; + } else { + if ($profile->aliases->count()) { + $res['alsoKnownAs'] = $profile->aliases->map(fn ($alias) => $alias->uri); + } - if($profile->moved_to_profile_id) { - $res['movedTo'] = AccountService::get($profile->moved_to_profile_id)['url']; - } + if ($profile->moved_to_profile_id) { + $movedTo = AccountService::get($profile->moved_to_profile_id); + if ($movedTo && isset($movedTo['url'], $movedTo['id'])) { + $res['movedTo'] = $movedTo['url']; + } + } + } - return $res; + return $res; } } diff --git a/app/Transformer/ActivityPub/StatusTransformer.php b/app/Transformer/ActivityPub/StatusTransformer.php index f5d5ea531..c7f61b88b 100644 --- a/app/Transformer/ActivityPub/StatusTransformer.php +++ b/app/Transformer/ActivityPub/StatusTransformer.php @@ -2,59 +2,62 @@ namespace App\Transformer\ActivityPub; -use App\Status; -use League\Fractal; use App\Services\MediaService; +use App\Status; +use App\Util\Lexer\Autolink; +use League\Fractal; class StatusTransformer extends Fractal\TransformerAbstract { public function transform(Status $status) { + $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : ""; + return [ - '@context' => [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - [ - 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', - 'featured' => [ - 'https://pixelfed.org/ns#featured' => ['@type' => '@id'], - ], + '@context' => [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + [ + 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', + 'featured' => [ + 'https://pixelfed.org/ns#featured' => ['@type' => '@id'], + ], + ], ], - ], - 'id' => $status->url(), + 'id' => $status->url(), - // TODO: handle other types - 'type' => 'Note', + // TODO: handle other types + 'type' => 'Note', - // XXX: CW Title - 'summary' => null, - 'content' => $status->rendered ?? $status->caption, - 'inReplyTo' => null, + // XXX: CW Title + 'summary' => null, + 'content' => $content, + 'inReplyTo' => null, - // TODO: fix date format - 'published' => $status->created_at->toAtomString(), - 'url' => $status->url(), - 'attributedTo' => $status->profile->permalink(), - 'to' => [ - // TODO: handle proper scope - 'https://www.w3.org/ns/activitystreams#Public', - ], - 'cc' => [ - // TODO: add cc's - $status->profile->permalink('/followers'), - ], - 'sensitive' => (bool) $status->is_nsfw, - 'atomUri' => $status->url(), - 'inReplyToAtomUri' => null, - 'attachment' => MediaService::activitypub($status->id), - 'tag' => [], - 'location' => $status->place_id ? [ - 'type' => 'Place', - 'name' => $status->place->name, - 'longitude' => $status->place->long, - 'latitude' => $status->place->lat, - 'country' => $status->place->country + // TODO: fix date format + 'published' => $status->created_at->toAtomString(), + 'url' => $status->url(), + 'attributedTo' => $status->profile->permalink(), + 'to' => [ + // TODO: handle proper scope + 'https://www.w3.org/ns/activitystreams#Public', + ], + 'cc' => [ + // TODO: add cc's + $status->profile->permalink('/followers'), + ], + 'sensitive' => (bool) $status->is_nsfw, + 'atomUri' => $status->url(), + 'inReplyToAtomUri' => null, + 'attachment' => MediaService::activitypub($status->id), + 'tag' => [], + 'location' => $status->place_id ? [ + 'type' => 'Place', + 'name' => $status->place->name, + 'longitude' => $status->place->long, + 'latitude' => $status->place->lat, + 'country' => $status->place->country, ] : null, - ]; + ]; } } diff --git a/app/Transformer/ActivityPub/Verb/CreateNote.php b/app/Transformer/ActivityPub/Verb/CreateNote.php index 55fdfa8f4..cf2f0fb51 100644 --- a/app/Transformer/ActivityPub/Verb/CreateNote.php +++ b/app/Transformer/ActivityPub/Verb/CreateNote.php @@ -2,140 +2,144 @@ namespace App\Transformer\ActivityPub\Verb; -use App\Status; -use League\Fractal; use App\Models\CustomEmoji; +use App\Status; +use App\Util\Lexer\Autolink; use Illuminate\Support\Str; +use League\Fractal; class CreateNote extends Fractal\TransformerAbstract { - public function transform(Status $status) - { - $mentions = $status->mentions->map(function ($mention) { - $webfinger = $mention->emailUrl(); - $name = Str::startsWith($webfinger, '@') ? - $webfinger : - '@' . $webfinger; - return [ - 'type' => 'Mention', - 'href' => $mention->permalink(), - 'name' => $name - ]; - })->toArray(); + public function transform(Status $status) + { + $mentions = $status->mentions->map(function ($mention) { + $webfinger = $mention->emailUrl(); + $name = Str::startsWith($webfinger, '@') ? + $webfinger : + '@'.$webfinger; - if($status->in_reply_to_id != null) { - $parent = $status->parent()->profile; - if($parent) { - $webfinger = $parent->emailUrl(); - $name = Str::startsWith($webfinger, '@') ? - $webfinger : - '@' . $webfinger; - $reply = [ - 'type' => 'Mention', - 'href' => $parent->permalink(), - 'name' => $name - ]; - $mentions = array_merge($reply, $mentions); - } - } + return [ + 'type' => 'Mention', + 'href' => $mention->permalink(), + 'name' => $name, + ]; + })->toArray(); - $hashtags = $status->hashtags->map(function ($hashtag) { - return [ - 'type' => 'Hashtag', - 'href' => $hashtag->url(), - 'name' => "#{$hashtag->name}", - ]; - })->toArray(); + if ($status->in_reply_to_id != null) { + $parent = $status->parent()->profile; + if ($parent) { + $webfinger = $parent->emailUrl(); + $name = Str::startsWith($webfinger, '@') ? + $webfinger : + '@'.$webfinger; + $reply = [ + 'type' => 'Mention', + 'href' => $parent->permalink(), + 'name' => $name, + ]; + $mentions = array_merge($reply, $mentions); + } + } - $emojis = CustomEmoji::scan($status->caption, true) ?? []; - $emoji = array_merge($emojis, $mentions); - $tags = array_merge($emoji, $hashtags); + $hashtags = $status->hashtags->map(function ($hashtag) { + return [ + 'type' => 'Hashtag', + 'href' => $hashtag->url(), + 'name' => "#{$hashtag->name}", + ]; + })->toArray(); - return [ - '@context' => [ - 'https://w3id.org/security/v1', - 'https://www.w3.org/ns/activitystreams', - [ - 'Hashtag' => 'as:Hashtag', - 'sensitive' => 'as:sensitive', - 'schema' => 'http://schema.org/', - 'pixelfed' => 'http://pixelfed.org/ns#', - 'commentsEnabled' => [ - '@id' => 'pixelfed:commentsEnabled', - '@type' => 'schema:Boolean' - ], - 'capabilities' => [ - '@id' => 'pixelfed:capabilities', - '@container' => '@set' - ], - 'announce' => [ - '@id' => 'pixelfed:canAnnounce', - '@type' => '@id' - ], - 'like' => [ - '@id' => 'pixelfed:canLike', - '@type' => '@id' - ], - 'reply' => [ - '@id' => 'pixelfed:canReply', - '@type' => '@id' - ], - 'toot' => 'http://joinmastodon.org/ns#', - 'Emoji' => 'toot:Emoji', - 'blurhash' => 'toot:blurhash', - ] - ], - 'id' => $status->permalink(), - 'type' => 'Create', - 'actor' => $status->profile->permalink(), - 'published' => $status->created_at->toAtomString(), - 'to' => $status->scopeToAudience('to'), - 'cc' => $status->scopeToAudience('cc'), - 'object' => [ - 'id' => $status->url(), - 'type' => 'Note', - 'summary' => $status->is_nsfw ? $status->cw_summary : null, - 'content' => $status->rendered ?? $status->caption, - 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, - 'published' => $status->created_at->toAtomString(), - 'url' => $status->url(), - 'attributedTo' => $status->profile->permalink(), - 'to' => $status->scopeToAudience('to'), - 'cc' => $status->scopeToAudience('cc'), - 'sensitive' => (bool) $status->is_nsfw, - 'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) { - $res = [ - 'type' => $media->activityVerb(), - 'mediaType' => $media->mime, - 'url' => $media->url(), - 'name' => $media->caption, - ]; - if($media->blurhash) { - $res['blurhash'] = $media->blurhash; - } - if($media->width) { - $res['width'] = $media->width; - } - if($media->height) { - $res['height'] = $media->height; - } - return $res; - })->toArray(), - 'tag' => $tags, - 'commentsEnabled' => (bool) !$status->comments_disabled, - 'capabilities' => [ - 'announce' => 'https://www.w3.org/ns/activitystreams#Public', - 'like' => 'https://www.w3.org/ns/activitystreams#Public', - 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public' - ], - 'location' => $status->place_id ? [ - 'type' => 'Place', - 'name' => $status->place->name, - 'longitude' => $status->place->long, - 'latitude' => $status->place->lat, - 'country' => $status->place->country - ] : null, - ] - ]; - } + $emojis = CustomEmoji::scan($status->caption, true) ?? []; + $emoji = array_merge($emojis, $mentions); + $tags = array_merge($emoji, $hashtags); + $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : ""; + + return [ + '@context' => [ + 'https://w3id.org/security/v1', + 'https://www.w3.org/ns/activitystreams', + [ + 'Hashtag' => 'as:Hashtag', + 'sensitive' => 'as:sensitive', + 'schema' => 'http://schema.org/', + 'pixelfed' => 'http://pixelfed.org/ns#', + 'commentsEnabled' => [ + '@id' => 'pixelfed:commentsEnabled', + '@type' => 'schema:Boolean', + ], + 'capabilities' => [ + '@id' => 'pixelfed:capabilities', + '@container' => '@set', + ], + 'announce' => [ + '@id' => 'pixelfed:canAnnounce', + '@type' => '@id', + ], + 'like' => [ + '@id' => 'pixelfed:canLike', + '@type' => '@id', + ], + 'reply' => [ + '@id' => 'pixelfed:canReply', + '@type' => '@id', + ], + 'toot' => 'http://joinmastodon.org/ns#', + 'Emoji' => 'toot:Emoji', + 'blurhash' => 'toot:blurhash', + ], + ], + 'id' => $status->permalink(), + 'type' => 'Create', + 'actor' => $status->profile->permalink(), + 'published' => $status->created_at->toAtomString(), + 'to' => $status->scopeToAudience('to'), + 'cc' => $status->scopeToAudience('cc'), + 'object' => [ + 'id' => $status->url(), + 'type' => 'Note', + 'summary' => $status->is_nsfw ? $status->cw_summary : null, + 'content' => $content, + 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, + 'published' => $status->created_at->toAtomString(), + 'url' => $status->url(), + 'attributedTo' => $status->profile->permalink(), + 'to' => $status->scopeToAudience('to'), + 'cc' => $status->scopeToAudience('cc'), + 'sensitive' => (bool) $status->is_nsfw, + 'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) { + $res = [ + 'type' => $media->activityVerb(), + 'mediaType' => $media->mime, + 'url' => $media->url(), + 'name' => $media->caption, + ]; + if ($media->blurhash) { + $res['blurhash'] = $media->blurhash; + } + if ($media->width) { + $res['width'] = $media->width; + } + if ($media->height) { + $res['height'] = $media->height; + } + + return $res; + })->toArray(), + 'tag' => $tags, + 'commentsEnabled' => (bool) ! $status->comments_disabled, + 'capabilities' => [ + 'announce' => 'https://www.w3.org/ns/activitystreams#Public', + 'like' => 'https://www.w3.org/ns/activitystreams#Public', + 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public', + ], + 'location' => $status->place_id ? [ + 'type' => 'Place', + 'name' => $status->place->name, + 'longitude' => $status->place->long, + 'latitude' => $status->place->lat, + 'country' => $status->place->country, + ] : null, + ], + ]; + } } diff --git a/app/Transformer/ActivityPub/Verb/DeleteActor.php b/app/Transformer/ActivityPub/Verb/DeleteActor.php new file mode 100644 index 000000000..5d3fdbc07 --- /dev/null +++ b/app/Transformer/ActivityPub/Verb/DeleteActor.php @@ -0,0 +1,24 @@ + 'https://www.w3.org/ns/activitystreams', + 'id' => $profile->permalink('#delete'), + 'type' => 'Delete', + 'actor' => $profile->permalink(), + 'to' => [ + 'https://www.w3.org/ns/activitystreams#Public' + ], + 'object' => $profile->permalink() + ]; + } + +} diff --git a/app/Transformer/ActivityPub/Verb/Move.php b/app/Transformer/ActivityPub/Verb/Move.php new file mode 100644 index 000000000..2460914be --- /dev/null +++ b/app/Transformer/ActivityPub/Verb/Move.php @@ -0,0 +1,26 @@ +target->permalink(); + $id = $migration->target->permalink('#moves/'.$migration->id); + $to = $migration->target->permalink('/followers'); + + return [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $id, + 'actor' => $objUrl, + 'type' => 'Move', + 'object' => $objUrl, + 'target' => $migration->profile->permalink(), + 'to' => $to, + ]; + } +} diff --git a/app/Transformer/ActivityPub/Verb/Note.php b/app/Transformer/ActivityPub/Verb/Note.php index 1350641d4..bc34761cd 100644 --- a/app/Transformer/ActivityPub/Verb/Note.php +++ b/app/Transformer/ActivityPub/Verb/Note.php @@ -2,133 +2,137 @@ namespace App\Transformer\ActivityPub\Verb; -use App\Status; -use League\Fractal; use App\Models\CustomEmoji; +use App\Status; +use App\Util\Lexer\Autolink; use Illuminate\Support\Str; +use League\Fractal; class Note extends Fractal\TransformerAbstract { - public function transform(Status $status) - { + public function transform(Status $status) + { - $mentions = $status->mentions->map(function ($mention) { - $webfinger = $mention->emailUrl(); - $name = Str::startsWith($webfinger, '@') ? - $webfinger : - '@' . $webfinger; - return [ - 'type' => 'Mention', - 'href' => $mention->permalink(), - 'name' => $name - ]; - })->toArray(); + $mentions = $status->mentions->map(function ($mention) { + $webfinger = $mention->emailUrl(); + $name = Str::startsWith($webfinger, '@') ? + $webfinger : + '@'.$webfinger; - if($status->in_reply_to_id != null) { - $parent = $status->parent()->profile; - if($parent) { - $webfinger = $parent->emailUrl(); - $name = Str::startsWith($webfinger, '@') ? - $webfinger : - '@' . $webfinger; - $reply = [ - 'type' => 'Mention', - 'href' => $parent->permalink(), - 'name' => $name - ]; - array_push($mentions, $reply); - } - } - - $hashtags = $status->hashtags->map(function ($hashtag) { - return [ - 'type' => 'Hashtag', - 'href' => $hashtag->url(), - 'name' => "#{$hashtag->name}", - ]; - })->toArray(); + return [ + 'type' => 'Mention', + 'href' => $mention->permalink(), + 'name' => $name, + ]; + })->toArray(); - $emojis = CustomEmoji::scan($status->caption, true) ?? []; - $emoji = array_merge($emojis, $mentions); - $tags = array_merge($emoji, $hashtags); + if ($status->in_reply_to_id != null) { + $parent = $status->parent()->profile; + if ($parent) { + $webfinger = $parent->emailUrl(); + $name = Str::startsWith($webfinger, '@') ? + $webfinger : + '@'.$webfinger; + $reply = [ + 'type' => 'Mention', + 'href' => $parent->permalink(), + 'name' => $name, + ]; + array_push($mentions, $reply); + } + } - return [ - '@context' => [ - 'https://w3id.org/security/v1', - 'https://www.w3.org/ns/activitystreams', - [ - 'Hashtag' => 'as:Hashtag', - 'sensitive' => 'as:sensitive', - 'schema' => 'http://schema.org/', - 'pixelfed' => 'http://pixelfed.org/ns#', - 'commentsEnabled' => [ - '@id' => 'pixelfed:commentsEnabled', - '@type' => 'schema:Boolean' - ], - 'capabilities' => [ - '@id' => 'pixelfed:capabilities', - '@container' => '@set' - ], - 'announce' => [ - '@id' => 'pixelfed:canAnnounce', - '@type' => '@id' - ], - 'like' => [ - '@id' => 'pixelfed:canLike', - '@type' => '@id' - ], - 'reply' => [ - '@id' => 'pixelfed:canReply', - '@type' => '@id' - ], - 'toot' => 'http://joinmastodon.org/ns#', - 'Emoji' => 'toot:Emoji', - 'blurhash' => 'toot:blurhash', - ] - ], - 'id' => $status->url(), - 'type' => 'Note', - 'summary' => $status->is_nsfw ? $status->cw_summary : null, - 'content' => $status->rendered ?? $status->caption, - 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, - 'published' => $status->created_at->toAtomString(), - 'url' => $status->url(), - 'attributedTo' => $status->profile->permalink(), - 'to' => $status->scopeToAudience('to'), - 'cc' => $status->scopeToAudience('cc'), - 'sensitive' => (bool) $status->is_nsfw, - 'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) { - $res = [ - 'type' => $media->activityVerb(), - 'mediaType' => $media->mime, - 'url' => $media->url(), - 'name' => $media->caption, - ]; - if($media->blurhash) { - $res['blurhash'] = $media->blurhash; - } - if($media->width) { - $res['width'] = $media->width; - } - if($media->height) { - $res['height'] = $media->height; - } - return $res; - })->toArray(), - 'tag' => $tags, - 'commentsEnabled' => (bool) !$status->comments_disabled, - 'capabilities' => [ - 'announce' => 'https://www.w3.org/ns/activitystreams#Public', - 'like' => 'https://www.w3.org/ns/activitystreams#Public', - 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public' - ], - 'location' => $status->place_id ? [ - 'type' => 'Place', - 'name' => $status->place->name, - 'longitude' => $status->place->long, - 'latitude' => $status->place->lat, - 'country' => $status->place->country - ] : null, - ]; - } + $hashtags = $status->hashtags->map(function ($hashtag) { + return [ + 'type' => 'Hashtag', + 'href' => $hashtag->url(), + 'name' => "#{$hashtag->name}", + ]; + })->toArray(); + + $emojis = CustomEmoji::scan($status->caption, true) ?? []; + $emoji = array_merge($emojis, $mentions); + $tags = array_merge($emoji, $hashtags); + $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : ""; + + return [ + '@context' => [ + 'https://w3id.org/security/v1', + 'https://www.w3.org/ns/activitystreams', + [ + 'Hashtag' => 'as:Hashtag', + 'sensitive' => 'as:sensitive', + 'schema' => 'http://schema.org/', + 'pixelfed' => 'http://pixelfed.org/ns#', + 'commentsEnabled' => [ + '@id' => 'pixelfed:commentsEnabled', + '@type' => 'schema:Boolean', + ], + 'capabilities' => [ + '@id' => 'pixelfed:capabilities', + '@container' => '@set', + ], + 'announce' => [ + '@id' => 'pixelfed:canAnnounce', + '@type' => '@id', + ], + 'like' => [ + '@id' => 'pixelfed:canLike', + '@type' => '@id', + ], + 'reply' => [ + '@id' => 'pixelfed:canReply', + '@type' => '@id', + ], + 'toot' => 'http://joinmastodon.org/ns#', + 'Emoji' => 'toot:Emoji', + 'blurhash' => 'toot:blurhash', + ], + ], + 'id' => $status->url(), + 'type' => 'Note', + 'summary' => $status->is_nsfw ? $status->cw_summary : null, + 'content' => $content, + 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, + 'published' => $status->created_at->toAtomString(), + 'url' => $status->url(), + 'attributedTo' => $status->profile->permalink(), + 'to' => $status->scopeToAudience('to'), + 'cc' => $status->scopeToAudience('cc'), + 'sensitive' => (bool) $status->is_nsfw, + 'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) { + $res = [ + 'type' => $media->activityVerb(), + 'mediaType' => $media->mime, + 'url' => $media->url(), + 'name' => $media->caption, + ]; + if ($media->blurhash) { + $res['blurhash'] = $media->blurhash; + } + if ($media->width) { + $res['width'] = $media->width; + } + if ($media->height) { + $res['height'] = $media->height; + } + + return $res; + })->toArray(), + 'tag' => $tags, + 'commentsEnabled' => (bool) ! $status->comments_disabled, + 'capabilities' => [ + 'announce' => 'https://www.w3.org/ns/activitystreams#Public', + 'like' => 'https://www.w3.org/ns/activitystreams#Public', + 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public', + ], + 'location' => $status->place_id ? [ + 'type' => 'Place', + 'name' => $status->place->name, + 'longitude' => $status->place->long, + 'latitude' => $status->place->lat, + 'country' => $status->place->country, + ] : null, + ]; + } } diff --git a/app/Transformer/ActivityPub/Verb/Question.php b/app/Transformer/ActivityPub/Verb/Question.php index fb9313fb1..3d53ebcb5 100644 --- a/app/Transformer/ActivityPub/Verb/Question.php +++ b/app/Transformer/ActivityPub/Verb/Question.php @@ -3,104 +3,107 @@ namespace App\Transformer\ActivityPub\Verb; use App\Status; -use League\Fractal; +use App\Util\Lexer\Autolink; use Illuminate\Support\Str; +use League\Fractal; class Question extends Fractal\TransformerAbstract { - public function transform(Status $status) - { - $mentions = $status->mentions->map(function ($mention) { - $webfinger = $mention->emailUrl(); - $name = Str::startsWith($webfinger, '@') ? - $webfinger : - '@' . $webfinger; - return [ - 'type' => 'Mention', - 'href' => $mention->permalink(), - 'name' => $name - ]; - })->toArray(); + public function transform(Status $status) + { + $mentions = $status->mentions->map(function ($mention) { + $webfinger = $mention->emailUrl(); + $name = Str::startsWith($webfinger, '@') ? + $webfinger : + '@'.$webfinger; - $hashtags = $status->hashtags->map(function ($hashtag) { - return [ - 'type' => 'Hashtag', - 'href' => $hashtag->url(), - 'name' => "#{$hashtag->name}", - ]; - })->toArray(); - $tags = array_merge($mentions, $hashtags); + return [ + 'type' => 'Mention', + 'href' => $mention->permalink(), + 'name' => $name, + ]; + })->toArray(); - return [ - '@context' => [ - 'https://w3id.org/security/v1', - 'https://www.w3.org/ns/activitystreams', - [ - 'Hashtag' => 'as:Hashtag', - 'sensitive' => 'as:sensitive', - 'schema' => 'http://schema.org/', - 'pixelfed' => 'http://pixelfed.org/ns#', - 'commentsEnabled' => [ - '@id' => 'pixelfed:commentsEnabled', - '@type' => 'schema:Boolean' - ], - 'capabilities' => [ - '@id' => 'pixelfed:capabilities', - '@container' => '@set' - ], - 'announce' => [ - '@id' => 'pixelfed:canAnnounce', - '@type' => '@id' - ], - 'like' => [ - '@id' => 'pixelfed:canLike', - '@type' => '@id' - ], - 'reply' => [ - '@id' => 'pixelfed:canReply', - '@type' => '@id' - ], - 'toot' => 'http://joinmastodon.org/ns#', - 'Emoji' => 'toot:Emoji' - ] - ], - 'id' => $status->url(), - 'type' => 'Question', - 'summary' => null, - 'content' => $status->rendered ?? $status->caption, - 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, - 'published' => $status->created_at->toAtomString(), - 'url' => $status->url(), - 'attributedTo' => $status->profile->permalink(), - 'to' => $status->scopeToAudience('to'), - 'cc' => $status->scopeToAudience('cc'), - 'sensitive' => (bool) $status->is_nsfw, - 'attachment' => [], - 'tag' => $tags, - 'commentsEnabled' => (bool) !$status->comments_disabled, - 'capabilities' => [ - 'announce' => 'https://www.w3.org/ns/activitystreams#Public', - 'like' => 'https://www.w3.org/ns/activitystreams#Public', - 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public' - ], - 'location' => $status->place_id ? [ - 'type' => 'Place', - 'name' => $status->place->name, - 'longitude' => $status->place->long, - 'latitude' => $status->place->lat, - 'country' => $status->place->country - ] : null, - 'endTime' => $status->poll->expires_at->toAtomString(), - 'oneOf' => collect($status->poll->poll_options)->map(function($option, $index) use($status) { - return [ - 'type' => 'Note', - 'name' => $option, - 'replies' => [ - 'type' => 'Collection', - 'totalItems' => $status->poll->cached_tallies[$index] - ] - ]; - }) - ]; - } + $hashtags = $status->hashtags->map(function ($hashtag) { + return [ + 'type' => 'Hashtag', + 'href' => $hashtag->url(), + 'name' => "#{$hashtag->name}", + ]; + })->toArray(); + $tags = array_merge($mentions, $hashtags); + $content = $status->caption ? Autolink::create()->autolink($status->caption) : null; + + return [ + '@context' => [ + 'https://w3id.org/security/v1', + 'https://www.w3.org/ns/activitystreams', + [ + 'Hashtag' => 'as:Hashtag', + 'sensitive' => 'as:sensitive', + 'schema' => 'http://schema.org/', + 'pixelfed' => 'http://pixelfed.org/ns#', + 'commentsEnabled' => [ + '@id' => 'pixelfed:commentsEnabled', + '@type' => 'schema:Boolean', + ], + 'capabilities' => [ + '@id' => 'pixelfed:capabilities', + '@container' => '@set', + ], + 'announce' => [ + '@id' => 'pixelfed:canAnnounce', + '@type' => '@id', + ], + 'like' => [ + '@id' => 'pixelfed:canLike', + '@type' => '@id', + ], + 'reply' => [ + '@id' => 'pixelfed:canReply', + '@type' => '@id', + ], + 'toot' => 'http://joinmastodon.org/ns#', + 'Emoji' => 'toot:Emoji', + ], + ], + 'id' => $status->url(), + 'type' => 'Question', + 'summary' => null, + 'content' => $content, + 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, + 'published' => $status->created_at->toAtomString(), + 'url' => $status->url(), + 'attributedTo' => $status->profile->permalink(), + 'to' => $status->scopeToAudience('to'), + 'cc' => $status->scopeToAudience('cc'), + 'sensitive' => (bool) $status->is_nsfw, + 'attachment' => [], + 'tag' => $tags, + 'commentsEnabled' => (bool) ! $status->comments_disabled, + 'capabilities' => [ + 'announce' => 'https://www.w3.org/ns/activitystreams#Public', + 'like' => 'https://www.w3.org/ns/activitystreams#Public', + 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public', + ], + 'location' => $status->place_id ? [ + 'type' => 'Place', + 'name' => $status->place->name, + 'longitude' => $status->place->long, + 'latitude' => $status->place->lat, + 'country' => $status->place->country, + ] : null, + 'endTime' => $status->poll->expires_at->toAtomString(), + 'oneOf' => collect($status->poll->poll_options)->map(function ($option, $index) use ($status) { + return [ + 'type' => 'Note', + 'name' => $option, + 'replies' => [ + 'type' => 'Collection', + 'totalItems' => $status->poll->cached_tallies[$index], + ], + ]; + }), + ]; + } } diff --git a/app/Transformer/ActivityPub/Verb/UpdateNote.php b/app/Transformer/ActivityPub/Verb/UpdateNote.php index bdbb20c45..4199f7230 100644 --- a/app/Transformer/ActivityPub/Verb/UpdateNote.php +++ b/app/Transformer/ActivityPub/Verb/UpdateNote.php @@ -2,132 +2,135 @@ namespace App\Transformer\ActivityPub\Verb; -use App\Status; -use League\Fractal; use App\Models\CustomEmoji; +use App\Status; +use App\Util\Lexer\Autolink; use Illuminate\Support\Str; +use League\Fractal; class UpdateNote extends Fractal\TransformerAbstract { - public function transform(Status $status) - { - $mentions = $status->mentions->map(function ($mention) { - $webfinger = $mention->emailUrl(); - $name = Str::startsWith($webfinger, '@') ? - $webfinger : - '@' . $webfinger; - return [ - 'type' => 'Mention', - 'href' => $mention->permalink(), - 'name' => $name - ]; - })->toArray(); + public function transform(Status $status) + { + $mentions = $status->mentions->map(function ($mention) { + $webfinger = $mention->emailUrl(); + $name = Str::startsWith($webfinger, '@') ? + $webfinger : + '@'.$webfinger; - if($status->in_reply_to_id != null) { - $parent = $status->parent()->profile; - if($parent) { - $webfinger = $parent->emailUrl(); - $name = Str::startsWith($webfinger, '@') ? - $webfinger : - '@' . $webfinger; - $reply = [ - 'type' => 'Mention', - 'href' => $parent->permalink(), - 'name' => $name - ]; - $mentions = array_merge($reply, $mentions); - } - } + return [ + 'type' => 'Mention', + 'href' => $mention->permalink(), + 'name' => $name, + ]; + })->toArray(); - $hashtags = $status->hashtags->map(function ($hashtag) { - return [ - 'type' => 'Hashtag', - 'href' => $hashtag->url(), - 'name' => "#{$hashtag->name}", - ]; - })->toArray(); + if ($status->in_reply_to_id != null) { + $parent = $status->parent()->profile; + if ($parent) { + $webfinger = $parent->emailUrl(); + $name = Str::startsWith($webfinger, '@') ? + $webfinger : + '@'.$webfinger; + $reply = [ + 'type' => 'Mention', + 'href' => $parent->permalink(), + 'name' => $name, + ]; + $mentions = array_merge($reply, $mentions); + } + } - $emojis = CustomEmoji::scan($status->caption, true) ?? []; - $emoji = array_merge($emojis, $mentions); - $tags = array_merge($emoji, $hashtags); + $hashtags = $status->hashtags->map(function ($hashtag) { + return [ + 'type' => 'Hashtag', + 'href' => $hashtag->url(), + 'name' => "#{$hashtag->name}", + ]; + })->toArray(); - $latestEdit = $status->edits()->latest()->first(); + $emojis = CustomEmoji::scan($status->caption, true) ?? []; + $emoji = array_merge($emojis, $mentions); + $tags = array_merge($emoji, $hashtags); - return [ - '@context' => [ - 'https://w3id.org/security/v1', - 'https://www.w3.org/ns/activitystreams', - [ - 'Hashtag' => 'as:Hashtag', - 'sensitive' => 'as:sensitive', - 'schema' => 'http://schema.org/', - 'pixelfed' => 'http://pixelfed.org/ns#', - 'commentsEnabled' => [ - '@id' => 'pixelfed:commentsEnabled', - '@type' => 'schema:Boolean' - ], - 'capabilities' => [ - '@id' => 'pixelfed:capabilities', - '@container' => '@set' - ], - 'announce' => [ - '@id' => 'pixelfed:canAnnounce', - '@type' => '@id' - ], - 'like' => [ - '@id' => 'pixelfed:canLike', - '@type' => '@id' - ], - 'reply' => [ - '@id' => 'pixelfed:canReply', - '@type' => '@id' - ], - 'toot' => 'http://joinmastodon.org/ns#', - 'Emoji' => 'toot:Emoji' - ] - ], - 'id' => $status->permalink('#updates/' . $latestEdit->id), - 'type' => 'Update', - 'actor' => $status->profile->permalink(), - 'published' => $latestEdit->created_at->toAtomString(), - 'to' => $status->scopeToAudience('to'), - 'cc' => $status->scopeToAudience('cc'), - 'object' => [ - 'id' => $status->url(), - 'type' => 'Note', - 'summary' => $status->is_nsfw ? $status->cw_summary : null, - 'content' => $status->rendered ?? $status->caption, - 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, - 'published' => $status->created_at->toAtomString(), - 'url' => $status->url(), - 'attributedTo' => $status->profile->permalink(), - 'to' => $status->scopeToAudience('to'), - 'cc' => $status->scopeToAudience('cc'), - 'sensitive' => (bool) $status->is_nsfw, - 'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) { - return [ - 'type' => $media->activityVerb(), - 'mediaType' => $media->mime, - 'url' => $media->url(), - 'name' => $media->caption, - ]; - })->toArray(), - 'tag' => $tags, - 'commentsEnabled' => (bool) !$status->comments_disabled, - 'updated' => $latestEdit->created_at->toAtomString(), - 'capabilities' => [ - 'announce' => 'https://www.w3.org/ns/activitystreams#Public', - 'like' => 'https://www.w3.org/ns/activitystreams#Public', - 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public' - ], - 'location' => $status->place_id ? [ - 'type' => 'Place', - 'name' => $status->place->name, - 'longitude' => $status->place->long, - 'latitude' => $status->place->lat, - 'country' => $status->place->country - ] : null, - ] - ]; - } + $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : ""; + $latestEdit = $status->edits()->latest()->first(); + + return [ + '@context' => [ + 'https://w3id.org/security/v1', + 'https://www.w3.org/ns/activitystreams', + [ + 'Hashtag' => 'as:Hashtag', + 'sensitive' => 'as:sensitive', + 'schema' => 'http://schema.org/', + 'pixelfed' => 'http://pixelfed.org/ns#', + 'commentsEnabled' => [ + '@id' => 'pixelfed:commentsEnabled', + '@type' => 'schema:Boolean', + ], + 'capabilities' => [ + '@id' => 'pixelfed:capabilities', + '@container' => '@set', + ], + 'announce' => [ + '@id' => 'pixelfed:canAnnounce', + '@type' => '@id', + ], + 'like' => [ + '@id' => 'pixelfed:canLike', + '@type' => '@id', + ], + 'reply' => [ + '@id' => 'pixelfed:canReply', + '@type' => '@id', + ], + 'toot' => 'http://joinmastodon.org/ns#', + 'Emoji' => 'toot:Emoji', + ], + ], + 'id' => $status->permalink('#updates/'.$latestEdit->id), + 'type' => 'Update', + 'actor' => $status->profile->permalink(), + 'published' => $latestEdit->created_at->toAtomString(), + 'to' => $status->scopeToAudience('to'), + 'cc' => $status->scopeToAudience('cc'), + 'object' => [ + 'id' => $status->url(), + 'type' => 'Note', + 'summary' => $status->is_nsfw ? $status->cw_summary : null, + 'content' => $content, + 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, + 'published' => $status->created_at->toAtomString(), + 'url' => $status->url(), + 'attributedTo' => $status->profile->permalink(), + 'to' => $status->scopeToAudience('to'), + 'cc' => $status->scopeToAudience('cc'), + 'sensitive' => (bool) $status->is_nsfw, + 'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) { + return [ + 'type' => $media->activityVerb(), + 'mediaType' => $media->mime, + 'url' => $media->url(), + 'name' => $media->caption, + ]; + })->toArray(), + 'tag' => $tags, + 'commentsEnabled' => (bool) ! $status->comments_disabled, + 'updated' => $latestEdit->created_at->toAtomString(), + 'capabilities' => [ + 'announce' => 'https://www.w3.org/ns/activitystreams#Public', + 'like' => 'https://www.w3.org/ns/activitystreams#Public', + 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public', + ], + 'location' => $status->place_id ? [ + 'type' => 'Place', + 'name' => $status->place->name, + 'longitude' => $status->place->long, + 'latitude' => $status->place->lat, + 'country' => $status->place->country, + ] : null, + ], + ]; + } } diff --git a/app/Transformer/Api/AccountTransformer.php b/app/Transformer/Api/AccountTransformer.php index 6c6fa17e4..9411d5a18 100644 --- a/app/Transformer/Api/AccountTransformer.php +++ b/app/Transformer/Api/AccountTransformer.php @@ -2,12 +2,13 @@ namespace App\Transformer\Api; -use Auth; -use Cache; use App\Profile; -use App\User; -use League\Fractal; +use App\Services\AccountService; use App\Services\PronounService; +use App\User; +use App\UserSetting; +use Cache; +use League\Fractal; class AccountTransformer extends Fractal\TransformerAbstract { @@ -15,47 +16,77 @@ class AccountTransformer extends Fractal\TransformerAbstract // 'relationship', ]; - public function transform(Profile $profile) - { - if(!$profile) { - return []; - } + public function transform(Profile $profile) + { + if (! $profile) { + return []; + } - $adminIds = Cache::remember('pf:admin-ids', 604800, function() { - return User::whereIsAdmin(true)->pluck('profile_id')->toArray(); - }); + $adminIds = Cache::remember('pf:admin-ids', 604800, function () { + return User::whereIsAdmin(true)->pluck('profile_id')->toArray(); + }); - $local = $profile->private_key != null; - $is_admin = !$local ? false : in_array($profile->id, $adminIds); - $acct = $local ? $profile->username : substr($profile->username, 1); - $username = $local ? $profile->username : explode('@', $acct)[0]; - return [ - 'id' => (string) $profile->id, - 'username' => $username, - 'acct' => $acct, - 'display_name' => $profile->name, - 'discoverable' => true, - 'locked' => (bool) $profile->is_private, - 'followers_count' => (int) $profile->followers_count, - 'following_count' => (int) $profile->following_count, - 'statuses_count' => (int) $profile->status_count, - 'note' => $profile->bio ?? '', - 'note_text' => $profile->bio ? strip_tags($profile->bio) : null, - 'url' => $profile->url(), - 'avatar' => $profile->avatarUrl(), - 'website' => $profile->website, - 'local' => (bool) $local, - 'is_admin' => (bool) $is_admin, - 'created_at' => $profile->created_at->toJSON(), - 'header_bg' => $profile->header_bg, - 'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(), - 'pronouns' => PronounService::get($profile->id), - 'location' => $profile->location - ]; - } + $local = $profile->private_key != null; + $local = $profile->user_id && $profile->private_key != null; + $hideFollowing = false; + $hideFollowers = false; + if ($local) { + $hideFollowing = Cache::remember('pf:acct-trans:hideFollowing:'.$profile->id, 2592000, function () use ($profile) { + $settings = UserSetting::whereUserId($profile->user_id)->first(); + if (! $settings) { + return false; + } - protected function includeRelationship(Profile $profile) - { - return $this->item($profile, new RelationshipTransformer()); - } + return $settings->show_profile_following_count == false; + }); + $hideFollowers = Cache::remember('pf:acct-trans:hideFollowers:'.$profile->id, 2592000, function () use ($profile) { + $settings = UserSetting::whereUserId($profile->user_id)->first(); + if (! $settings) { + return false; + } + + return $settings->show_profile_follower_count == false; + }); + } + $is_admin = ! $local ? false : in_array($profile->id, $adminIds); + $acct = $local ? $profile->username : substr($profile->username, 1); + $username = $local ? $profile->username : explode('@', $acct)[0]; + $res = [ + 'id' => (string) $profile->id, + 'username' => $username, + 'acct' => $acct, + 'display_name' => $profile->name, + 'discoverable' => true, + 'locked' => (bool) $profile->is_private, + 'followers_count' => $hideFollowers ? 0 : (int) $profile->followers_count, + 'following_count' => $hideFollowing ? 0 : (int) $profile->following_count, + 'statuses_count' => (int) $profile->status_count, + 'note' => $profile->bio ?? '', + 'note_text' => $profile->bio ? strip_tags($profile->bio) : null, + 'url' => $profile->url(), + 'avatar' => $profile->avatarUrl(), + 'website' => $profile->website, + 'local' => (bool) $local, + 'is_admin' => (bool) $is_admin, + 'created_at' => $profile->created_at->toJSON(), + 'header_bg' => $profile->header_bg, + 'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(), + 'pronouns' => PronounService::get($profile->id), + 'location' => $profile->location, + ]; + + if ($profile->moved_to_profile_id) { + $mt = AccountService::getMastodon($profile->moved_to_profile_id, true); + if ($mt) { + $res['moved'] = $mt; + } + } + + return $res; + } + + protected function includeRelationship(Profile $profile) + { + return $this->item($profile, new RelationshipTransformer()); + } } diff --git a/app/Transformer/Api/GroupPostTransformer.php b/app/Transformer/Api/GroupPostTransformer.php new file mode 100644 index 000000000..0999b3fa4 --- /dev/null +++ b/app/Transformer/Api/GroupPostTransformer.php @@ -0,0 +1,59 @@ + (string) $status->id, + 'gid' => $status->group_id ? (string) $status->group_id : null, + 'url' => '/groups/' . $status->group_id . '/p/' . $status->id, + 'content' => $status->caption, + 'content_text' => $status->caption, + 'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)), + 'reblogs_count' => $status->reblogs_count ?? 0, + 'favourites_count' => $status->likes_count ?? 0, + 'reblogged' => null, + 'favourited' => null, + 'muted' => null, + 'sensitive' => (bool) $status->is_nsfw, + 'spoiler_text' => $status->cw_summary ?? '', + 'visibility' => $status->visibility, + 'application' => [ + 'name' => 'web', + 'website' => null + ], + 'language' => null, + 'pf_type' => $status->type, + 'reply_count' => (int) $status->reply_count ?? 0, + 'comments_disabled' => (bool) $status->comments_disabled, + 'thread' => false, + 'media_attachments' => GroupMediaService::get($status->id), + 'replies' => [], + 'parent' => [], + 'place' => null, + 'local' => (bool) !$status->remote_url, + 'account' => AccountService::get($status->profile_id, true), + 'poll' => [], + ]; + } +} diff --git a/app/Transformer/Api/Mastodon/v1/StatusTransformer.php b/app/Transformer/Api/Mastodon/v1/StatusTransformer.php index bfbc3d58b..16ff4cc30 100644 --- a/app/Transformer/Api/Mastodon/v1/StatusTransformer.php +++ b/app/Transformer/Api/Mastodon/v1/StatusTransformer.php @@ -2,48 +2,50 @@ namespace App\Transformer\Api\Mastodon\v1; -use App\Status; -use League\Fractal; -use Cache; use App\Services\MediaService; use App\Services\ProfileService; use App\Services\StatusHashtagService; +use App\Status; +use App\Util\Lexer\Autolink; +use League\Fractal; class StatusTransformer extends Fractal\TransformerAbstract { - public function transform(Status $status) - { - return [ - 'id' => (string) $status->id, - 'created_at' => $status->created_at->toJSON(), - 'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null, - 'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null, - 'sensitive' => (bool) $status->is_nsfw, - 'spoiler_text' => $status->cw_summary ?? '', - 'visibility' => $status->visibility ?? $status->scope, - 'language' => 'en', - 'uri' => $status->permalink(''), - 'url' => $status->url(), - 'replies_count' => $status->reply_count ?? 0, - 'reblogs_count' => $status->reblogs_count ?? 0, - 'favourites_count' => $status->likes_count ?? 0, - 'reblogged' => $status->shared(), - 'favourited' => $status->liked(), - 'muted' => false, - 'bookmarked' => false, - 'content' => $status->rendered ?? $status->caption ?? '', - 'reblog' => null, - 'application' => [ - 'name' => 'web', - 'website' => null - ], - 'mentions' => [], - 'emojis' => [], - 'card' => null, - 'poll' => null, - 'media_attachments' => MediaService::get($status->id), - 'account' => ProfileService::get($status->profile_id, true), - 'tags' => StatusHashtagService::statusTags($status->id), - ]; - } + public function transform(Status $status) + { + $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : ""; + + return [ + 'id' => (string) $status->id, + 'created_at' => $status->created_at->toJSON(), + 'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null, + 'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null, + 'sensitive' => (bool) $status->is_nsfw, + 'spoiler_text' => $status->cw_summary ?? '', + 'visibility' => $status->visibility ?? $status->scope, + 'language' => 'en', + 'uri' => $status->permalink(''), + 'url' => $status->url(), + 'replies_count' => $status->reply_count ?? 0, + 'reblogs_count' => $status->reblogs_count ?? 0, + 'favourites_count' => $status->likes_count ?? 0, + 'reblogged' => $status->shared(), + 'favourited' => $status->liked(), + 'muted' => false, + 'bookmarked' => false, + 'content' => $content, + 'reblog' => null, + 'application' => [ + 'name' => 'web', + 'website' => null, + ], + 'mentions' => [], + 'emojis' => [], + 'card' => null, + 'poll' => null, + 'media_attachments' => MediaService::get($status->id), + 'account' => ProfileService::get($status->profile_id, true), + 'tags' => StatusHashtagService::statusTags($status->id), + ]; + } } diff --git a/app/Transformer/Api/NotificationTransformer.php b/app/Transformer/Api/NotificationTransformer.php index 837c027af..cad5732b5 100644 --- a/app/Transformer/Api/NotificationTransformer.php +++ b/app/Transformer/Api/NotificationTransformer.php @@ -4,78 +4,81 @@ namespace App\Transformer\Api; use App\Notification; use App\Services\AccountService; -use App\Services\HashidService; use App\Services\RelationshipService; use App\Services\StatusService; use League\Fractal; class NotificationTransformer extends Fractal\TransformerAbstract { - public function transform(Notification $notification) - { - $res = [ - 'id' => (string) $notification->id, - 'type' => $this->replaceTypeVerb($notification->action), - 'created_at' => (string) str_replace('+00:00', 'Z', $notification->created_at->format(DATE_RFC3339_EXTENDED)), - ]; + public function transform(Notification $notification) + { + $res = [ + 'id' => (string) $notification->id, + 'type' => $this->replaceTypeVerb($notification->action), + 'created_at' => (string) str_replace('+00:00', 'Z', $notification->created_at->format(DATE_RFC3339_EXTENDED)), + ]; - $n = $notification; + $n = $notification; - if($n->actor_id) { - $res['account'] = AccountService::get($n->actor_id); - if($n->profile_id != $n->actor_id) { - $res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id); - } - } + if ($n->actor_id) { + $res['account'] = AccountService::get($n->actor_id); + if ($n->profile_id != $n->actor_id) { + $res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id); + } + } - if($n->item_id && $n->item_type == 'App\Status') { - $res['status'] = StatusService::get($n->item_id, false); - } + if ($n->item_id && $n->item_type == 'App\Status') { + $res['status'] = StatusService::get($n->item_id, false); + } - if($n->item_id && $n->item_type == 'App\ModLog') { - $ml = $n->item; - if($ml && $ml->object_uid) { - $res['modlog'] = [ - 'id' => $ml->object_uid, - 'url' => url('/i/admin/users/modlogs/' . $ml->object_uid) - ]; - } - } + if ($n->item_id && $n->item_type == 'App\ModLog') { + $ml = $n->item; + if ($ml && $ml->object_uid) { + $res['modlog'] = [ + 'id' => $ml->object_uid, + 'url' => url('/i/admin/users/modlogs/'.$ml->object_uid), + ]; + } + } - if($n->item_id && $n->item_type == 'App\MediaTag') { - $ml = $n->item; - if($ml && $ml->tagged_username) { - $res['tagged'] = [ - 'username' => $ml->tagged_username, - 'post_url' => '/p/'.HashidService::encode($ml->status_id) - ]; - } - } + if ($n->item_id && $n->item_type == 'App\MediaTag') { + $ml = $n->item; + if ($ml && $ml->tagged_username) { + $np = StatusService::get($ml->status_id, false); + if ($np && isset($np['id'])) { + $res['tagged'] = [ + 'username' => $ml->tagged_username, + 'post_url' => $np['url'], + 'status_id' => $ml->status_id, + 'profile_id' => $ml->profile_id, + ]; + } + } + } - return $res; - } + return $res; + } - public function replaceTypeVerb($verb) - { - $verbs = [ - 'dm' => 'direct', - 'follow' => 'follow', - 'mention' => 'mention', - 'reblog' => 'share', - 'share' => 'share', - 'like' => 'favourite', - 'group:like' => 'favourite', - 'comment' => 'comment', - 'admin.user.modlog.comment' => 'modlog', - 'tagged' => 'tagged', - 'story:react' => 'story:react', - 'story:comment' => 'story:comment', - ]; + public function replaceTypeVerb($verb) + { + $verbs = [ + 'dm' => 'direct', + 'follow' => 'follow', + 'mention' => 'mention', + 'reblog' => 'share', + 'share' => 'share', + 'like' => 'favourite', + 'comment' => 'comment', + 'admin.user.modlog.comment' => 'modlog', + 'tagged' => 'tagged', + 'story:react' => 'story:react', + 'story:comment' => 'story:comment', + ]; - if(!isset($verbs[$verb])) { - return $verb; - } + if (! isset($verbs[$verb])) { + return $verb; + } - return $verbs[$verb]; - } + return $verbs[$verb]; + } } diff --git a/app/Transformer/Api/StatusStatelessTransformer.php b/app/Transformer/Api/StatusStatelessTransformer.php index 3c2c02d60..9f52ab50a 100644 --- a/app/Transformer/Api/StatusStatelessTransformer.php +++ b/app/Transformer/Api/StatusStatelessTransformer.php @@ -2,76 +2,73 @@ namespace App\Transformer\Api; -use App\Status; -use League\Fractal; -use Cache; +use App\Models\CustomEmoji; use App\Services\AccountService; use App\Services\HashidService; use App\Services\LikeService; use App\Services\MediaService; use App\Services\MediaTagService; -use App\Services\StatusService; +use App\Services\PollService; use App\Services\StatusHashtagService; use App\Services\StatusLabelService; use App\Services\StatusMentionService; -use App\Services\PollService; -use App\Models\CustomEmoji; +use App\Services\StatusService; +use App\Status; use App\Util\Lexer\Autolink; +use League\Fractal; class StatusStatelessTransformer extends Fractal\TransformerAbstract { - public function transform(Status $status) - { - $taggedPeople = MediaTagService::get($status->id); - $poll = $status->type === 'poll' ? PollService::get($status->id) : null; - $rendered = config('exp.autolink') ? - ( $status->caption ? Autolink::create()->autolink($status->caption) : '' ) : - ( $status->rendered ?? $status->caption ); + public function transform(Status $status) + { + $taggedPeople = MediaTagService::get($status->id); + $poll = $status->type === 'poll' ? PollService::get($status->id) : null; + $rendered = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : ""; - return [ - '_v' => 1, - 'id' => (string) $status->id, - //'gid' => $status->group_id ? (string) $status->group_id : null, - 'shortcode' => HashidService::encode($status->id), - 'uri' => $status->url(), - 'url' => $status->url(), - 'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null, - 'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null, - 'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id, false) : null, - 'content' => $rendered, - 'content_text' => $status->caption, - 'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)), - 'emojis' => CustomEmoji::scan($status->caption), - 'reblogs_count' => $status->reblogs_count ?? 0, - 'favourites_count' => $status->likes_count ?? 0, - 'reblogged' => null, - 'favourited' => null, - 'muted' => null, - 'sensitive' => (bool) $status->is_nsfw, - 'spoiler_text' => $status->cw_summary ?? '', - 'visibility' => $status->scope ?? $status->visibility, - 'application' => [ - 'name' => 'web', - 'website' => null - ], - 'language' => null, - 'mentions' => StatusMentionService::get($status->id), - 'pf_type' => $status->type ?? $status->setType(), - 'reply_count' => (int) $status->reply_count, - 'comments_disabled' => (bool) $status->comments_disabled, - 'thread' => false, - 'replies' => [], - 'parent' => [], - 'place' => $status->place, - 'local' => (bool) $status->local, - 'taggedPeople' => $taggedPeople, - 'label' => StatusLabelService::get($status), - 'liked_by' => LikeService::likedBy($status), - 'media_attachments' => MediaService::get($status->id), - 'account' => AccountService::get($status->profile_id, true), - 'tags' => StatusHashtagService::statusTags($status->id), - 'poll' => $poll, - 'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null, - ]; - } + return [ + '_v' => 1, + 'id' => (string) $status->id, + //'gid' => $status->group_id ? (string) $status->group_id : null, + 'shortcode' => HashidService::encode($status->id), + 'uri' => $status->url(), + 'url' => $status->url(), + 'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null, + 'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null, + 'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id, false) : null, + 'content' => $rendered, + 'content_text' => $status->caption, + 'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)), + 'emojis' => CustomEmoji::scan($status->caption), + 'reblogs_count' => $status->reblogs_count ?? 0, + 'favourites_count' => $status->likes_count ?? 0, + 'reblogged' => null, + 'favourited' => null, + 'muted' => null, + 'sensitive' => (bool) $status->is_nsfw, + 'spoiler_text' => $status->cw_summary ?? '', + 'visibility' => $status->scope ?? $status->visibility, + 'application' => [ + 'name' => 'web', + 'website' => null, + ], + 'language' => null, + 'mentions' => StatusMentionService::get($status->id), + 'pf_type' => $status->type ?? $status->setType(), + 'reply_count' => (int) $status->reply_count, + 'comments_disabled' => (bool) $status->comments_disabled, + 'thread' => false, + 'replies' => [], + 'parent' => [], + 'place' => $status->place, + 'local' => (bool) $status->local, + 'taggedPeople' => $taggedPeople, + 'label' => StatusLabelService::get($status), + 'liked_by' => LikeService::likedBy($status), + 'media_attachments' => MediaService::get($status->id), + 'account' => AccountService::get($status->profile_id, true), + 'tags' => StatusHashtagService::statusTags($status->id), + 'poll' => $poll, + 'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null, + ]; + } } diff --git a/app/Transformer/Api/StatusTransformer.php b/app/Transformer/Api/StatusTransformer.php index 22a840ce0..4dc08b618 100644 --- a/app/Transformer/Api/StatusTransformer.php +++ b/app/Transformer/Api/StatusTransformer.php @@ -2,80 +2,75 @@ namespace App\Transformer\Api; -use App\Like; -use App\Status; -use League\Fractal; -use Cache; +use App\Models\CustomEmoji; +use App\Services\BookmarkService; use App\Services\HashidService; use App\Services\LikeService; use App\Services\MediaService; use App\Services\MediaTagService; -use App\Services\StatusService; +use App\Services\PollService; +use App\Services\ProfileService; use App\Services\StatusHashtagService; use App\Services\StatusLabelService; use App\Services\StatusMentionService; -use App\Services\ProfileService; -use Illuminate\Support\Str; -use App\Services\PollService; -use App\Models\CustomEmoji; -use App\Services\BookmarkService; +use App\Services\StatusService; +use App\Status; use App\Util\Lexer\Autolink; +use League\Fractal; class StatusTransformer extends Fractal\TransformerAbstract { - public function transform(Status $status) - { - $pid = request()->user()->profile_id; - $taggedPeople = MediaTagService::get($status->id); - $poll = $status->type === 'poll' ? PollService::get($status->id, $pid) : null; - $rendered = config('exp.autolink') ? - ( $status->caption ? Autolink::create()->autolink($status->caption) : '' ) : - ( $status->rendered ?? $status->caption ); + public function transform(Status $status) + { + $pid = request()->user()->profile_id; + $taggedPeople = MediaTagService::get($status->id); + $poll = $status->type === 'poll' ? PollService::get($status->id, $pid) : null; + $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : ""; - return [ - '_v' => 1, - 'id' => (string) $status->id, - 'shortcode' => HashidService::encode($status->id), - 'uri' => $status->url(), - 'url' => $status->url(), - 'in_reply_to_id' => (string) $status->in_reply_to_id, - 'in_reply_to_account_id' => (string) $status->in_reply_to_profile_id, - 'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id) : null, - 'content' => $rendered, - 'content_text' => $status->caption, - 'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)), - 'emojis' => CustomEmoji::scan($status->caption), - 'reblogs_count' => 0, - 'favourites_count' => $status->likes_count ?? 0, - 'reblogged' => $status->shared(), - 'favourited' => $status->liked(), - 'muted' => null, - 'sensitive' => (bool) $status->is_nsfw, - 'spoiler_text' => $status->cw_summary ?? '', - 'visibility' => $status->scope ?? $status->visibility, - 'application' => [ - 'name' => 'web', - 'website' => null - ], - 'language' => null, - 'mentions' => StatusMentionService::get($status->id), - 'pf_type' => $status->type ?? $status->setType(), - 'reply_count' => (int) $status->reply_count, - 'comments_disabled' => (bool) $status->comments_disabled, - 'thread' => false, - 'replies' => [], - 'parent' => [], - 'place' => $status->place, - 'local' => (bool) $status->local, - 'taggedPeople' => $taggedPeople, - 'label' => StatusLabelService::get($status), - 'liked_by' => LikeService::likedBy($status), - 'media_attachments' => MediaService::get($status->id), - 'account' => ProfileService::get($status->profile_id, true), - 'tags' => StatusHashtagService::statusTags($status->id), - 'poll' => $poll, - 'bookmarked' => BookmarkService::get($pid, $status->id), - 'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null, - ]; - } + return [ + '_v' => 1, + 'id' => (string) $status->id, + 'shortcode' => HashidService::encode($status->id), + 'uri' => $status->url(), + 'url' => $status->url(), + 'in_reply_to_id' => (string) $status->in_reply_to_id, + 'in_reply_to_account_id' => (string) $status->in_reply_to_profile_id, + 'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id) : null, + 'content' => $content, + 'content_text' => $status->caption, + 'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)), + 'emojis' => CustomEmoji::scan($status->caption), + 'reblogs_count' => 0, + 'favourites_count' => $status->likes_count ?? 0, + 'reblogged' => $status->shared(), + 'favourited' => $status->liked(), + 'muted' => null, + 'sensitive' => (bool) $status->is_nsfw, + 'spoiler_text' => $status->cw_summary ?? '', + 'visibility' => $status->scope ?? $status->visibility, + 'application' => [ + 'name' => 'web', + 'website' => null, + ], + 'language' => null, + 'mentions' => StatusMentionService::get($status->id), + 'pf_type' => $status->type ?? $status->setType(), + 'reply_count' => (int) $status->reply_count, + 'comments_disabled' => (bool) $status->comments_disabled, + 'thread' => false, + 'replies' => [], + 'parent' => [], + 'place' => $status->place, + 'local' => (bool) $status->local, + 'taggedPeople' => $taggedPeople, + 'label' => StatusLabelService::get($status), + 'liked_by' => LikeService::likedBy($status), + 'media_attachments' => MediaService::get($status->id), + 'account' => ProfileService::get($status->profile_id, true), + 'tags' => StatusHashtagService::statusTags($status->id), + 'poll' => $poll, + 'bookmarked' => BookmarkService::get($pid, $status->id), + 'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null, + ]; + } } diff --git a/app/User.php b/app/User.php index a39f650be..086e4b0d8 100644 --- a/app/User.php +++ b/app/User.php @@ -2,28 +2,33 @@ namespace App; -use Laravel\Passport\HasApiTokens; -use Illuminate\Notifications\Notifiable; +use App\Services\AvatarService; +use App\Util\RateLimit\User as UserRateLimit; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; -use App\Util\RateLimit\User as UserRateLimit; -use App\Services\AvatarService; +use Illuminate\Notifications\Notifiable; +use Laravel\Passport\HasApiTokens; +use NotificationChannels\WebPush\HasPushSubscriptions; class User extends Authenticatable { - use Notifiable, SoftDeletes, HasApiTokens, UserRateLimit; + use HasApiTokens, HasFactory, HasPushSubscriptions, Notifiable, SoftDeletes, UserRateLimit; /** * The attributes that should be mutated to dates. * * @var array */ - protected $casts = [ - 'deleted_at' => 'datetime', - 'email_verified_at' => 'datetime', - '2fa_setup_at' => 'datetime', - 'last_active_at' => 'datetime', - ]; + protected function casts(): array + { + return [ + 'deleted_at' => 'datetime', + 'email_verified_at' => 'datetime', + '2fa_setup_at' => 'datetime', + 'last_active_at' => 'datetime', + ]; + } /** * The attributes that are mass assignable. @@ -38,7 +43,13 @@ class User extends Authenticatable 'app_register_ip', 'email_verified_at', 'last_active_at', - 'register_source' + 'register_source', + 'expo_token', + 'notify_enabled', + 'notify_like', + 'notify_follow', + 'notify_mention', + 'notify_comment', ]; /** @@ -50,7 +61,7 @@ class User extends Authenticatable 'email', 'password', 'is_admin', 'remember_token', 'email_verified_at', '2fa_enabled', '2fa_secret', '2fa_backup_codes', '2fa_setup_at', 'deleted_at', - 'updated_at' + 'updated_at', ]; public function profile() @@ -93,7 +104,7 @@ class User extends Authenticatable public function storageUsedKey() { - return 'profile:storage:used:' . $this->id; + return 'profile:storage:used:'.$this->id; } public function accountLog() @@ -108,11 +119,15 @@ class User extends Authenticatable public function avatarUrl() { - if(!$this->profile_id || $this->status) { - return config('app.url') . '/storage/avatars/default.jpg'; + if (! $this->profile_id || $this->status) { + return config('app.url').'/storage/avatars/default.jpg'; } return AvatarService::get($this->profile_id); } + public function routeNotificationForExpo() + { + return $this->expo_token; + } } diff --git a/app/UserFilter.php b/app/UserFilter.php index b0af2d777..dfa0d4662 100644 --- a/app/UserFilter.php +++ b/app/UserFilter.php @@ -33,4 +33,9 @@ class UserFilter extends Model { return $this->belongsTo(Instance::class, 'filterable_id'); } + + public function user() + { + return $this->belongsTo(Profile::class, 'user_id'); + } } diff --git a/app/Util/ActivityPub/DiscoverActor.php b/app/Util/ActivityPub/DiscoverActor.php index 61023900e..6680d944d 100644 --- a/app/Util/ActivityPub/DiscoverActor.php +++ b/app/Util/ActivityPub/DiscoverActor.php @@ -7,6 +7,7 @@ use Zttp\Zttp; class DiscoverActor { protected $url; + protected $response; public function __construct($url) @@ -17,9 +18,9 @@ class DiscoverActor public function fetch() { $res = Zttp::withHeaders([ - 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'User-Agent' => 'PixelfedBot - https://pixelfed.org', - ])->get($this->url); + 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'User-Agent' => 'PixelfedBot - https://pixelfed.org', + ])->get($this->url); $this->response = $res->body(); return $this; @@ -40,7 +41,7 @@ class DiscoverActor $this->fetch(); $res = $this->getResponse(); - if (empty($res) || !in_array('type', $res) || $res['type'] !== 'Person') { + if (empty($res) || ! in_array('type', $res) || $res['type'] !== 'Person') { throw new \Exception('Invalid Actor Object'); } diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index 1304f0811..bda1f4043 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -2,49 +2,37 @@ namespace App\Util\ActivityPub; -use DB, Cache, Purify, Storage, Request, Validator; -use App\{ - Activity, - Follower, - Instance, - Like, - Media, - Notification, - Profile, - Status -}; -use Zttp\Zttp; -use Carbon\Carbon; -use GuzzleHttp\Client; -use Illuminate\Http\File; -use Illuminate\Validation\Rule; -use App\Jobs\AvatarPipeline\CreateAvatar; -use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent; -use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail}; -use App\Jobs\StatusPipeline\NewStatusPipeline; +use App\Instance; +use App\Jobs\AvatarPipeline\RemoteAvatarFetch; +use App\Jobs\HomeFeedPipeline\FeedInsertRemotePipeline; +use App\Jobs\MediaPipeline\MediaStoragePipeline; use App\Jobs\StatusPipeline\StatusReplyPipeline; use App\Jobs\StatusPipeline\StatusTagsPipeline; -use App\Util\ActivityPub\HttpSignature; -use Illuminate\Support\Str; -use App\Services\ActivityPubFetchService; +use App\Media; +use App\Models\ModeratedProfile; +use App\Models\Poll; +use App\Profile; +use App\Services\Account\AccountStatService; use App\Services\ActivityPubDeliveryService; -use App\Services\CustomEmojiService; +use App\Services\ActivityPubFetchService; +use App\Services\DomainService; use App\Services\InstanceService; use App\Services\MediaPathService; -use App\Services\MediaStorageService; use App\Services\NetworkTimelineService; -use App\Jobs\MediaPipeline\MediaStoragePipeline; -use App\Jobs\AvatarPipeline\RemoteAvatarFetch; -use App\Util\Media\License; -use App\Models\Poll; -use Illuminate\Contracts\Cache\LockTimeoutException; -use App\Jobs\ProfilePipeline\IncrementPostCount; -use App\Jobs\ProfilePipeline\DecrementPostCount; -use App\Services\DomainService; use App\Services\UserFilterService; +use App\Status; +use App\Util\Media\License; +use Cache; +use Carbon\Carbon; +use Illuminate\Support\Str; +use Illuminate\Validation\Rule; +use League\Uri\Exceptions\UriException; +use League\Uri\Uri; +use Purify; +use Validator; -class Helpers { - +class Helpers +{ public static function validateObject($data) { $verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone']; @@ -53,14 +41,14 @@ class Helpers { 'type' => [ 'required', 'string', - Rule::in($verbs) + Rule::in($verbs), ], 'id' => 'required|string', 'actor' => 'required|string|url', 'object' => 'required', 'object.type' => 'required_if:type,Create', 'object.attributedTo' => 'required_if:type,Create|url', - 'published' => 'required_if:type,Create|date' + 'published' => 'required_if:type,Create|date', ])->passes(); return $valid; @@ -68,8 +56,8 @@ class Helpers { public static function verifyAttachments($data) { - if(!isset($data['object']) || empty($data['object'])) { - $data = ['object'=>$data]; + if (! isset($data['object']) || empty($data['object'])) { + $data = ['object' => $data]; } $activity = $data['object']; @@ -80,7 +68,7 @@ class Helpers { // Peertube // $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video', 'Link'] : ['Document', 'Image']; - if(!isset($activity['attachment']) || empty($activity['attachment'])) { + if (! isset($activity['attachment']) || empty($activity['attachment'])) { return false; } @@ -100,13 +88,13 @@ class Helpers { '*.type' => [ 'required', 'string', - Rule::in($mediaTypes) + Rule::in($mediaTypes), ], '*.url' => 'required|url', - '*.mediaType' => [ + '*.mediaType' => [ 'required', 'string', - Rule::in($mimeTypes) + Rule::in($mimeTypes), ], '*.name' => 'sometimes|nullable|string', '*.blurhash' => 'sometimes|nullable|string|min:6|max:164', @@ -119,7 +107,7 @@ class Helpers { public static function normalizeAudience($data, $localOnly = true) { - if(!isset($data['to'])) { + if (! isset($data['to'])) { return; } @@ -128,32 +116,35 @@ class Helpers { $audience['cc'] = []; $scope = 'private'; - if(is_array($data['to']) && !empty($data['to'])) { + if (is_array($data['to']) && ! empty($data['to'])) { foreach ($data['to'] as $to) { - if($to == 'https://www.w3.org/ns/activitystreams#Public') { + if ($to == 'https://www.w3.org/ns/activitystreams#Public') { $scope = 'public'; + continue; } $url = $localOnly ? self::validateLocalUrl($to) : self::validateUrl($to); - if($url != false) { + if ($url != false) { array_push($audience['to'], $url); } } } - if(is_array($data['cc']) && !empty($data['cc'])) { + if (is_array($data['cc']) && ! empty($data['cc'])) { foreach ($data['cc'] as $cc) { - if($cc == 'https://www.w3.org/ns/activitystreams#Public') { + if ($cc == 'https://www.w3.org/ns/activitystreams#Public') { $scope = 'unlisted'; + continue; } $url = $localOnly ? self::validateLocalUrl($cc) : self::validateUrl($cc); - if($url != false) { + if ($url != false) { array_push($audience['cc'], $url); } } } $audience['scope'] = $scope; + return $audience; } @@ -161,75 +152,96 @@ class Helpers { { $audience = self::normalizeAudience($data); $url = $profile->permalink(); + return in_array($url, $audience['to']) || in_array($url, $audience['cc']); } - public static function validateUrl($url) + public static function validateUrl($url = null, $disableDNSCheck = false, $forceBanCheck = false) { - if(is_array($url)) { + if (is_array($url) && ! empty($url)) { $url = $url[0]; } + if (! $url || strlen($url) === 0) { + return false; + } + try { + $uri = Uri::new($url); - $hash = hash('sha256', $url); - $key = "helpers:url:valid:sha256-{$hash}"; + if (! $uri) { + return false; + } + + if ($uri->getScheme() !== 'https') { + return false; + } + + $host = $uri->getHost(); + + if (! $host || $host === '') { + return false; + } + + if (! filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { + return false; + } + + if (! str_contains($host, '.')) { + return false; + } - $valid = Cache::remember($key, 900, function() use($url) { $localhosts = [ - '127.0.0.1', 'localhost', '::1' + 'localhost', + '127.0.0.1', + '::1', + 'broadcasthost', + 'ip6-localhost', + 'ip6-loopback', ]; - if(strtolower(mb_substr($url, 0, 8)) !== 'https://') { + if (in_array($host, $localhosts)) { return false; } - if(substr_count($url, '://') !== 1) { - return false; - } - - if(mb_substr($url, 0, 8) !== 'https://') { - $url = 'https://' . substr($url, 8); - } - - $valid = filter_var($url, FILTER_VALIDATE_URL); - - if(!$valid) { - return false; - } - - $host = parse_url($valid, PHP_URL_HOST); - - if(in_array($host, $localhosts)) { - return false; - } - - if(config('security.url.verify_dns')) { - if(DomainService::hasValidDns($host) === false) { + if ($disableDNSCheck !== true && app()->environment() === 'production' && (bool) config('security.url.verify_dns')) { + $hash = hash('sha256', $host); + $key = "helpers:url:valid-dns:sha256-{$hash}"; + $domainValidDns = Cache::remember($key, 14440, function () use ($host) { + return DomainService::hasValidDns($host); + }); + if (! $domainValidDns) { return false; } } - if(app()->environment() === 'production') { + if ($forceBanCheck || $disableDNSCheck !== true && app()->environment() === 'production') { $bannedInstances = InstanceService::getBannedDomains(); - if(in_array($host, $bannedInstances)) { + if (in_array($host, $bannedInstances)) { return false; } } - return $url; - }); - - return $valid; + return $uri->toString(); + } catch (UriException $e) { + return false; + } } public static function validateLocalUrl($url) { $url = self::validateUrl($url); - if($url == true) { + if ($url == true) { $domain = config('pixelfed.domain.app'); - $host = parse_url($url, PHP_URL_HOST); + + $uri = Uri::new($url); + $host = $uri->getHost(); + if (! $host || empty($host)) { + return false; + } $url = strtolower($domain) === strtolower($host) ? $url : false; + return $url; } + return false; } @@ -237,15 +249,16 @@ class Helpers { { $version = config('pixelfed.version'); $url = config('app.url'); + return [ - 'Accept' => 'application/activity+json', + 'Accept' => 'application/activity+json', 'User-Agent' => "(Pixelfed/{$version}; +{$url})", ]; } public static function fetchFromUrl($url = false) { - if(self::validateUrl($url) == false) { + if (self::validateUrl($url) == false) { return; } @@ -253,13 +266,13 @@ class Helpers { $key = "helpers:url:fetcher:sha256-{$hash}"; $ttl = now()->addMinutes(15); - return Cache::remember($key, $ttl, function() use($url) { + return Cache::remember($key, $ttl, function () use ($url) { $res = ActivityPubFetchService::get($url); - if(!$res || empty($res)) { + if (! $res || empty($res)) { return false; } $res = json_decode($res, true, 8); - if(json_last_error() == JSON_ERROR_NONE) { + if (json_last_error() == JSON_ERROR_NONE) { return $res; } else { return false; @@ -274,48 +287,86 @@ class Helpers { public static function pluckval($val) { - if(is_string($val)) { + if (is_string($val)) { return $val; } - if(is_array($val)) { - return !empty($val) ? head($val) : null; + if (is_array($val)) { + return ! empty($val) ? head($val) : null; } return null; } + public static function validateTimestamp($timestamp) + { + try { + $date = Carbon::parse($timestamp); + $now = Carbon::now(); + $tenYearsAgo = $now->copy()->subYears(10); + $isMoreThanTenYearsOld = $date->lt($tenYearsAgo); + $tomorrow = $now->copy()->addDay(); + $isMoreThanOneDayFuture = $date->gt($tomorrow); + + return ! ($isMoreThanTenYearsOld || $isMoreThanOneDayFuture); + } catch (\Exception $e) { + return false; + } + } + public static function statusFirstOrFetch($url, $replyTo = false) { $url = self::validateUrl($url); - if($url == false) { + if ($url == false) { return; } $host = parse_url($url, PHP_URL_HOST); $local = config('pixelfed.domain.app') == $host ? true : false; - if($local) { + if ($local) { $id = (int) last(explode('/', $url)); - return Status::whereNotIn('scope', ['draft','archived'])->findOrFail($id); + + return Status::whereNotIn('scope', ['draft', 'archived'])->findOrFail($id); } - $cached = Status::whereNotIn('scope', ['draft','archived']) + $cached = Status::whereNotIn('scope', ['draft', 'archived']) ->whereUri($url) ->orWhere('object_url', $url) ->first(); - if($cached) { + if ($cached) { return $cached; } $res = self::fetchFromUrl($url); - if(!$res || empty($res) || isset($res['error']) || !isset($res['@context']) || !isset($res['published']) ) { + if (! $res || empty($res) || isset($res['error']) || ! isset($res['@context']) || ! isset($res['published'])) { return; } - if(isset($res['object'])) { + if (! self::validateTimestamp($res['published'])) { + return; + } + + if (config('autospam.live_filters.enabled')) { + $filters = config('autospam.live_filters.filters'); + if (! empty($filters) && isset($res['content']) && ! empty($res['content']) && strlen($filters) > 3) { + $filters = array_map('trim', explode(',', $filters)); + $content = $res['content']; + foreach ($filters as $filter) { + $filter = trim(strtolower($filter)); + if (! $filter || ! strlen($filter)) { + continue; + } + if (str_contains(strtolower($content), $filter)) { + return; + } + } + } + } + + if (isset($res['object'])) { $activity = $res; } else { $activity = ['object' => $res]; @@ -325,37 +376,37 @@ class Helpers { $cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false; - if(isset($res['to']) == true) { - if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) { + if (isset($res['to']) == true) { + if (is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) { $scope = 'public'; } - if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) { + if (is_string($res['to']) && $res['to'] == 'https://www.w3.org/ns/activitystreams#Public') { $scope = 'public'; } } - if(isset($res['cc']) == true) { - if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) { + if (isset($res['cc']) == true) { + if (is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) { $scope = 'unlisted'; } - if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) { + if (is_string($res['cc']) && $res['cc'] == 'https://www.w3.org/ns/activitystreams#Public') { $scope = 'unlisted'; } } - if(config('costar.enabled') == true) { + if (config('costar.enabled') == true) { $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null) { + if ($blockedKeywords !== null) { $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($res['content'], $kw) == true) { + foreach ($keywords as $kw) { + if (Str::contains($res['content'], $kw) == true) { return; } } } $unlisted = config('costar.domain.unlisted'); - if(in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) { + if (in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) { $unlisted = true; $scope = 'unlisted'; } else { @@ -363,7 +414,7 @@ class Helpers { } $cwDomains = config('costar.domain.cw'); - if(in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) { + if (in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) { $cw = true; } } @@ -372,11 +423,15 @@ class Helpers { $idDomain = parse_url($id, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST); - if(!self::validateUrl($id)) { + if ($idDomain && $urlDomain && strtolower($idDomain) !== strtolower($urlDomain)) { return; } - if(!isset($activity['object']['attributedTo'])) { + if (! self::validateUrl($id)) { + return; + } + + if (! isset($activity['object']['attributedTo'])) { return; } @@ -384,39 +439,38 @@ class Helpers { $activity['object']['attributedTo'] : (is_array($activity['object']['attributedTo']) ? collect($activity['object']['attributedTo']) - ->filter(function($o) { + ->filter(function ($o) { return $o && isset($o['type']) && $o['type'] == 'Person'; }) ->pluck('id') ->first() : null ); - if($attributedTo) { + if ($attributedTo) { $actorDomain = parse_url($attributedTo, PHP_URL_HOST); - if(!self::validateUrl($attributedTo) || + if (! self::validateUrl($attributedTo) || $idDomain !== $actorDomain || $actorDomain !== $urlDomain - ) - { + ) { return; } } - if($idDomain !== $urlDomain) { + if ($idDomain !== $urlDomain) { return; } $profile = self::profileFirstOrNew($attributedTo); - if(!$profile) { + if (! $profile) { return; } - if(isset($activity['object']['inReplyTo']) && !empty($activity['object']['inReplyTo']) || $replyTo == true) { + if (isset($activity['object']['inReplyTo']) && ! empty($activity['object']['inReplyTo']) || $replyTo == true) { $reply_to = self::statusFirstOrFetch(self::pluckval($activity['object']['inReplyTo']), false); - if($reply_to) { + if ($reply_to) { $blocks = UserFilterService::blocks($reply_to->profile_id); - if(in_array($profile->id, $blocks)) { + if (in_array($profile->id, $blocks)) { return; } } @@ -426,15 +480,15 @@ class Helpers { } $ts = self::pluckval($res['published']); - if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) { + if ($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) { $scope = 'unlisted'; } - if(in_array($urlDomain, InstanceService::getNsfwDomains())) { + if (in_array($urlDomain, InstanceService::getNsfwDomains())) { $cw = true; } - if($res['type'] === 'Question') { + if ($res['type'] === 'Question') { $status = self::storePoll( $profile, $res, @@ -445,6 +499,7 @@ class Helpers { $scope, $id ); + return $status; } else { $status = self::storeStatus($url, $profile, $res); @@ -455,11 +510,18 @@ class Helpers { public static function storeStatus($url, $profile, $activity) { + $originalUrl = $url; $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($activity['url']); $url = isset($activity['url']) && is_string($activity['url']) ? self::pluckval($activity['url']) : self::pluckval($id); $idDomain = parse_url($id, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST); - if(!self::validateUrl($id) || !self::validateUrl($url)) { + $originalUrlDomain = parse_url($originalUrl, PHP_URL_HOST); + if (! self::validateUrl($id) || ! self::validateUrl($url)) { + return; + } + + if (strtolower($originalUrlDomain) !== strtolower($idDomain) || + strtolower($originalUrlDomain) !== strtolower($urlDomain)) { return; } @@ -470,27 +532,27 @@ class Helpers { $cw = self::getSensitive($activity, $url); $pid = is_object($profile) ? $profile->id : (is_array($profile) ? $profile['id'] : null); $isUnlisted = is_object($profile) ? $profile->unlisted : (is_array($profile) ? $profile['unlisted'] : false); - $commentsDisabled = isset($activity['commentsEnabled']) ? !boolval($activity['commentsEnabled']) : false; + $commentsDisabled = isset($activity['commentsEnabled']) ? ! boolval($activity['commentsEnabled']) : false; - if(!$pid) { + if (! $pid) { return; } - if($scope == 'public') { - if($isUnlisted == true) { + if ($scope == 'public') { + if ($isUnlisted == true) { $scope = 'unlisted'; } } - + $defaultCaption = config_cache('database.default') === 'mysql' ? null : ""; $status = Status::updateOrCreate( [ - 'uri' => $url + 'uri' => $url, ], [ 'profile_id' => $pid, 'url' => $url, 'object_url' => $id, - 'caption' => isset($activity['content']) ? Purify::clean(strip_tags($activity['content'])) : null, - 'rendered' => isset($activity['content']) ? Purify::clean($activity['content']) : null, + 'caption' => isset($activity['content']) ? Purify::clean(strip_tags($activity['content'])) : $defaultCaption, + 'rendered' => $defaultCaption, 'created_at' => Carbon::parse($ts)->tz('UTC'), 'in_reply_to_id' => $reply_to, 'local' => false, @@ -499,24 +561,24 @@ class Helpers { 'visibility' => $scope, 'cw_summary' => ($cw == true && isset($activity['summary']) ? Purify::clean(strip_tags($activity['summary'])) : null), - 'comments_disabled' => $commentsDisabled + 'comments_disabled' => $commentsDisabled, ] ); - if($reply_to == null) { + if ($reply_to == null) { self::importNoteAttachment($activity, $status); } else { - if(isset($activity['attachment']) && !empty($activity['attachment'])) { + if (isset($activity['attachment']) && ! empty($activity['attachment'])) { self::importNoteAttachment($activity, $status); } StatusReplyPipeline::dispatch($status); } - if(isset($activity['tag']) && is_array($activity['tag']) && !empty($activity['tag'])) { + if (isset($activity['tag']) && is_array($activity['tag']) && ! empty($activity['tag'])) { StatusTagsPipeline::dispatch($activity, $status); } - if( config('instance.timeline.network.cached') && + if (config('instance.timeline.network.cached') && $status->in_reply_to_id === null && $status->reblog_of_id === null && in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) && @@ -528,27 +590,34 @@ class Helpers { ->unique() ->values() ->toArray(); - if(!in_array($urlDomain, $filteredDomains)) { - if(!$isUnlisted) { + if (! in_array($urlDomain, $filteredDomains)) { + if (! $isUnlisted) { NetworkTimelineService::add($status->id); } } } - IncrementPostCount::dispatch($pid)->onQueue('low'); + AccountStatService::incrementPostCount($pid); + + if ($status->in_reply_to_id === null && + in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ) { + FeedInsertRemotePipeline::dispatch($status->id, $pid)->onQueue('feed'); + } return $status; } public static function getSensitive($activity, $url) { - $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($url); - $url = isset($activity['url']) ? self::pluckval($activity['url']) : $id; - $urlDomain = parse_url($url, PHP_URL_HOST); + if (! $url || ! strlen($url)) { + return true; + } + $urlDomain = parse_url($url, PHP_URL_HOST); $cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false; - if(in_array($urlDomain, InstanceService::getNsfwDomains())) { + if (in_array($urlDomain, InstanceService::getNsfwDomains())) { $cw = true; } @@ -558,13 +627,13 @@ class Helpers { public static function getReplyTo($activity) { $reply_to = null; - $inReplyTo = isset($activity['inReplyTo']) && !empty($activity['inReplyTo']) ? + $inReplyTo = isset($activity['inReplyTo']) && ! empty($activity['inReplyTo']) ? self::pluckval($activity['inReplyTo']) : false; - if($inReplyTo) { + if ($inReplyTo) { $reply_to = self::statusFirstOrFetch($inReplyTo); - if($reply_to) { + if ($reply_to) { $reply_to = optional($reply_to)->id; } } else { @@ -581,25 +650,25 @@ class Helpers { $urlDomain = parse_url(self::pluckval($url), PHP_URL_HOST); $scope = 'private'; - if(isset($activity['to']) == true) { - if(is_array($activity['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to'])) { + if (isset($activity['to']) == true) { + if (is_array($activity['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to'])) { $scope = 'public'; } - if(is_string($activity['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $activity['to']) { + if (is_string($activity['to']) && $activity['to'] == 'https://www.w3.org/ns/activitystreams#Public') { $scope = 'public'; } } - if(isset($activity['cc']) == true) { - if(is_array($activity['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['cc'])) { + if (isset($activity['cc']) == true) { + if (is_array($activity['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['cc'])) { $scope = 'unlisted'; } - if(is_string($activity['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $activity['cc']) { + if (is_string($activity['cc']) && $activity['cc'] == 'https://www.w3.org/ns/activitystreams#Public') { $scope = 'unlisted'; } } - if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) { + if ($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) { $scope = 'unlisted'; } @@ -608,25 +677,26 @@ class Helpers { private static function storePoll($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id) { - if(!isset($res['endTime']) || !isset($res['oneOf']) || !is_array($res['oneOf']) || count($res['oneOf']) > 4) { + if (! isset($res['endTime']) || ! isset($res['oneOf']) || ! is_array($res['oneOf']) || count($res['oneOf']) > 4) { return; } - $options = collect($res['oneOf'])->map(function($option) { + $options = collect($res['oneOf'])->map(function ($option) { return $option['name']; })->toArray(); - $cachedTallies = collect($res['oneOf'])->map(function($option) { + $cachedTallies = collect($res['oneOf'])->map(function ($option) { return $option['replies']['totalItems'] ?? 0; })->toArray(); + $defaultCaption = config_cache('database.default') === 'mysql' ? null : ""; $status = new Status; $status->profile_id = $profile->id; $status->url = isset($res['url']) ? $res['url'] : $url; $status->uri = isset($res['url']) ? $res['url'] : $url; $status->object_url = $id; - $status->caption = strip_tags($res['content']); - $status->rendered = Purify::clean($res['content']); + $status->caption = strip_tags(Purify::clean($res['content'])) ?? $defaultCaption; + $status->rendered = $defaultCaption; $status->created_at = Carbon::parse($ts)->tz('UTC'); $status->in_reply_to_id = null; $status->local = false; @@ -662,9 +732,10 @@ class Helpers { public static function importNoteAttachment($data, Status $status) { - if(self::verifyAttachments($data) == false) { + if (self::verifyAttachments($data) == false) { // \Log::info('importNoteAttachment::failedVerification.', [$data['id']]); $status->viewType(); + return; } $attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment']; @@ -677,11 +748,11 @@ class Helpers { $storagePath = MediaPathService::get($user, 2); $allowed = explode(',', config_cache('pixelfed.media_types')); - foreach($attachments as $key => $media) { + foreach ($attachments as $key => $media) { $type = $media['mediaType']; $url = $media['url']; $valid = self::validateUrl($url); - if(in_array($type, $allowed) == false || $valid == false) { + if (in_array($type, $allowed) == false || $valid == false) { continue; } $blurhash = isset($media['blurhash']) ? $media['blurhash'] : null; @@ -690,7 +761,7 @@ class Helpers { $width = isset($media['width']) ? $media['width'] : false; $height = isset($media['height']) ? $media['height'] : false; - $media = new Media(); + $media = new Media; $media->blurhash = $blurhash; $media->remote_media = true; $media->status_id = $status->id; @@ -700,85 +771,110 @@ class Helpers { $media->remote_url = $url; $media->caption = $caption; $media->order = $key + 1; - if($width) { + if ($width) { $media->width = $width; } - if($height) { + if ($height) { $media->height = $height; } - if($license) { + if ($license) { $media->license = $license; } $media->mime = $type; $media->version = 3; $media->save(); - if(config_cache('pixelfed.cloud_storage') == true) { + if ((bool) config_cache('pixelfed.cloud_storage') == true) { MediaStoragePipeline::dispatch($media); } } $status->viewType(); - return; + } public static function profileFirstOrNew($url) { $url = self::validateUrl($url); - if($url == false) { + if ($url == false) { return; } $host = parse_url($url, PHP_URL_HOST); $local = config('pixelfed.domain.app') == $host ? true : false; - if($local == true) { + if ($local == true) { $id = last(explode('/', $url)); + return Profile::whereNull('status') ->whereNull('domain') ->whereUsername($id) ->firstOrFail(); } - if($profile = Profile::whereRemoteUrl($url)->first()) { - if($profile->last_fetched_at && $profile->last_fetched_at->lt(now()->subHours(24))) { + if ($profile = Profile::whereRemoteUrl($url)->first()) { + if ($profile->last_fetched_at && $profile->last_fetched_at->lt(now()->subHours(24))) { return self::profileUpdateOrCreate($url); } + return $profile; } return self::profileUpdateOrCreate($url); } - public static function profileUpdateOrCreate($url) + public static function profileUpdateOrCreate($url, $movedToCheck = false) { + $movedToPid = null; $res = self::fetchProfileFromUrl($url); - if(!$res || isset($res['id']) == false) { + if (! $res || isset($res['id']) == false) { return; } - $domain = parse_url($res['id'], PHP_URL_HOST); - if(!isset($res['preferredUsername']) && !isset($res['nickname'])) { + if (! self::validateUrl($res['inbox'])) { return; } + if (! self::validateUrl($res['id'])) { + return; + } + + if (ModeratedProfile::whereProfileUrl($res['id'])->whereIsBanned(true)->exists()) { + return; + } + + $urlDomain = parse_url($url, PHP_URL_HOST); + $domain = parse_url($res['id'], PHP_URL_HOST); + if (strtolower($urlDomain) !== strtolower($domain)) { + return; + } + if (! isset($res['preferredUsername']) && ! isset($res['nickname'])) { + return; + } + // skip invalid usernames + if (! ctype_alnum($res['preferredUsername'])) { + $tmpUsername = str_replace(['_', '.', '-'], '', $res['preferredUsername']); + if (! ctype_alnum($tmpUsername)) { + return; + } + } $username = (string) Purify::clean($res['preferredUsername'] ?? $res['nickname']); - if(empty($username)) { + if (empty($username)) { return; } $remoteUsername = $username; $webfinger = "@{$username}@{$domain}"; - if(!self::validateUrl($res['inbox'])) { - return; - } - if(!self::validateUrl($res['id'])) { - return; + $instance = Instance::updateOrCreate([ + 'domain' => $domain, + ]); + if ($instance->wasRecentlyCreated == true) { + \App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low'); } - $instance = Instance::updateOrCreate([ - 'domain' => $domain - ]); - if($instance->wasRecentlyCreated == true) { - \App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low'); + if (! $movedToCheck && isset($res['movedTo']) && Helpers::validateUrl($res['movedTo'])) { + $movedTo = self::profileUpdateOrCreate($res['movedTo'], true); + if ($movedTo) { + $movedToPid = $movedTo->id; + } } $profile = Profile::updateOrCreate( @@ -797,16 +893,18 @@ class Helpers { 'outbox_url' => isset($res['outbox']) ? $res['outbox'] : null, 'public_key' => $res['publicKey']['publicKeyPem'], 'indexable' => isset($res['indexable']) && is_bool($res['indexable']) ? $res['indexable'] : false, + 'moved_to_profile_id' => $movedToPid, ] ); - if( $profile->last_fetched_at == null || + if ($profile->last_fetched_at == null || $profile->last_fetched_at->lt(now()->subMonths(3)) ) { RemoteAvatarFetch::dispatch($profile); } $profile->last_fetched_at = now(); $profile->save(); + return $profile; } @@ -815,8 +913,16 @@ class Helpers { return self::profileFirstOrNew($url); } + public static function getSignedFetch($url) + { + return ActivityPubFetchService::get($url); + } + public static function sendSignedObject($profile, $url, $body) { + if (app()->environment() !== 'production') { + return; + } ActivityPubDeliveryService::queue() ->from($profile) ->to($url) diff --git a/app/Util/ActivityPub/HttpSignature.php b/app/Util/ActivityPub/HttpSignature.php index 5bfdcac09..20071932b 100644 --- a/app/Util/ActivityPub/HttpSignature.php +++ b/app/Util/ActivityPub/HttpSignature.php @@ -2,146 +2,198 @@ namespace App\Util\ActivityPub; -use Cache, Log; use App\Models\InstanceActor; use App\Profile; -use \DateTime; +use Cache; +use DateTime; -class HttpSignature { +class HttpSignature +{ + /* + * source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php + * thanks aaronpk! + */ - /* - * source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php - * thanks aaronpk! - */ + public static function sign(Profile $profile, $url, $body = false, $addlHeaders = []) + { + if ($body) { + $digest = self::_digest($body); + } + $user = $profile; + $headers = self::_headersToSign($url, $body ? $digest : false); + $headers = array_merge($headers, $addlHeaders); + $stringToSign = self::_headersToSigningString($headers); + $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); + $key = openssl_pkey_get_private($user->private_key); + if (empty($key)) { + return []; + } + openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); + if (empty($signature)) { + return []; + } + $signature = base64_encode($signature); + $signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; + unset($headers['(request-target)']); + $headers['Signature'] = $signatureHeader; - public static function sign(Profile $profile, $url, $body = false, $addlHeaders = []) { - if($body) { - $digest = self::_digest($body); - } - $user = $profile; - $headers = self::_headersToSign($url, $body ? $digest : false); - $headers = array_merge($headers, $addlHeaders); - $stringToSign = self::_headersToSigningString($headers); - $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); - $key = openssl_pkey_get_private($user->private_key); - openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); - $signature = base64_encode($signature); - $signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; - unset($headers['(request-target)']); - $headers['Signature'] = $signatureHeader; - - return self::_headersToCurlArray($headers); - } - - public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post') - { - $keyId = config('app.url') . '/i/actor#main-key'; - $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function() { - return InstanceActor::first()->private_key; - }); - if($body) { - $digest = self::_digest($body); - } - $headers = self::_headersToSign($url, $body ? $digest : false, $method); - $headers = array_merge($headers, $addlHeaders); - $stringToSign = self::_headersToSigningString($headers); - $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); - $key = openssl_pkey_get_private($privateKey); - openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); - $signature = base64_encode($signature); - $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; - unset($headers['(request-target)']); - $headers['Signature'] = $signatureHeader; - - return $headers; - } - - public static function parseSignatureHeader($signature) { - $parts = explode(',', $signature); - $signatureData = []; - - foreach($parts as $part) { - if(preg_match('/(.+)="(.+)"/', $part, $match)) { - $signatureData[$match[1]] = $match[2]; - } + return self::_headersToCurlArray($headers); } - if(!isset($signatureData['keyId'])) { - return [ - 'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData)) - ]; + public static function signRaw($privateKey, $keyId, $url, $body = false, $addlHeaders = []) + { + if (empty($privateKey) || empty($keyId)) { + return []; + } + if ($body) { + $digest = self::_digest($body); + } + $headers = self::_headersToSign($url, $body ? $digest : false); + $headers = array_merge($headers, $addlHeaders); + $stringToSign = self::_headersToSigningString($headers); + $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); + $key = openssl_pkey_get_private($privateKey); + if (empty($key)) { + return []; + } + openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); + if (empty($signature)) { + return []; + } + $signature = base64_encode($signature); + $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; + unset($headers['(request-target)']); + $headers['Signature'] = $signatureHeader; + + return self::_headersToCurlArray($headers); } - if(!filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) { - return [ - 'error' => 'keyId is not a URL: '.$signatureData['keyId'] - ]; + public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post') + { + $keyId = config('app.url').'/i/actor#main-key'; + if(config_cache('database.default') === 'mysql') { + $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () { + return InstanceActor::first()->private_key; + }); + } else { + $privateKey = InstanceActor::first()?->private_key; + } + abort_if(!$privateKey || empty($privateKey), 400, 'Missing instance actor key, please run php artisan instance:actor'); + if ($body) { + $digest = self::_digest($body); + } + $headers = self::_headersToSign($url, $body ? $digest : false, $method); + $headers = array_merge($headers, $addlHeaders); + $stringToSign = self::_headersToSigningString($headers); + $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); + $key = openssl_pkey_get_private($privateKey); + openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); + $signature = base64_encode($signature); + $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; + unset($headers['(request-target)']); + $headers['Signature'] = $signatureHeader; + + return $headers; } - if(!isset($signatureData['headers']) || !isset($signatureData['signature'])) { - return [ - 'error' => 'Signature is missing headers or signature parts' - ]; + public static function parseSignatureHeader($signature) + { + $parts = explode(',', $signature); + $signatureData = []; + + foreach ($parts as $part) { + if (preg_match('/(.+)="(.+)"/', $part, $match)) { + $signatureData[$match[1]] = $match[2]; + } + } + + if (! isset($signatureData['keyId'])) { + return [ + 'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData)), + ]; + } + + if (! filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) { + return [ + 'error' => 'keyId is not a URL: '.$signatureData['keyId'], + ]; + } + + if (! Helpers::validateUrl($signatureData['keyId'])) { + return [ + 'error' => 'keyId is not a URL: '.$signatureData['keyId'], + ]; + } + + if (! isset($signatureData['headers']) || ! isset($signatureData['signature'])) { + return [ + 'error' => 'Signature is missing headers or signature parts', + ]; + } + + return $signatureData; } - return $signatureData; - } + public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body) + { + $digest = 'SHA-256='.base64_encode(hash('sha256', $body, true)); + $headersToSign = []; + foreach (explode(' ', $signatureData['headers']) as $h) { + if ($h == '(request-target)') { + $headersToSign[$h] = 'post '.$path; + } elseif ($h == 'digest') { + $headersToSign[$h] = $digest; + } elseif (isset($inputHeaders[$h][0])) { + $headersToSign[$h] = $inputHeaders[$h][0]; + } + } + $signingString = self::_headersToSigningString($headersToSign); - public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body) { - $digest = 'SHA-256='.base64_encode(hash('sha256', $body, true)); - $headersToSign = []; - foreach(explode(' ',$signatureData['headers']) as $h) { - if($h == '(request-target)') { - $headersToSign[$h] = 'post '.$path; - } elseif($h == 'digest') { - $headersToSign[$h] = $digest; - } elseif(isset($inputHeaders[$h][0])) { - $headersToSign[$h] = $inputHeaders[$h][0]; - } - } - $signingString = self::_headersToSigningString($headersToSign); + $verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256); - $verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256); - - return [$verified, $signingString]; - } - - private static function _headersToSigningString($headers) { - return implode("\n", array_map(function($k, $v){ - return strtolower($k).': '.$v; - }, array_keys($headers), $headers)); - } - - private static function _headersToCurlArray($headers) { - return array_map(function($k, $v){ - return "$k: $v"; - }, array_keys($headers), $headers); - } - - private static function _digest($body) { - if(is_array($body)) { - $body = json_encode($body); - } - return base64_encode(hash('sha256', $body, true)); - } - - protected static function _headersToSign($url, $digest = false, $method = 'post') { - $date = new DateTime('UTC'); - - if(!in_array($method, ['post', 'get'])) { - throw new \Exception('Invalid method used to sign headers in HttpSignature'); - } - $headers = [ - '(request-target)' => $method . ' '.parse_url($url, PHP_URL_PATH), - 'Host' => parse_url($url, PHP_URL_HOST), - 'Date' => $date->format('D, d M Y H:i:s \G\M\T'), - ]; - - if($digest) { - $headers['Digest'] = 'SHA-256='.$digest; + return [$verified, $signingString]; } - return $headers; - } + private static function _headersToSigningString($headers) + { + return implode("\n", array_map(function ($k, $v) { + return strtolower($k).': '.$v; + }, array_keys($headers), $headers)); + } + private static function _headersToCurlArray($headers) + { + return array_map(function ($k, $v) { + return "$k: $v"; + }, array_keys($headers), $headers); + } + + private static function _digest($body) + { + if (is_array($body)) { + $body = json_encode($body); + } + + return base64_encode(hash('sha256', $body, true)); + } + + protected static function _headersToSign($url, $digest = false, $method = 'post') + { + $date = new DateTime('UTC'); + + if (! in_array($method, ['post', 'get'])) { + throw new \Exception('Invalid method used to sign headers in HttpSignature'); + } + $headers = [ + '(request-target)' => $method.' '.parse_url($url, PHP_URL_PATH), + 'Host' => parse_url($url, PHP_URL_HOST), + 'Date' => $date->format('D, d M Y H:i:s \G\M\T'), + ]; + + if ($digest) { + $headers['Digest'] = 'SHA-256='.$digest; + } + + return $headers; + } } diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 4441becfb..e98b48c49 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -2,1236 +2,1396 @@ namespace App\Util\ActivityPub; -use Cache, DB, Log, Purify, Redis, Storage, Validator; -use App\{ - Activity, - DirectMessage, - Follower, - FollowRequest, - Instance, - Like, - Notification, - Media, - Profile, - Status, - StatusHashtag, - Story, - StoryView, - UserFilter -}; -use Carbon\Carbon; -use App\Util\ActivityPub\Helpers; -use Illuminate\Support\Str; -use App\Jobs\LikePipeline\LikePipeline; -use App\Jobs\FollowPipeline\FollowPipeline; +use App\DirectMessage; +use App\Follower; +use App\FollowRequest; +use App\Instance; use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; +use App\Jobs\FollowPipeline\FollowPipeline; +use App\Jobs\HomeFeedPipeline\FeedRemoveRemotePipeline; +use App\Jobs\LikePipeline\LikePipeline; +use App\Jobs\MovePipeline\CleanupLegacyAccountMovePipeline; +use App\Jobs\MovePipeline\MoveMigrateFollowersPipeline; +use App\Jobs\MovePipeline\ProcessMovePipeline; +use App\Jobs\MovePipeline\UnfollowLegacyAccountMovePipeline; +use App\Jobs\ProfilePipeline\HandleUpdateActivity; +use App\Jobs\PushNotificationPipeline\MentionPushNotifyPipeline; use App\Jobs\StatusPipeline\RemoteStatusDelete; +use App\Jobs\StatusPipeline\StatusRemoteUpdatePipeline; use App\Jobs\StoryPipeline\StoryExpire; use App\Jobs\StoryPipeline\StoryFetch; -use App\Jobs\StatusPipeline\StatusRemoteUpdatePipeline; -use App\Jobs\ProfilePipeline\HandleUpdateActivity; - +use App\Like; +use App\Media; +use App\Models\Conversation; +use App\Models\RemoteReport; +use App\Notification; +use App\Profile; +use App\Services\AccountService; +use App\Services\FollowerService; +use App\Services\NotificationAppGatewayService; +use App\Services\PollService; +use App\Services\PushNotificationService; +use App\Services\ReblogService; +use App\Services\UserFilterService; +use App\Status; +use App\Story; +use App\StoryView; +use App\UserFilter; use App\Util\ActivityPub\Validator\Accept as AcceptValidator; -use App\Util\ActivityPub\Validator\Add as AddValidator; use App\Util\ActivityPub\Validator\Announce as AnnounceValidator; use App\Util\ActivityPub\Validator\Follow as FollowValidator; use App\Util\ActivityPub\Validator\Like as LikeValidator; -use App\Util\ActivityPub\Validator\UndoFollow as UndoFollowValidator; +use App\Util\ActivityPub\Validator\MoveValidator; use App\Util\ActivityPub\Validator\UpdatePersonValidator; - -use App\Services\PollService; -use App\Services\FollowerService; -use App\Services\ReblogService; -use App\Services\StatusService; -use App\Services\UserFilterService; -use App\Services\NetworkTimelineService; -use App\Models\Conversation; -use App\Models\RemoteReport; -use App\Jobs\ProfilePipeline\IncrementPostCount; -use App\Jobs\ProfilePipeline\DecrementPostCount; +use Cache; +use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; +use Purify; +use Storage; +use Throwable; class Inbox { - protected $headers; - protected $profile; - protected $payload; - protected $logger; - - public function __construct($headers, $profile, $payload) - { - $this->headers = $headers; - $this->profile = $profile; - $this->payload = $payload; - } - - public function handle() - { - $this->handleVerb(); - return; - } - - public function handleVerb() - { - $verb = (string) $this->payload['type']; - switch ($verb) { - - case 'Add': - $this->handleAddActivity(); - break; - - case 'Create': - $this->handleCreateActivity(); - break; - - case 'Follow': - if(FollowValidator::validate($this->payload) == false) { return; } - $this->handleFollowActivity(); - break; - - case 'Announce': - if(AnnounceValidator::validate($this->payload) == false) { return; } - $this->handleAnnounceActivity(); - break; - - case 'Accept': - if(AcceptValidator::validate($this->payload) == false) { return; } - $this->handleAcceptActivity(); - break; - - case 'Delete': - $this->handleDeleteActivity(); - break; - - case 'Like': - if(LikeValidator::validate($this->payload) == false) { return; } - $this->handleLikeActivity(); - break; - - case 'Reject': - $this->handleRejectActivity(); - break; - - case 'Undo': - $this->handleUndoActivity(); - break; - - case 'View': - $this->handleViewActivity(); - break; - - case 'Story:Reaction': - $this->handleStoryReactionActivity(); - break; - - case 'Story:Reply': - $this->handleStoryReplyActivity(); - break; - - case 'Flag': - $this->handleFlagActivity(); - break; - - case 'Update': - $this->handleUpdateActivity(); - break; - - default: - // TODO: decide how to handle invalid verbs. - break; - } - } - - public function verifyNoteAttachment() - { - $activity = $this->payload['object']; - - if(isset($activity['inReplyTo']) && - !empty($activity['inReplyTo']) && - Helpers::validateUrl($activity['inReplyTo']) - ) { - // reply detected, skip attachment check - return true; - } - - $valid = Helpers::verifyAttachments($activity); - - return $valid; - } - - public function actorFirstOrCreate($actorUrl) - { - return Helpers::profileFetch($actorUrl); - } - - public function handleAddActivity() - { - // stories ;) - - if(!isset( - $this->payload['actor'], - $this->payload['object'] - )) { - return; - } - - $actor = $this->payload['actor']; - $obj = $this->payload['object']; - - if(!Helpers::validateUrl($actor)) { - return; - } - - if(!isset($obj['type'])) { - return; - } - - switch($obj['type']) { - case 'Story': - StoryFetch::dispatch($this->payload); - break; - } - - return; - } - - public function handleCreateActivity() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { - return; - } - - if(!isset($activity['to'])) { - return; - } - $to = isset($activity['to']) ? $activity['to'] : []; - $cc = isset($activity['cc']) ? $activity['cc'] : []; - - if($activity['type'] == 'Question') { - $this->handlePollCreate(); - return; - } - - if( is_array($to) && - is_array($cc) && - count($to) == 1 && - count($cc) == 0 && - parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app') - ) { - $this->handleDirectMessage(); - return; - } - - if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) { - $this->handleNoteReply(); - - } elseif ($activity['type'] == 'Note' && !empty($activity['attachment'])) { - if(!$this->verifyNoteAttachment()) { - return; - } - $this->handleNoteCreate(); - } - return; - } - - public function handleNoteReply() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { - return; - } - - $inReplyTo = $activity['inReplyTo']; - $url = isset($activity['url']) ? $activity['url'] : $activity['id']; - - Helpers::statusFirstOrFetch($url, true); - return; - } - - public function handlePollCreate() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { - return; - } - $url = isset($activity['url']) ? $activity['url'] : $activity['id']; - Helpers::statusFirstOrFetch($url); - return; - } - - public function handleNoteCreate() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { - return; - } - - if( isset($activity['inReplyTo']) && - isset($activity['name']) && - !isset($activity['content']) && - !isset($activity['attachment']) && - Helpers::validateLocalUrl($activity['inReplyTo']) - ) { - $this->handlePollVote(); - return; - } - - if($actor->followers_count == 0) { - if(config('federation.activitypub.ingest.store_notes_without_followers')) { - } else if(FollowerService::followerCount($actor->id, true) == 0) { - return; - } - } - - $hasUrl = isset($activity['url']); - $url = isset($activity['url']) ? $activity['url'] : $activity['id']; - - if($hasUrl) { - if(Status::whereUri($url)->exists()) { - return; - } - } else { - if(Status::whereObjectUrl($url)->exists()) { - return; - } - } - - Helpers::storeStatus( - $url, - $actor, - $activity - ); - return; - } - - public function handlePollVote() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - - if(!$actor) { - return; - } - - $status = Helpers::statusFetch($activity['inReplyTo']); - - if(!$status) { - return; - } - - $poll = $status->poll; - - if(!$poll) { - return; - } - - if(now()->gt($poll->expires_at)) { - return; - } - - $choices = $poll->poll_options; - $choice = array_search($activity['name'], $choices); - - if($choice === false) { - return; - } - - if(PollVote::whereStatusId($status->id)->whereProfileId($actor->id)->exists()) { - return; - } - - $vote = new PollVote; - $vote->status_id = $status->id; - $vote->profile_id = $actor->id; - $vote->poll_id = $poll->id; - $vote->choice = $choice; - $vote->uri = isset($activity['id']) ? $activity['id'] : null; - $vote->save(); - - $tallies = $poll->cached_tallies; - $tallies[$choice] = $tallies[$choice] + 1; - $poll->cached_tallies = $tallies; - $poll->votes_count = array_sum($tallies); - $poll->save(); - - PollService::del($status->id); - - return; - } - - public function handleDirectMessage() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - $profile = Profile::whereNull('domain') - ->whereUsername(array_last(explode('/', $activity['to'][0]))) - ->firstOrFail(); - - if(in_array($actor->id, $profile->blockedIds()->toArray())) { - return; - } - - $msg = $activity['content']; - $msgText = strip_tags($activity['content']); - - if(Str::startsWith($msgText, '@' . $profile->username)) { - $len = strlen('@' . $profile->username); - $msgText = substr($msgText, $len + 1); - } - - if($profile->user->settings->public_dm == false || $profile->is_private) { - if($profile->follows($actor) == true) { - $hidden = false; - } else { - $hidden = true; - } - } else { - $hidden = false; - } - - $status = new Status; - $status->profile_id = $actor->id; - $status->caption = $msgText; - $status->rendered = $msg; - $status->visibility = 'direct'; - $status->scope = 'direct'; - $status->url = $activity['id']; - $status->in_reply_to_profile_id = $profile->id; - $status->save(); - - $dm = new DirectMessage; - $dm->to_id = $profile->id; - $dm->from_id = $actor->id; - $dm->status_id = $status->id; - $dm->is_hidden = $hidden; - $dm->type = 'text'; - $dm->save(); - - Conversation::updateOrInsert( - [ - 'to_id' => $profile->id, - 'from_id' => $actor->id - ], - [ - 'type' => 'text', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => $hidden - ] - ); - - if(count($activity['attachment'])) { - $photos = 0; - $videos = 0; - $allowed = explode(',', config_cache('pixelfed.media_types')); - $activity['attachment'] = array_slice($activity['attachment'], 0, config_cache('pixelfed.max_album_length')); - foreach($activity['attachment'] as $a) { - $type = $a['mediaType']; - $url = $a['url']; - $valid = Helpers::validateUrl($url); - if(in_array($type, $allowed) == false || $valid == false) { - continue; - } - - $media = new Media(); - $media->remote_media = true; - $media->status_id = $status->id; - $media->profile_id = $status->profile_id; - $media->user_id = null; - $media->media_path = $url; - $media->remote_url = $url; - $media->mime = $type; - $media->save(); - if(explode('/', $type)[0] == 'image') { - $photos = $photos + 1; - } - if(explode('/', $type)[0] == 'video') { - $videos = $videos + 1; - } - } - - if($photos && $videos == 0) { - $dm->type = $photos == 1 ? 'photo' : 'photos'; - $dm->save(); - } - if($videos && $photos == 0) { - $dm->type = $videos == 1 ? 'video' : 'videos'; - $dm->save(); - } - } - - if(filter_var($msgText, FILTER_VALIDATE_URL)) { - if(Helpers::validateUrl($msgText)) { - $dm->type = 'link'; - $dm->meta = [ - 'domain' => parse_url($msgText, PHP_URL_HOST), - 'local' => parse_url($msgText, PHP_URL_HOST) == - parse_url(config('app.url'), PHP_URL_HOST) - ]; - $dm->save(); - } - } - - $nf = UserFilter::whereUserId($profile->id) - ->whereFilterableId($actor->id) - ->whereFilterableType('App\Profile') - ->whereFilterType('dm.mute') - ->exists(); - - if($profile->domain == null && $hidden == false && !$nf) { - $notification = new Notification(); - $notification->profile_id = $profile->id; - $notification->actor_id = $actor->id; - $notification->action = 'dm'; - $notification->item_id = $dm->id; - $notification->item_type = "App\DirectMessage"; - $notification->save(); - } - - return; - } - - public function handleFollowActivity() - { - $actor = $this->actorFirstOrCreate($this->payload['actor']); - $target = $this->actorFirstOrCreate($this->payload['object']); - if(!$actor || !$target) { - return; - } - - if($actor->domain == null || $target->domain !== null) { - return; - } - - if( - Follower::whereProfileId($actor->id) - ->whereFollowingId($target->id) - ->exists() || - FollowRequest::whereFollowerId($actor->id) - ->whereFollowingId($target->id) - ->exists() - ) { - return; - } - - $blocks = UserFilterService::blocks($target->id); - if($blocks && in_array($actor->id, $blocks)) { - return; - } - - if($target->is_private == true) { - FollowRequest::updateOrCreate([ - 'follower_id' => $actor->id, - 'following_id' => $target->id, - ],[ - 'activity' => collect($this->payload)->only(['id','actor','object','type'])->toArray() - ]); - } else { - $follower = new Follower; - $follower->profile_id = $actor->id; - $follower->following_id = $target->id; - $follower->local_profile = empty($actor->domain); - $follower->save(); - - FollowPipeline::dispatch($follower); - FollowerService::add($actor->id, $target->id); - - // send Accept to remote profile - $accept = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $target->permalink().'#accepts/follows/' . $follower->id, - 'type' => 'Accept', - 'actor' => $target->permalink(), - 'object' => [ - 'id' => $this->payload['id'], - 'actor' => $actor->permalink(), - 'type' => 'Follow', - 'object' => $target->permalink() - ] - ]; - Helpers::sendSignedObject($target, $actor->inbox_url, $accept); - Cache::forget('profile:follower_count:'.$target->id); - Cache::forget('profile:follower_count:'.$actor->id); - Cache::forget('profile:following_count:'.$target->id); - Cache::forget('profile:following_count:'.$actor->id); - } - - return; - } - - public function handleAnnounceActivity() - { - $actor = $this->actorFirstOrCreate($this->payload['actor']); - $activity = $this->payload['object']; - - if(!$actor || $actor->domain == null) { - return; - } - - $parent = Helpers::statusFetch($activity); - - if(!$parent || empty($parent)) { - return; - } - - $blocks = UserFilterService::blocks($parent->profile_id); - if($blocks && in_array($actor->id, $blocks)) { - return; - } - - $status = Status::firstOrCreate([ - 'profile_id' => $actor->id, - 'reblog_of_id' => $parent->id, - 'type' => 'share' - ]); - - Notification::firstOrCreate( - [ - 'profile_id' => $parent->profile_id, - 'actor_id' => $actor->id, - 'action' => 'share', - 'item_id' => $parent->id, - 'item_type' => 'App\Status', - ] - ); - - $parent->reblogs_count = $parent->reblogs_count + 1; - $parent->save(); - - ReblogService::addPostReblog($parent->profile_id, $status->id); - - return; - } - - public function handleAcceptActivity() - { - $actor = $this->payload['object']['actor']; - $obj = $this->payload['object']['object']; - $type = $this->payload['object']['type']; - - if($type !== 'Follow') { - return; - } - - $actor = Helpers::validateLocalUrl($actor); - $target = Helpers::validateUrl($obj); - - if(!$actor || !$target) { - return; - } - - $actor = Helpers::profileFetch($actor); - $target = Helpers::profileFetch($target); - - if(!$actor || !$target) { - return; - } - - $request = FollowRequest::whereFollowerId($actor->id) - ->whereFollowingId($target->id) - ->whereIsRejected(false) - ->first(); - - if(!$request) { - return; - } - - $follower = Follower::firstOrCreate([ - 'profile_id' => $actor->id, - 'following_id' => $target->id, - ]); - FollowPipeline::dispatch($follower); - - $request->delete(); - - return; - } - - public function handleDeleteActivity() - { - if(!isset( - $this->payload['actor'], - $this->payload['object'] - )) { - return; - } - $actor = $this->payload['actor']; - $obj = $this->payload['object']; - if(is_string($obj) == true && $actor == $obj && Helpers::validateUrl($obj)) { - $profile = Profile::whereRemoteUrl($obj)->first(); - if(!$profile || $profile->private_key != null) { - return; - } - DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); - return; - } else { - if(!isset( - $obj['id'], - $this->payload['object'], - $this->payload['object']['id'], - $this->payload['object']['type'] - )) { - return; - } - $type = $this->payload['object']['type']; - $typeCheck = in_array($type, ['Person', 'Tombstone', 'Story']); - if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) { - return; - } - if(parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { - return; - } - $id = $this->payload['object']['id']; - switch ($type) { - case 'Person': - $profile = Profile::whereRemoteUrl($actor)->first(); - if(!$profile || $profile->private_key != null) { - return; - } - DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); - return; - break; - - case 'Tombstone': - $profile = Profile::whereRemoteUrl($actor)->first(); - if(!$profile || $profile->private_key != null) { - return; - } - $status = Status::whereProfileId($profile->id) - ->whereObjectUrl($id) - ->first(); - if(!$status) { - return; - } - RemoteStatusDelete::dispatch($status)->onQueue('high'); - return; - break; - - case 'Story': - $story = Story::whereObjectId($id) - ->first(); - if($story) { - StoryExpire::dispatch($story)->onQueue('story'); - } - return; - break; - - default: - return; - break; - } - } - return; - } - - public function handleLikeActivity() - { - $actor = $this->payload['actor']; - - if(!Helpers::validateUrl($actor)) { - return; - } - - $profile = self::actorFirstOrCreate($actor); - $obj = $this->payload['object']; - if(!Helpers::validateUrl($obj)) { - return; - } - $status = Helpers::statusFirstOrFetch($obj); - if(!$status || !$profile) { - return; - } - - $blocks = UserFilterService::blocks($status->profile_id); - if($blocks && in_array($profile->id, $blocks)) { - return; - } - - $like = Like::firstOrCreate([ - 'profile_id' => $profile->id, - 'status_id' => $status->id - ]); - - if($like->wasRecentlyCreated == true) { - $status->likes_count = $status->likes_count + 1; - $status->save(); - LikePipeline::dispatch($like); - } - - return; - } - - public function handleRejectActivity() - { - } - - public function handleUndoActivity() - { - $actor = $this->payload['actor']; - $profile = self::actorFirstOrCreate($actor); - $obj = $this->payload['object']; - - if(!$profile) { - return; - } - // TODO: Some implementations do not inline the object, skip for now - if(!$obj || !is_array($obj) || !isset($obj['type'])) { - return; - } - - switch ($obj['type']) { - case 'Accept': - break; - - case 'Announce': - if(is_array($obj) && isset($obj['object'])) { - $obj = $obj['object']; - } - if(!is_string($obj)) { - return; - } - if(Helpers::validateLocalUrl($obj)) { - $parsedId = last(explode('/', $obj)); - $status = Status::find($parsedId); - } else { - $status = Status::whereUri($obj)->first(); - } - if(!$status) { - return; - } - Status::whereProfileId($profile->id) - ->whereReblogOfId($status->id) - ->delete(); - ReblogService::removePostReblog($profile->id, $status->id); - Notification::whereProfileId($status->profile_id) - ->whereActorId($profile->id) - ->whereAction('share') - ->whereItemId($status->reblog_of_id) - ->whereItemType('App\Status') - ->forceDelete(); - break; - - case 'Block': - break; - - case 'Follow': - $following = self::actorFirstOrCreate($obj['object']); - if(!$following) { - return; - } - Follower::whereProfileId($profile->id) - ->whereFollowingId($following->id) - ->delete(); - Notification::whereProfileId($following->id) - ->whereActorId($profile->id) - ->whereAction('follow') - ->whereItemId($following->id) - ->whereItemType('App\Profile') - ->forceDelete(); - FollowerService::remove($profile->id, $following->id); - break; - - case 'Like': - $objectUri = $obj['object']; - if(!is_string($objectUri)) { - if(is_array($objectUri) && isset($objectUri['id']) && is_string($objectUri['id'])) { - $objectUri = $objectUri['id']; - } else { - return; - } - } - $status = Helpers::statusFirstOrFetch($objectUri); - if(!$status) { - return; - } - Like::whereProfileId($profile->id) - ->whereStatusId($status->id) - ->forceDelete(); - Notification::whereProfileId($status->profile_id) - ->whereActorId($profile->id) - ->whereAction('like') - ->whereItemId($status->id) - ->whereItemType('App\Status') - ->forceDelete(); - break; - } - return; - } - - public function handleViewActivity() - { - if(!isset( - $this->payload['actor'], - $this->payload['object'] - )) { - return; - } - - $actor = $this->payload['actor']; - $obj = $this->payload['object']; - - if(!Helpers::validateUrl($actor)) { - return; - } - - if(!$obj || !is_array($obj)) { - return; - } - - if(!isset($obj['type']) || !isset($obj['object']) || $obj['type'] != 'Story') { - return; - } - - if(!Helpers::validateLocalUrl($obj['object'])) { - return; - } - - $profile = Helpers::profileFetch($actor); - $storyId = Str::of($obj['object'])->explode('/')->last(); - - $story = Story::whereActive(true) - ->whereLocal(true) - ->find($storyId); - - if(!$story) { - return; - } - - if(!FollowerService::follows($profile->id, $story->profile_id)) { - return; - } - - $view = StoryView::firstOrCreate([ - 'story_id' => $story->id, - 'profile_id' => $profile->id - ]); - - if($view->wasRecentlyCreated == true) { - $story->view_count++; - $story->save(); - } - - return; - } - - public function handleStoryReactionActivity() - { - if(!isset( - $this->payload['actor'], - $this->payload['id'], - $this->payload['inReplyTo'], - $this->payload['content'] - )) { - return; - } - - $id = $this->payload['id']; - $actor = $this->payload['actor']; - $storyUrl = $this->payload['inReplyTo']; - $to = $this->payload['to']; - $text = Purify::clean($this->payload['content']); - - if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { - return; - } - - if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) { - return; - } - - if(!Helpers::validateLocalUrl($storyUrl)) { - return; - } - - if(!Helpers::validateLocalUrl($to)) { - return; - } - - if(Status::whereObjectUrl($id)->exists()) { - return; - } - - $storyId = Str::of($storyUrl)->explode('/')->last(); - $targetProfile = Helpers::profileFetch($to); - - $story = Story::whereProfileId($targetProfile->id) - ->find($storyId); - - if(!$story) { - return; - } - - if($story->can_react == false) { - return; - } - - $actorProfile = Helpers::profileFetch($actor); - - if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) { - return; - } - - $status = new Status; - $status->profile_id = $actorProfile->id; - $status->type = 'story:reaction'; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id, - 'reaction' => $text - ]); - $status->save(); - - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $actorProfile->id; - $dm->type = 'story:react'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $targetProfile->username, - 'story_actor_username' => $actorProfile->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'reaction' => $text - ]); - $dm->save(); - - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $actorProfile->id - ], - [ - 'type' => 'story:react', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); - - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:react'; - $n->save(); - - return; - } - - public function handleStoryReplyActivity() - { - if(!isset( - $this->payload['actor'], - $this->payload['id'], - $this->payload['inReplyTo'], - $this->payload['content'] - )) { - return; - } - - $id = $this->payload['id']; - $actor = $this->payload['actor']; - $storyUrl = $this->payload['inReplyTo']; - $to = $this->payload['to']; - $text = Purify::clean($this->payload['content']); - - if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { - return; - } - - if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) { - return; - } - - if(!Helpers::validateLocalUrl($storyUrl)) { - return; - } - - if(!Helpers::validateLocalUrl($to)) { - return; - } - - if(Status::whereObjectUrl($id)->exists()) { - return; - } - - $storyId = Str::of($storyUrl)->explode('/')->last(); - $targetProfile = Helpers::profileFetch($to); - - $story = Story::whereProfileId($targetProfile->id) - ->find($storyId); - - if(!$story) { - return; - } - - if($story->can_react == false) { - return; - } - - $actorProfile = Helpers::profileFetch($actor); - - if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) { - return; - } - - $status = new Status; - $status->profile_id = $actorProfile->id; - $status->type = 'story:reply'; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id, - 'caption' => $text - ]); - $status->save(); - - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $actorProfile->id; - $dm->type = 'story:comment'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $targetProfile->username, - 'story_actor_username' => $actorProfile->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'caption' => $text - ]); - $dm->save(); - - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $actorProfile->id - ], - [ - 'type' => 'story:comment', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); - - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:comment'; - $n->save(); - - return; - } - - public function handleFlagActivity() - { - if(!isset( - $this->payload['id'], - $this->payload['type'], - $this->payload['actor'], - $this->payload['object'] - )) { - return; - } - - $id = $this->payload['id']; - $actor = $this->payload['actor']; - - if(Helpers::validateLocalUrl($id) || parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { - return; - } - - $content = isset($this->payload['content']) ? Purify::clean($this->payload['content']) : null; - $object = $this->payload['object']; - - if(empty($object) || (!is_array($object) && !is_string($object))) { - return; - } - - if(is_array($object) && count($object) > 100) { - return; - } - - $objects = collect([]); - $accountId = null; - - foreach($object as $objectUrl) { - if(!Helpers::validateLocalUrl($objectUrl)) { - continue; - } - - if(str_contains($objectUrl, '/users/')) { - $username = last(explode('/', $objectUrl)); - $profileId = Profile::whereUsername($username)->first(); - if($profileId) { - $accountId = $profileId->id; - } - } else if(str_contains($objectUrl, '/p/')) { - $postId = last(explode('/', $objectUrl)); - $objects->push($postId); - } else { - continue; - } - } - - if(!$accountId || !$objects->count()) { - return; - } - - $instanceHost = parse_url($id, PHP_URL_HOST); - - $instance = Instance::updateOrCreate([ - 'domain' => $instanceHost - ]); - - $report = new RemoteReport; - $report->status_ids = $objects->toArray(); - $report->comment = $content; - $report->account_id = $accountId; - $report->uri = $id; - $report->instance_id = $instance->id; - $report->report_meta = [ - 'actor' => $actor, - 'object' => $object - ]; - $report->save(); - - return; - } - - public function handleUpdateActivity() - { - $activity = $this->payload['object']; - - if(!isset($activity['type'], $activity['id'])) { - return; - } - - if(!Helpers::validateUrl($activity['id'])) { - return; - } - - if($activity['type'] === 'Note') { - if(Status::whereObjectUrl($activity['id'])->exists()) { - StatusRemoteUpdatePipeline::dispatch($activity); - } - } else if ($activity['type'] === 'Person') { - if(UpdatePersonValidator::validate($this->payload)) { - HandleUpdateActivity::dispatch($this->payload)->onQueue('low'); - } - } - } + protected $headers; + + protected $profile; + + protected $payload; + + protected $logger; + + public function __construct($headers, $profile, $payload) + { + $this->headers = $headers; + $this->profile = $profile; + $this->payload = $payload; + } + + public function handle() + { + $this->handleVerb(); + + } + + public function handleVerb() + { + $verb = (string) $this->payload['type']; + switch ($verb) { + + case 'Add': + $this->handleAddActivity(); + break; + + case 'Create': + $this->handleCreateActivity(); + break; + + case 'Follow': + if (FollowValidator::validate($this->payload) == false) { + return; + } + $this->handleFollowActivity(); + break; + + case 'Announce': + if (AnnounceValidator::validate($this->payload) == false) { + return; + } + $this->handleAnnounceActivity(); + break; + + case 'Accept': + if (AcceptValidator::validate($this->payload) == false) { + return; + } + $this->handleAcceptActivity(); + break; + + case 'Delete': + $this->handleDeleteActivity(); + break; + + case 'Like': + if (LikeValidator::validate($this->payload) == false) { + return; + } + $this->handleLikeActivity(); + break; + + case 'Reject': + $this->handleRejectActivity(); + break; + + case 'Undo': + $this->handleUndoActivity(); + break; + + case 'View': + $this->handleViewActivity(); + break; + + case 'Story:Reaction': + $this->handleStoryReactionActivity(); + break; + + case 'Story:Reply': + $this->handleStoryReplyActivity(); + break; + + case 'Flag': + $this->handleFlagActivity(); + break; + + case 'Update': + $this->handleUpdateActivity(); + break; + + case 'Move': + if (MoveValidator::validate($this->payload) == false) { + Log::info('[AP][INBOX][MOVE] VALIDATE_FAILURE '.json_encode($this->payload)); + + return; + } + $this->handleMoveActivity(); + break; + + default: + // TODO: decide how to handle invalid verbs. + break; + } + } + + public function verifyNoteAttachment() + { + $activity = $this->payload['object']; + + if (isset($activity['inReplyTo']) && + ! empty($activity['inReplyTo']) && + Helpers::validateUrl($activity['inReplyTo']) + ) { + // reply detected, skip attachment check + return true; + } + + $valid = Helpers::verifyAttachments($activity); + + return $valid; + } + + public function actorFirstOrCreate($actorUrl) + { + return Helpers::profileFetch($actorUrl); + } + + public function handleAddActivity() + { + // stories ;) + + if (! isset( + $this->payload['actor'], + $this->payload['object'] + )) { + return; + } + + $actor = $this->payload['actor']; + $obj = $this->payload['object']; + + if (! Helpers::validateUrl($actor)) { + return; + } + + if (! isset($obj['type'])) { + return; + } + + switch ($obj['type']) { + case 'Story': + StoryFetch::dispatch($this->payload); + break; + } + + } + + public function handleCreateActivity() + { + $activity = $this->payload['object']; + if (config('autospam.live_filters.enabled')) { + $filters = config('autospam.live_filters.filters'); + if (! empty($filters) && isset($activity['content']) && ! empty($activity['content']) && strlen($filters) > 3) { + $filters = array_map('trim', explode(',', $filters)); + $content = $activity['content']; + foreach ($filters as $filter) { + $filter = trim(strtolower($filter)); + if (! $filter || ! strlen($filter)) { + continue; + } + if (str_contains(strtolower($content), $filter)) { + return; + } + } + } + } + $actor = $this->actorFirstOrCreate($this->payload['actor']); + if (! $actor || $actor->domain == null) { + return; + } + + if (! isset($activity['to'])) { + return; + } + $to = isset($activity['to']) ? $activity['to'] : []; + $cc = isset($activity['cc']) ? $activity['cc'] : []; + + if ($activity['type'] == 'Question') { + //$this->handlePollCreate(); + + return; + } + + if (is_array($to) && + is_array($cc) && + count($to) == 1 && + count($cc) == 0 && + parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app') + ) { + $this->handleDirectMessage(); + + return; + } + + if ($activity['type'] == 'Note' && ! empty($activity['inReplyTo'])) { + $this->handleNoteReply(); + + } elseif ($activity['type'] == 'Note' && ! empty($activity['attachment'])) { + if (! $this->verifyNoteAttachment()) { + return; + } + $this->handleNoteCreate(); + } + + } + + public function handleNoteReply() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + if (! $actor || $actor->domain == null) { + return; + } + + $inReplyTo = $activity['inReplyTo']; + $url = isset($activity['url']) ? $activity['url'] : $activity['id']; + + Helpers::statusFirstOrFetch($url, true); + + } + + public function handlePollCreate() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + if (! $actor || $actor->domain == null) { + return; + } + $url = isset($activity['url']) ? $activity['url'] : $activity['id']; + Helpers::statusFirstOrFetch($url); + + } + + public function handleNoteCreate() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + if (! $actor || $actor->domain == null) { + return; + } + + if (isset($activity['inReplyTo']) && + isset($activity['name']) && + ! isset($activity['content']) && + ! isset($activity['attachment']) && + Helpers::validateLocalUrl($activity['inReplyTo']) + ) { + $this->handlePollVote(); + + return; + } + + if ($actor->followers_count == 0) { + if (config('federation.activitypub.ingest.store_notes_without_followers')) { + } elseif (FollowerService::followerCount($actor->id, true) == 0) { + return; + } + } + + $hasUrl = isset($activity['url']); + $url = isset($activity['url']) ? $activity['url'] : $activity['id']; + + if ($hasUrl) { + if (Status::whereUri($url)->exists()) { + return; + } + } else { + if (Status::whereObjectUrl($url)->exists()) { + return; + } + } + + Helpers::storeStatus( + $url, + $actor, + $activity + ); + + } + + public function handlePollVote() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + + if (! $actor) { + return; + } + + $status = Helpers::statusFetch($activity['inReplyTo']); + + if (! $status) { + return; + } + + $poll = $status->poll; + + if (! $poll) { + return; + } + + if (now()->gt($poll->expires_at)) { + return; + } + + $choices = $poll->poll_options; + $choice = array_search($activity['name'], $choices); + + if ($choice === false) { + return; + } + + if (PollVote::whereStatusId($status->id)->whereProfileId($actor->id)->exists()) { + return; + } + + $vote = new PollVote; + $vote->status_id = $status->id; + $vote->profile_id = $actor->id; + $vote->poll_id = $poll->id; + $vote->choice = $choice; + $vote->uri = isset($activity['id']) ? $activity['id'] : null; + $vote->save(); + + $tallies = $poll->cached_tallies; + $tallies[$choice] = $tallies[$choice] + 1; + $poll->cached_tallies = $tallies; + $poll->votes_count = array_sum($tallies); + $poll->save(); + + PollService::del($status->id); + + } + + public function handleDirectMessage() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + $profile = Profile::whereNull('domain') + ->whereUsername(array_last(explode('/', $activity['to'][0]))) + ->firstOrFail(); + + if (! $actor || in_array($actor->id, $profile->blockedIds()->toArray())) { + return; + } + + if (AccountService::blocksDomain($profile->id, $actor->domain) == true) { + return; + } + + $msg = Purify::clean($activity['content']); + $msgText = strip_tags($msg); + + if (Str::startsWith($msgText, '@'.$profile->username)) { + $len = strlen('@'.$profile->username); + $msgText = substr($msgText, $len + 1); + } + + if ($profile->user->settings->public_dm == false || $profile->is_private) { + if ($profile->follows($actor) == true) { + $hidden = false; + } else { + $hidden = true; + } + } else { + $hidden = false; + } + + $status = new Status; + $status->profile_id = $actor->id; + $status->caption = $msgText; + $status->visibility = 'direct'; + $status->scope = 'direct'; + $status->url = $activity['id']; + $status->uri = $activity['id']; + $status->object_url = $activity['id']; + $status->in_reply_to_profile_id = $profile->id; + $status->save(); + + $dm = new DirectMessage; + $dm->to_id = $profile->id; + $dm->from_id = $actor->id; + $dm->status_id = $status->id; + $dm->is_hidden = $hidden; + $dm->type = 'text'; + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $profile->id, + 'from_id' => $actor->id, + ], + [ + 'type' => 'text', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => $hidden, + ] + ); + + if (count($activity['attachment'])) { + $photos = 0; + $videos = 0; + $allowed = explode(',', config_cache('pixelfed.media_types')); + $activity['attachment'] = array_slice($activity['attachment'], 0, config_cache('pixelfed.max_album_length')); + foreach ($activity['attachment'] as $a) { + $type = $a['mediaType']; + $url = $a['url']; + $valid = Helpers::validateUrl($url); + if (in_array($type, $allowed) == false || $valid == false) { + continue; + } + + $media = new Media; + $media->remote_media = true; + $media->status_id = $status->id; + $media->profile_id = $status->profile_id; + $media->user_id = null; + $media->media_path = $url; + $media->remote_url = $url; + $media->mime = $type; + $media->save(); + if (explode('/', $type)[0] == 'image') { + $photos = $photos + 1; + } + if (explode('/', $type)[0] == 'video') { + $videos = $videos + 1; + } + } + + if ($photos && $videos == 0) { + $dm->type = $photos == 1 ? 'photo' : 'photos'; + $dm->save(); + } + if ($videos && $photos == 0) { + $dm->type = $videos == 1 ? 'video' : 'videos'; + $dm->save(); + } + } + + if (filter_var($msgText, FILTER_VALIDATE_URL)) { + if (Helpers::validateUrl($msgText)) { + $dm->type = 'link'; + $dm->meta = [ + 'domain' => parse_url($msgText, PHP_URL_HOST), + 'local' => parse_url($msgText, PHP_URL_HOST) == + parse_url(config('app.url'), PHP_URL_HOST), + ]; + $dm->save(); + } + } + + $nf = UserFilter::whereUserId($profile->id) + ->whereFilterableId($actor->id) + ->whereFilterableType('App\Profile') + ->whereFilterType('dm.mute') + ->exists(); + + if ($profile->domain == null && $hidden == false && ! $nf) { + $notification = new Notification; + $notification->profile_id = $profile->id; + $notification->actor_id = $actor->id; + $notification->action = 'dm'; + $notification->item_id = $dm->id; + $notification->item_type = "App\DirectMessage"; + $notification->save(); + + if (NotificationAppGatewayService::enabled()) { + if (PushNotificationService::check('mention', $profile->id)) { + $user = User::whereProfileId($profile->id)->first(); + if ($user && $user->expo_token && $user->notify_enabled) { + MentionPushNotifyPipeline::dispatch($user->expo_token, $actor->username)->onQueue('pushnotify'); + } + } + } + } + + } + + public function handleFollowActivity() + { + $actor = $this->actorFirstOrCreate($this->payload['actor']); + $target = $this->actorFirstOrCreate($this->payload['object']); + if (! $actor || ! $target) { + return; + } + + if ($actor->domain == null || $target->domain !== null) { + return; + } + + if (AccountService::blocksDomain($target->id, $actor->domain) == true) { + return; + } + + if ( + Follower::whereProfileId($actor->id) + ->whereFollowingId($target->id) + ->exists() || + FollowRequest::whereFollowerId($actor->id) + ->whereFollowingId($target->id) + ->exists() + ) { + return; + } + + $blocks = UserFilterService::blocks($target->id); + if ($blocks && in_array($actor->id, $blocks)) { + return; + } + + if ($target->is_private == true) { + FollowRequest::updateOrCreate([ + 'follower_id' => $actor->id, + 'following_id' => $target->id, + ], [ + 'activity' => collect($this->payload)->only(['id', 'actor', 'object', 'type'])->toArray(), + ]); + } else { + $follower = new Follower; + $follower->profile_id = $actor->id; + $follower->following_id = $target->id; + $follower->local_profile = empty($actor->domain); + $follower->save(); + + FollowPipeline::dispatch($follower); + FollowerService::add($actor->id, $target->id); + + // send Accept to remote profile + $accept = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $target->permalink().'#accepts/follows/'.$follower->id, + 'type' => 'Accept', + 'actor' => $target->permalink(), + 'object' => [ + 'id' => $this->payload['id'], + 'actor' => $actor->permalink(), + 'type' => 'Follow', + 'object' => $target->permalink(), + ], + ]; + Helpers::sendSignedObject($target, $actor->inbox_url, $accept); + Cache::forget('profile:follower_count:'.$target->id); + Cache::forget('profile:follower_count:'.$actor->id); + Cache::forget('profile:following_count:'.$target->id); + Cache::forget('profile:following_count:'.$actor->id); + } + + } + + public function handleAnnounceActivity() + { + $actor = $this->actorFirstOrCreate($this->payload['actor']); + $activity = $this->payload['object']; + + if (! $actor || $actor->domain == null) { + return; + } + + $parent = Helpers::statusFetch($activity); + + if (! $parent || empty($parent)) { + return; + } + + if (AccountService::blocksDomain($parent->profile_id, $actor->domain) == true) { + return; + } + + $blocks = UserFilterService::blocks($parent->profile_id); + if ($blocks && in_array($actor->id, $blocks)) { + return; + } + + $status = Status::firstOrCreate([ + 'profile_id' => $actor->id, + 'reblog_of_id' => $parent->id, + 'type' => 'share', + ]); + + Notification::firstOrCreate( + [ + 'profile_id' => $parent->profile_id, + 'actor_id' => $actor->id, + 'action' => 'share', + 'item_id' => $parent->id, + 'item_type' => 'App\Status', + ] + ); + + $parent->reblogs_count = $parent->reblogs_count + 1; + $parent->save(); + + ReblogService::addPostReblog($parent->profile_id, $status->id); + + } + + public function handleAcceptActivity() + { + $actor = $this->payload['object']['actor']; + $obj = $this->payload['object']['object']; + $type = $this->payload['object']['type']; + + if ($type !== 'Follow') { + return; + } + + $actor = Helpers::validateLocalUrl($actor); + $target = Helpers::validateUrl($obj); + + if (! $actor || ! $target) { + return; + } + + $actor = Helpers::profileFetch($actor); + $target = Helpers::profileFetch($target); + + if (! $actor || ! $target) { + return; + } + + if (AccountService::blocksDomain($target->id, $actor->domain) == true) { + return; + } + + $request = FollowRequest::whereFollowerId($actor->id) + ->whereFollowingId($target->id) + ->whereIsRejected(false) + ->first(); + + if (! $request) { + return; + } + + $follower = Follower::firstOrCreate([ + 'profile_id' => $actor->id, + 'following_id' => $target->id, + ]); + FollowPipeline::dispatch($follower); + + $request->delete(); + + } + + public function handleDeleteActivity() + { + if (! isset( + $this->payload['actor'], + $this->payload['object'] + )) { + return; + } + $actor = $this->payload['actor']; + $obj = $this->payload['object']; + if (is_string($obj) == true && $actor == $obj && Helpers::validateUrl($obj)) { + $profile = Profile::whereRemoteUrl($obj)->first(); + if (! $profile || $profile->private_key != null) { + return; + } + DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); + + return; + } else { + if (! isset( + $obj['id'], + $this->payload['object'], + $this->payload['object']['id'], + $this->payload['object']['type'] + )) { + return; + } + $type = $this->payload['object']['type']; + $typeCheck = in_array($type, ['Person', 'Tombstone', 'Story']); + if (! Helpers::validateUrl($actor) || ! Helpers::validateUrl($obj['id']) || ! $typeCheck) { + return; + } + if (parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + return; + } + $id = $this->payload['object']['id']; + switch ($type) { + case 'Person': + $profile = Profile::whereRemoteUrl($actor)->first(); + if (! $profile || $profile->private_key != null) { + return; + } + DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); + + return; + break; + + case 'Tombstone': + $profile = Profile::whereRemoteUrl($actor)->first(); + if (! $profile || $profile->private_key != null) { + return; + } + + $status = Status::where('object_url', $id)->first(); + if (! $status) { + $status = Status::where('url', $id)->first(); + if (! $status) { + return; + } + } + if ($status->profile_id != $profile->id) { + return; + } + if ($status->scope && in_array($status->scope, ['public', 'unlisted', 'private'])) { + if ($status->type && ! in_array($status->type, ['story:reaction', 'story:reply', 'reply'])) { + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } + } + RemoteStatusDelete::dispatch($status)->onQueue('high'); + + return; + break; + + case 'Story': + $story = Story::whereObjectId($id) + ->first(); + if ($story) { + StoryExpire::dispatch($story)->onQueue('story'); + } + + return; + break; + + default: + return; + break; + } + } + + } + + public function handleLikeActivity() + { + $actor = $this->payload['actor']; + + if (! Helpers::validateUrl($actor)) { + return; + } + + $profile = self::actorFirstOrCreate($actor); + $obj = $this->payload['object']; + if (! Helpers::validateUrl($obj)) { + return; + } + $status = Helpers::statusFirstOrFetch($obj); + if (! $status || ! $profile) { + return; + } + + if (AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { + return; + } + + $blocks = UserFilterService::blocks($status->profile_id); + if ($blocks && in_array($profile->id, $blocks)) { + return; + } + + $like = Like::firstOrCreate([ + 'profile_id' => $profile->id, + 'status_id' => $status->id, + ]); + + if ($like->wasRecentlyCreated == true) { + $status->likes_count = $status->likes_count + 1; + $status->save(); + LikePipeline::dispatch($like); + } + + } + + public function handleRejectActivity() {} + + public function handleUndoActivity() + { + $actor = $this->payload['actor']; + $profile = self::actorFirstOrCreate($actor); + $obj = $this->payload['object']; + + if (! $profile) { + return; + } + // TODO: Some implementations do not inline the object, skip for now + if (! $obj || ! is_array($obj) || ! isset($obj['type'])) { + return; + } + + switch ($obj['type']) { + case 'Accept': + break; + + case 'Announce': + if (is_array($obj) && isset($obj['object'])) { + $obj = $obj['object']; + } + if (! is_string($obj)) { + return; + } + if (Helpers::validateLocalUrl($obj)) { + $parsedId = last(explode('/', $obj)); + $status = Status::find($parsedId); + } else { + $status = Status::whereUri($obj)->first(); + } + if (! $status) { + return; + } + if (AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { + return; + } + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + Status::whereProfileId($profile->id) + ->whereReblogOfId($status->id) + ->delete(); + ReblogService::removePostReblog($profile->id, $status->id); + Notification::whereProfileId($status->profile_id) + ->whereActorId($profile->id) + ->whereAction('share') + ->whereItemId($status->reblog_of_id) + ->whereItemType('App\Status') + ->forceDelete(); + break; + + case 'Block': + break; + + case 'Follow': + $following = self::actorFirstOrCreate($obj['object']); + if (! $following) { + return; + } + if (AccountService::blocksDomain($following->id, $profile->domain) == true) { + return; + } + Follower::whereProfileId($profile->id) + ->whereFollowingId($following->id) + ->delete(); + Notification::whereProfileId($following->id) + ->whereActorId($profile->id) + ->whereAction('follow') + ->whereItemId($following->id) + ->whereItemType('App\Profile') + ->forceDelete(); + FollowerService::remove($profile->id, $following->id); + break; + + case 'Like': + $objectUri = $obj['object']; + if (! is_string($objectUri)) { + if (is_array($objectUri) && isset($objectUri['id']) && is_string($objectUri['id'])) { + $objectUri = $objectUri['id']; + } else { + return; + } + } + $status = Helpers::statusFirstOrFetch($objectUri); + if (! $status) { + return; + } + if (AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { + return; + } + Like::whereProfileId($profile->id) + ->whereStatusId($status->id) + ->forceDelete(); + Notification::whereProfileId($status->profile_id) + ->whereActorId($profile->id) + ->whereAction('like') + ->whereItemId($status->id) + ->whereItemType('App\Status') + ->forceDelete(); + break; + } + + } + + public function handleViewActivity() + { + if (! isset( + $this->payload['actor'], + $this->payload['object'] + )) { + return; + } + + $actor = $this->payload['actor']; + $obj = $this->payload['object']; + + if (! Helpers::validateUrl($actor)) { + return; + } + + if (! $obj || ! is_array($obj)) { + return; + } + + if (! isset($obj['type']) || ! isset($obj['object']) || $obj['type'] != 'Story') { + return; + } + + if (! Helpers::validateLocalUrl($obj['object'])) { + return; + } + + $profile = Helpers::profileFetch($actor); + $storyId = Str::of($obj['object'])->explode('/')->last(); + + $story = Story::whereActive(true) + ->whereLocal(true) + ->find($storyId); + + if (! $story) { + return; + } + + if (AccountService::blocksDomain($story->profile_id, $profile->domain) == true) { + return; + } + + if (! FollowerService::follows($profile->id, $story->profile_id)) { + return; + } + + $view = StoryView::firstOrCreate([ + 'story_id' => $story->id, + 'profile_id' => $profile->id, + ]); + + if ($view->wasRecentlyCreated == true) { + $story->view_count++; + $story->save(); + } + + } + + public function handleStoryReactionActivity() + { + if (! isset( + $this->payload['actor'], + $this->payload['id'], + $this->payload['inReplyTo'], + $this->payload['content'] + )) { + return; + } + + $id = $this->payload['id']; + $actor = $this->payload['actor']; + $storyUrl = $this->payload['inReplyTo']; + $to = $this->payload['to']; + $text = Purify::clean($this->payload['content']); + + if (parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + return; + } + + if (! Helpers::validateUrl($id) || ! Helpers::validateUrl($actor)) { + return; + } + + if (! Helpers::validateLocalUrl($storyUrl)) { + return; + } + + if (! Helpers::validateLocalUrl($to)) { + return; + } + + if (Status::whereObjectUrl($id)->exists()) { + return; + } + + $storyId = Str::of($storyUrl)->explode('/')->last(); + $targetProfile = Helpers::profileFetch($to); + + $story = Story::whereProfileId($targetProfile->id) + ->find($storyId); + + if (! $story) { + return; + } + + if ($story->can_react == false) { + return; + } + + $actorProfile = Helpers::profileFetch($actor); + + if (AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) { + return; + } + + if (! FollowerService::follows($actorProfile->id, $targetProfile->id)) { + return; + } + + $url = $id; + + if (str_ends_with($url, '/activity')) { + $url = substr($url, 0, -9); + } + + $status = new Status; + $status->profile_id = $actorProfile->id; + $status->type = 'story:reaction'; + $status->url = $url; + $status->uri = $url; + $status->object_url = $url; + $status->caption = $text; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id, + 'reaction' => $text, + ]); + $status->save(); + + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $actorProfile->id; + $dm->type = 'story:react'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $targetProfile->username, + 'story_actor_username' => $actorProfile->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'reaction' => $text, + ]); + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $actorProfile->id, + ], + [ + 'type' => 'story:react', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false, + ] + ); + + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:react'; + $n->save(); + + } + + public function handleStoryReplyActivity() + { + if (! isset( + $this->payload['actor'], + $this->payload['id'], + $this->payload['inReplyTo'], + $this->payload['content'] + )) { + return; + } + + $id = $this->payload['id']; + $actor = $this->payload['actor']; + $storyUrl = $this->payload['inReplyTo']; + $to = $this->payload['to']; + $text = Purify::clean($this->payload['content']); + + if (parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + return; + } + + if (! Helpers::validateUrl($id) || ! Helpers::validateUrl($actor)) { + return; + } + + if (! Helpers::validateLocalUrl($storyUrl)) { + return; + } + + if (! Helpers::validateLocalUrl($to)) { + return; + } + + if (Status::whereObjectUrl($id)->exists()) { + return; + } + + $storyId = Str::of($storyUrl)->explode('/')->last(); + $targetProfile = Helpers::profileFetch($to); + + $story = Story::whereProfileId($targetProfile->id) + ->find($storyId); + + if (! $story) { + return; + } + + if ($story->can_react == false) { + return; + } + + $actorProfile = Helpers::profileFetch($actor); + + if (AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) { + return; + } + + if (! FollowerService::follows($actorProfile->id, $targetProfile->id)) { + return; + } + + $url = $id; + + if (str_ends_with($url, '/activity')) { + $url = substr($url, 0, -9); + } + + $status = new Status; + $status->profile_id = $actorProfile->id; + $status->type = 'story:reply'; + $status->caption = $text; + $status->url = $url; + $status->uri = $url; + $status->object_url = $url; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id, + 'caption' => $text, + ]); + $status->save(); + + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $actorProfile->id; + $dm->type = 'story:comment'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $targetProfile->username, + 'story_actor_username' => $actorProfile->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'caption' => $text, + ]); + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $actorProfile->id, + ], + [ + 'type' => 'story:comment', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false, + ] + ); + + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:comment'; + $n->save(); + + } + + public function handleFlagActivity() + { + if (! isset( + $this->payload['id'], + $this->payload['type'], + $this->payload['actor'], + $this->payload['object'] + )) { + return; + } + + $id = $this->payload['id']; + $actor = $this->payload['actor']; + + if (Helpers::validateLocalUrl($id) || parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + return; + } + + $content = null; + if (isset($this->payload['content'])) { + if (strlen($this->payload['content']) > 5000) { + $content = Purify::clean(substr($this->payload['content'], 0, 5000).' ... (truncated message due to exceeding max length)'); + } else { + $content = Purify::clean($this->payload['content']); + } + } + $object = $this->payload['object']; + + if (empty($object) || (! is_array($object) && ! is_string($object))) { + return; + } + + if (is_array($object) && count($object) > 100) { + return; + } + + $objects = collect([]); + $accountId = null; + + foreach ($object as $objectUrl) { + if (! Helpers::validateLocalUrl($objectUrl)) { + return; + } + + if (str_contains($objectUrl, '/users/')) { + $username = last(explode('/', $objectUrl)); + $profileId = Profile::whereUsername($username)->first(); + if ($profileId) { + $accountId = $profileId->id; + } + } elseif (str_contains($objectUrl, '/p/')) { + $postId = last(explode('/', $objectUrl)); + $objects->push($postId); + } else { + continue; + } + } + + if (! $accountId && ! $objects->count()) { + return; + } + + if ($objects->count()) { + $obc = $objects->count(); + if ($obc > 25) { + if ($obc > 30) { + return; + } else { + $objLimit = $objects->take(20); + $objects = collect($objLimit->all()); + $obc = $objects->count(); + } + } + $count = Status::whereProfileId($accountId)->find($objects)->count(); + if ($obc !== $count) { + return; + } + } + + $instanceHost = parse_url($id, PHP_URL_HOST); + + $instance = Instance::updateOrCreate([ + 'domain' => $instanceHost, + ]); + + $report = new RemoteReport; + $report->status_ids = $objects->toArray(); + $report->comment = $content; + $report->account_id = $accountId; + $report->uri = $id; + $report->instance_id = $instance->id; + $report->report_meta = [ + 'actor' => $actor, + 'object' => $object, + ]; + $report->save(); + + } + + public function handleUpdateActivity() + { + $activity = $this->payload['object']; + + if (! isset($activity['type'], $activity['id'])) { + return; + } + + if (! Helpers::validateUrl($activity['id'])) { + return; + } + + if ($activity['type'] === 'Note') { + if (Status::whereObjectUrl($activity['id'])->exists()) { + StatusRemoteUpdatePipeline::dispatch($activity); + } + } elseif ($activity['type'] === 'Person') { + if (UpdatePersonValidator::validate($this->payload)) { + HandleUpdateActivity::dispatch($this->payload)->onQueue('low'); + } + } + } + + public function handleMoveActivity() + { + $actor = $this->payload['actor']; + $activity = $this->payload['object']; + $target = $this->payload['target']; + if ( + ! Helpers::validateUrl($actor) || + ! Helpers::validateUrl($activity) || + ! Helpers::validateUrl($target) + ) { + return; + } + + Bus::chain([ + new ProcessMovePipeline($target, $activity), + new MoveMigrateFollowersPipeline($target, $activity), + new UnfollowLegacyAccountMovePipeline($target, $activity), + new CleanupLegacyAccountMovePipeline($target, $activity), + ]) + ->catch(function (Throwable $e) { + Log::error($e); + }) + ->onQueue('move') + ->delay(now()->addMinutes(random_int(1, 3))) + ->dispatch(); + } } diff --git a/app/Util/ActivityPub/Outbox.php b/app/Util/ActivityPub/Outbox.php index 43adb36e3..aba34955e 100644 --- a/app/Util/ActivityPub/Outbox.php +++ b/app/Util/ActivityPub/Outbox.php @@ -2,34 +2,32 @@ namespace App\Util\ActivityPub; -use App\Profile; -use App\Status; -use League\Fractal; use App\Http\Controllers\ProfileController; -use App\Transformer\ActivityPub\ProfileOutbox; +use App\Status; use App\Transformer\ActivityPub\Verb\CreateNote; +use League\Fractal; -class Outbox { +class Outbox +{ + public static function get($profile) + { + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); + abort_if(! config('federation.activitypub.outbox'), 404); - public static function get($profile) - { - abort_if(!config_cache('federation.activitypub.enabled'), 404); - abort_if(!config('federation.activitypub.outbox'), 404); - - if($profile->status != null) { + if ($profile->status != null) { return ProfileController::accountCheck($profile); } - if($profile->is_private) { - return ['error'=>'403', 'msg' => 'private profile']; + if ($profile->is_private) { + return ['error' => '403', 'msg' => 'private profile']; } $timeline = $profile - ->statuses() - ->whereScope('public') - ->orderBy('created_at', 'desc') - ->take(10) - ->get(); + ->statuses() + ->whereScope('public') + ->orderBy('created_at', 'desc') + ->take(10) + ->get(); $count = Status::whereProfileId($profile->id)->count(); @@ -38,14 +36,14 @@ class Outbox { $res = $fractal->createData($resource)->toArray(); $outbox = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - '_debug' => 'Outbox only supports latest 10 objects, pagination is not supported', - 'id' => $profile->permalink('/outbox'), - 'type' => 'OrderedCollection', - 'totalItems' => $count, - 'orderedItems' => $res['data'] + '@context' => 'https://www.w3.org/ns/activitystreams', + '_debug' => 'Outbox only supports latest 10 objects, pagination is not supported', + 'id' => $profile->permalink('/outbox'), + 'type' => 'OrderedCollection', + 'totalItems' => $count, + 'orderedItems' => $res['data'], ]; - return $outbox; - } + return $outbox; + } } diff --git a/app/Util/ActivityPub/Validator/MoveValidator.php b/app/Util/ActivityPub/Validator/MoveValidator.php new file mode 100644 index 000000000..a7ca43297 --- /dev/null +++ b/app/Util/ActivityPub/Validator/MoveValidator.php @@ -0,0 +1,23 @@ + 'required', + 'type' => [ + 'required', + Rule::in(['Move']), + ], + 'actor' => 'required|url', + 'object' => 'required|url', + 'target' => 'required|url', + ])->passes(); + } +} diff --git a/app/Util/Lexer/RestrictedNames.php b/app/Util/Lexer/RestrictedNames.php index 4224ae96c..0974f2a9c 100644 --- a/app/Util/Lexer/RestrictedNames.php +++ b/app/Util/Lexer/RestrictedNames.php @@ -4,376 +4,381 @@ namespace App\Util\Lexer; class RestrictedNames { - public static $additional = [ - 'autoconfig', - 'blog', - 'broadcasthost', - 'copyright', - 'download', - 'domainadmin', - 'domainadministrator', - 'errors', - 'events', - 'example', - 'faq', - 'faqs', - 'features', - 'ftp', - 'guest', - 'guests', - 'hostmaster', - 'hostmaster', - 'imap', - 'info', - 'information', - 'is', - 'isatap', - 'it', - 'localdomain', - 'localhost', - 'mail', - 'mailer-daemon', - 'mailerdaemon', - 'marketing', - 'me', - 'mis', - 'mx', - 'no-reply', - 'nobody', - 'noc', - 'noreply', - 'ns0', - 'ns1', - 'ns2', - 'ns3', - 'ns4', - 'ns5', - 'ns6', - 'ns7', - 'ns8', - 'ns9', - 'owner', - 'pop', - 'pop3', - 'postmaster', - 'pricing', - 'root', - 'sales', - 'security', - 'signin', - 'signout', - 'smtp', - 'src', - 'ssladmin', - 'ssladministrator', - 'sslwebmaster', - 'sys', - 'sysadmin', - 'system', - 'tutorial', - 'tutorials', - 'usenet', - 'uucp', - 'webmaster', - 'wpad', - ]; + public static $additional = [ + 'autoconfig', + 'blog', + 'broadcasthost', + 'copyright', + 'download', + 'domainadmin', + 'domainadministrator', + 'errors', + 'events', + 'example', + 'faq', + 'faqs', + 'features', + 'ftp', + 'guest', + 'guests', + 'hostmaster', + 'hostmaster', + 'imap', + 'info', + 'information', + 'is', + 'isatap', + 'it', + 'localdomain', + 'localhost', + 'mail', + 'mailer-daemon', + 'mailerdaemon', + 'marketing', + 'me', + 'mis', + 'mx', + 'no-reply', + 'nobody', + 'noc', + 'noreply', + 'ns0', + 'ns1', + 'ns2', + 'ns3', + 'ns4', + 'ns5', + 'ns6', + 'ns7', + 'ns8', + 'ns9', + 'owner', + 'pop', + 'pop3', + 'postmaster', + 'pricing', + 'root', + 'sales', + 'security', + 'signin', + 'signout', + 'smtp', + 'src', + 'ssladmin', + 'ssladministrator', + 'sslwebmaster', + 'sys', + 'sysadmin', + 'system', + 'tutorial', + 'tutorials', + 'usenet', + 'uucp', + 'webmaster', + 'wpad', + ]; - public static $reserved = [ - // Reserved for instance admin - 'admin', - 'administrator', + public static $reserved = [ + // Reserved for instance admin + 'admin', + 'administrator', - // Static Assets - 'assets', - 'public', - 'storage', - 'htaccess', - '.htaccess', - 'favicon.ico', - 'embed.js', - 'index.php', - 'manifest.json', - 'mix-manifest.json', - 'robots.txt', + // Federation + 'authorize_interaction', - // Laravel Horizon - 'horizon', + // Static Assets + 'assets', + 'public', + 'storage', + 'htaccess', + '.htaccess', + 'favicon.ico', + 'embed.js', + 'index.php', + 'manifest.json', + 'mix-manifest.json', + 'robots.txt', - // Reserved routes - 'a', - 'app', - 'about', - 'aboutus', - 'about-us', - 'abuse', - 'actor', - 'actors', - 'account', - 'admins', - 'api', - 'audio', - 'auth', - 'avatar', - 'avatars', - 'b', - 'bartender', - 'broadcast', - 'broadcaster', - 'booth', - 'bouncer', - 'browse', - 'c', - 'cdn', - 'circle', - 'circles', - 'checkpoint', - 'collection', - 'collections', - 'community', - 'communities', - 'contact', - 'contact-us', - 'contact_us', - 'costar', - 'costars', - 'css', - 'd', - 'dashboard', - 'delete', - 'deleted', - 'deleting', - 'dmca', - 'db', - 'deck', - 'dev', - 'developer', - 'developers', - 'discover', - 'discovers', - 'dj', - 'doc', - 'docs', - 'docs', - 'drive', - 'drives', - 'driver', - 'e', - 'embed', - 'email', - 'emails', - 'emoji', - 'emojis', - 'error', - 'explore', - 'export', - 'exports', - 'external', - 'f', - 'fedi', - 'fediverse', - 'feed', - 'featured', - 'font', - 'fonts', - 'follow', - 'follows', - 'followme', - 'follow-me', - 'follow_me', - 'g', - 'go', - 'gdpr', - 'graph', - 'ghost', - 'ghosts', - 'global', - 'group', - 'groups', - 'h', - 'header', - 'headers', - 'home', - 'help', - 'helpcenter', - 'help-center', - 'help_center', - 'help_center_', - 'help-center-', - 'help-center_', - 'help_center-', - 'i', - 'instance', - 'inbox', - 'img', - 'imgs', - 'image', - 'images', - 'invite', - 'invites', - 'import', - 'imports', - 'j', - 'join', - 'js', - 'k', - 'key', - 'l', - 'lang', - 'language', - '_lang', - '_language', - 'lab', - 'labs', - 'legal', - 'link', - 'live', - 'look', - 'look-back', - 'loop', - 'loops', - 'location', - 'locations', - 'login', - 'logout', - 'm', - 'media', - 'mini', - 'micro', - 'menu', - 'music', - 'my2020', - 'my2021', - 'my2022', - 'my2023', - 'my2024', - 'my2025', - 'my2026', - 'my2027', - 'my2028', - 'my2029', - 'my2030', - 'my', - 'n', - 'news', - 'new', - 'news', - 'news', - 'newsfeed', - 'newsroom', - 'newsrooms', - 'news-room', - 'news-rooms', - 'network', - 'networks', - 'o', - 'oauth', - 'official', - 'p', - 'page', - 'pages', - 'pin', - 'pins', - 'photo', - 'photos', - 'password', - 'portfolio', - 'portfolios', - 'pre', - 'post', - 'privacy', - 'private', - 'q', - 'quote', - 'query', - 'r', - 'redirect', - 'redirects', - 'register', - 'registers', - 'review', - 'reviews', - 'reset', - 'report', - 'results', - 'reports', - 'robot', - 'robots', - 's', - 'sc', - 'search', - 'sell', - 'send', - 'settings', - 'short', - 'shortcode', - 'status', - 'statuses', - 'site', - 'sites', - 'stage', - 'static', - 'story', - 'stories', - 'support', - 'svg', - 'svgs', - 't', - 'terms', - 'telescope', - 'timeline', - 'timelines', - 'tour', - 'tv', - 'u', - 'user', - 'users', - 'username', - 'usernames', - 'v', - 'valet', - 'video', - 'videos', - 'vendor', - 'w', - 'waiter', - 'wall', - 'whats-new', - 'whatsnew', - 'whatnew', - 'whats-news', - 'web', - 'ws', - 'wss', - 'www', - 'x', - 'y', - 'year', - 'year-in-review', - 'z', - '400', - '401', - '403', - '404', - '500', - '503', - '504', - ]; + // Laravel Horizon + 'horizon', - public static function get() - { - $banned = []; + // Reserved routes + 'a', + 'app', + 'about', + 'aboutus', + 'about-us', + 'abuse', + 'actor', + 'actors', + 'account', + 'admins', + 'api', + 'audio', + 'auth', + 'avatar', + 'avatars', + 'b', + 'bartender', + 'broadcast', + 'broadcaster', + 'booth', + 'bouncer', + 'browse', + 'c', + 'cdn', + 'circle', + 'circles', + 'checkpoint', + 'collection', + 'collections', + 'community', + 'communities', + 'contact', + 'contact-us', + 'contact_us', + 'costar', + 'costars', + 'css', + 'd', + 'dashboard', + 'delete', + 'deleted', + 'deleting', + 'dmca', + 'db', + 'deck', + 'dev', + 'developer', + 'developers', + 'discover', + 'discovers', + 'dj', + 'doc', + 'docs', + 'docs', + 'drive', + 'drives', + 'driver', + 'e', + 'embed', + 'email', + 'emails', + 'emoji', + 'emojis', + 'error', + 'explore', + 'export', + 'exports', + 'external', + 'f', + 'fedi', + 'fediverse', + 'feed', + 'featured', + 'font', + 'fonts', + 'follow', + 'follows', + 'followme', + 'follow-me', + 'follow_me', + 'g', + 'go', + 'gdpr', + 'graph', + 'ghost', + 'ghosts', + 'global', + 'group', + 'groups', + 'h', + 'header', + 'headers', + 'home', + 'help', + 'help.center', + 'helpcenter', + 'help-center', + 'help_center', + 'help_center_', + 'help-center-', + 'help-center_', + 'help_center-', + 'i', + 'instance', + 'inbox', + 'img', + 'imgs', + 'image', + 'images', + 'invite', + 'invites', + 'import', + 'imports', + 'intent', + 'j', + 'join', + 'js', + 'k', + 'key', + 'l', + 'lang', + 'language', + '_lang', + '_language', + 'lab', + 'labs', + 'legal', + 'link', + 'live', + 'look', + 'look-back', + 'loop', + 'loops', + 'location', + 'locations', + 'login', + 'logout', + 'm', + 'media', + 'mini', + 'micro', + 'menu', + 'music', + 'my2020', + 'my2021', + 'my2022', + 'my2023', + 'my2024', + 'my2025', + 'my2026', + 'my2027', + 'my2028', + 'my2029', + 'my2030', + 'my', + 'n', + 'news', + 'new', + 'news', + 'news', + 'newsfeed', + 'newsroom', + 'newsrooms', + 'news-room', + 'news-rooms', + 'network', + 'networks', + 'o', + 'oauth', + 'official', + 'p', + 'page', + 'pages', + 'pin', + 'pins', + 'photo', + 'photos', + 'password', + 'portfolio', + 'portfolios', + 'pre', + 'post', + 'privacy', + 'private', + 'q', + 'quote', + 'query', + 'r', + 'redirect', + 'redirects', + 'register', + 'registers', + 'review', + 'reviews', + 'reset', + 'report', + 'results', + 'reports', + 'robot', + 'robots', + 's', + 'sc', + 'search', + 'sell', + 'send', + 'settings', + 'short', + 'shortcode', + 'status', + 'statuses', + 'site', + 'sites', + 'stage', + 'static', + 'story', + 'stories', + 'support', + 'svg', + 'svgs', + 't', + 'terms', + 'telescope', + 'timeline', + 'timelines', + 'tour', + 'tv', + 'u', + 'user', + 'users', + 'username', + 'usernames', + 'v', + 'valet', + 'video', + 'videos', + 'vendor', + 'w', + 'waiter', + 'wall', + 'whats-new', + 'whatsnew', + 'whatnew', + 'whats-news', + 'web', + 'ws', + 'wss', + 'www', + 'x', + 'y', + 'year', + 'year-in-review', + 'z', + '400', + '401', + '403', + '404', + '500', + '503', + '504', + ]; - if(config('instance.username.banned')) { - $banned = array_map('trim', explode(',', config('instance.username.banned'))); - } + public static function get() + { + $banned = []; - $additional = self::$additional; - $reserved = self::$reserved; + if (config('instance.username.banned')) { + $banned = array_map('trim', explode(',', config('instance.username.banned'))); + } - $res = array_merge($additional, $reserved, $banned); - $res = array_unique($res); - sort($res); - - return $res; - } + $additional = self::$additional; + $reserved = self::$reserved; + + $res = array_merge($additional, $reserved, $banned); + $res = array_unique($res); + sort($res); + + return $res; + } } diff --git a/app/Util/Site/Config.php b/app/Util/Site/Config.php index e0916591d..530dc7108 100644 --- a/app/Util/Site/Config.php +++ b/app/Util/Site/Config.php @@ -5,48 +5,50 @@ namespace App\Util\Site; use Cache; use Illuminate\Support\Str; -class Config { +class Config +{ + const CACHE_KEY = 'api:site:configuration:_v0.9'; - const CACHE_KEY = 'api:site:configuration:_v0.8'; - - public static function get() { - return Cache::remember(self::CACHE_KEY, 900, function() { + public static function get() + { + return Cache::remember(self::CACHE_KEY, 900, function () { $hls = [ 'enabled' => config('media.hls.enabled'), ]; - if(config('media.hls.enabled')) { + if (config('media.hls.enabled')) { $hls = [ 'enabled' => true, 'debug' => (bool) config('media.hls.debug'), 'p2p' => (bool) config('media.hls.p2p'), 'p2p_debug' => (bool) config('media.hls.p2p_debug'), 'tracker' => config('media.hls.tracker'), - 'ice' => config('media.hls.ice') + 'ice' => config('media.hls.ice'), ]; } + return [ 'version' => config('pixelfed.version'), 'open_registration' => (bool) config_cache('pixelfed.open_registration'), 'uploader' => [ - 'max_photo_size' => (int) config('pixelfed.max_photo_size'), - 'max_caption_length' => (int) config('pixelfed.max_caption_length'), - 'max_altext_length' => (int) config('pixelfed.max_altext_length', 150), + 'max_photo_size' => (int) config_cache('pixelfed.max_photo_size'), + 'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150), 'album_limit' => (int) config_cache('pixelfed.max_album_length'), 'image_quality' => (int) config_cache('pixelfed.image_quality'), - 'max_collection_length' => (int) config('pixelfed.max_collection_length', 18), + 'max_collection_length' => (int) config_cache('pixelfed.max_collection_length', 18), - 'optimize_image' => (bool) config('pixelfed.optimize_image'), - 'optimize_video' => (bool) config('pixelfed.optimize_video'), + 'optimize_image' => (bool) config_cache('pixelfed.optimize_image'), + 'optimize_video' => (bool) config_cache('pixelfed.optimize_video'), 'media_types' => config_cache('pixelfed.media_types'), 'mime_types' => config_cache('pixelfed.media_types') ? explode(',', config_cache('pixelfed.media_types')) : [], - 'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit') + 'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'), ], 'activitypub' => [ 'enabled' => (bool) config_cache('federation.activitypub.enabled'), - 'remote_follow' => config('federation.activitypub.remoteFollow') + 'remote_follow' => config('federation.activitypub.remoteFollow'), ], 'ab' => config('exp'), @@ -54,8 +56,8 @@ class Config { 'site' => [ 'name' => config_cache('app.name'), 'domain' => config('pixelfed.domain.app'), - 'url' => config('app.url'), - 'description' => config_cache('app.short_description') + 'url' => config('app.url'), + 'description' => config_cache('app.short_description'), ], 'account' => [ @@ -63,15 +65,15 @@ class Config { 'max_bio_length' => config('pixelfed.max_bio_length'), 'max_name_length' => config('pixelfed.max_name_length'), 'min_password_length' => config('pixelfed.min_password_length'), - 'max_account_size' => config('pixelfed.max_account_size') + 'max_account_size' => config('pixelfed.max_account_size'), ], 'username' => [ 'remote' => [ 'formats' => config('instance.username.remote.formats'), 'format' => config('instance.username.remote.format'), - 'custom' => config('instance.username.remote.custom') - ] + 'custom' => config('instance.username.remote.custom'), + ], ], 'features' => [ @@ -85,22 +87,30 @@ class Config { 'import' => [ 'instagram' => (bool) config_cache('pixelfed.import.instagram.enabled'), 'mastodon' => false, - 'pixelfed' => false + 'pixelfed' => false, ], 'label' => [ 'covid' => [ 'enabled' => (bool) config('instance.label.covid.enabled'), 'org' => config('instance.label.covid.org'), 'url' => config('instance.label.covid.url'), - ] + ], ], - 'hls' => $hls - ] + 'hls' => $hls, + 'groups' => (bool) config('groups.enabled'), + ], ]; }); } - public static function json() { + public static function refresh() + { + Cache::forget(self::CACHE_KEY); + return self::get(); + } + + public static function json() + { return json_encode(self::get(), JSON_FORCE_OBJECT); } } diff --git a/app/Util/Site/Nodeinfo.php b/app/Util/Site/Nodeinfo.php index 0458299c5..9c0031ef4 100644 --- a/app/Util/Site/Nodeinfo.php +++ b/app/Util/Site/Nodeinfo.php @@ -2,12 +2,9 @@ namespace App\Util\Site; -use Illuminate\Support\Facades\Cache; -use App\Like; -use App\Profile; -use App\Status; +use App\Services\InstanceService; use App\User; -use Illuminate\Support\Str; +use Illuminate\Support\Facades\Cache; class Nodeinfo { @@ -17,49 +14,48 @@ class Nodeinfo $activeHalfYear = self::activeUsersHalfYear(); $activeMonth = self::activeUsersMonthly(); - $users = Cache::remember('api:nodeinfo:users', 43200, function() { + $users = Cache::remember('api:nodeinfo:users', 43200, function () { return User::count(); }); - $statuses = Cache::remember('api:nodeinfo:statuses', 21600, function() { - return Status::whereLocal(true)->count(); - }); + $statuses = InstanceService::totalLocalStatuses(); - $features = [ 'features' => \App\Util\Site\Config::get()['features'] ]; + $features = ['features' => \App\Util\Site\Config::get()['features']]; return [ 'metadata' => [ 'nodeName' => config_cache('app.name'), 'software' => [ - 'homepage' => 'https://pixelfed.org', - 'repo' => 'https://github.com/pixelfed/pixelfed', + 'homepage' => 'https://pixelfed.org', + 'repo' => 'https://github.com/pixelfed/pixelfed', ], - 'config' => $features + 'config' => $features, ], - 'protocols' => [ + 'protocols' => [ 'activitypub', ], 'services' => [ - 'inbound' => [], + 'inbound' => [], 'outbound' => [], ], 'software' => [ - 'name' => 'pixelfed', - 'version' => config('pixelfed.version'), + 'name' => 'pixelfed', + 'version' => config('pixelfed.version'), ], 'usage' => [ - 'localPosts' => (int) $statuses, + 'localPosts' => (int) $statuses, 'localComments' => 0, - 'users' => [ - 'total' => (int) $users, + 'users' => [ + 'total' => (int) $users, 'activeHalfyear' => (int) $activeHalfYear, - 'activeMonth' => (int) $activeMonth, + 'activeMonth' => (int) $activeMonth, ], ], 'version' => '2.0', ]; }); $res['openRegistrations'] = (bool) config_cache('pixelfed.open_registration'); + return $res; } @@ -69,7 +65,7 @@ class Nodeinfo 'links' => [ [ 'href' => config('pixelfed.nodeinfo.url'), - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', ], ], ]; @@ -77,18 +73,18 @@ class Nodeinfo public static function activeUsersMonthly() { - return Cache::remember('api:nodeinfo:active-users-monthly', 43200, function() { + return Cache::remember('api:nodeinfo:active-users-monthly', 43200, function () { return User::withTrashed() - ->select('last_active_at, updated_at') - ->where('updated_at', '>', now()->subWeeks(5)) - ->orWhere('last_active_at', '>', now()->subWeeks(5)) - ->count(); + ->select('last_active_at, updated_at') + ->where('updated_at', '>', now()->subWeeks(5)) + ->orWhere('last_active_at', '>', now()->subWeeks(5)) + ->count(); }); } public static function activeUsersHalfYear() { - return Cache::remember('api:nodeinfo:active-users-half-year', 43200, function() { + return Cache::remember('api:nodeinfo:active-users-half-year', 43200, function () { return User::withTrashed() ->select('last_active_at, updated_at') ->where('last_active_at', '>', now()->subMonths(6)) diff --git a/app/Util/Webfinger/Webfinger.php b/app/Util/Webfinger/Webfinger.php index 879103332..3897fc162 100644 --- a/app/Util/Webfinger/Webfinger.php +++ b/app/Util/Webfinger/Webfinger.php @@ -4,43 +4,67 @@ namespace App\Util\Webfinger; class Webfinger { - protected $user; - protected $subject; - protected $aliases; - protected $links; + protected $user; - public function __construct($user) - { - $this->subject = 'acct:'.$user->username.'@'.parse_url(config('app.url'), PHP_URL_HOST); - $this->aliases = [ - $user->url(), - $user->permalink(), - ]; - $this->links = [ - [ - 'rel' => 'http://webfinger.net/rel/profile-page', - 'type' => 'text/html', - 'href' => $user->url(), - ], - [ - 'rel' => 'http://schemas.google.com/g/2010#updates-from', - 'type' => 'application/atom+xml', - 'href' => $user->permalink('.atom'), - ], - [ - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => $user->permalink(), - ], - ]; - } + protected $subject; - public function generate() - { - return [ - 'subject' => $this->subject, - 'aliases' => $this->aliases, - 'links' => $this->links, - ]; - } + protected $aliases; + + protected $links; + + public function __construct($user) + { + $avatar = $user ? $user->avatarUrl() : url('/storage/avatars/default.jpg'); + $avatarPath = parse_url($avatar, PHP_URL_PATH); + $extension = pathinfo($avatarPath, PATHINFO_EXTENSION); + $mimeTypes = [ + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'svg' => 'image/svg', + ]; + $avatarType = $mimeTypes[$extension] ?? 'application/octet-stream'; + + $this->subject = 'acct:'.$user->username.'@'.parse_url(config('app.url'), PHP_URL_HOST); + $this->aliases = [ + $user->url(), + $user->permalink(), + ]; + $this->links = [ + [ + 'rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => $user->url(), + ], + [ + 'rel' => 'http://schemas.google.com/g/2010#updates-from', + 'type' => 'application/atom+xml', + 'href' => $user->permalink('.atom'), + ], + [ + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => $user->permalink(), + ], + [ + 'rel' => 'http://webfinger.net/rel/avatar', + 'type' => $avatarType, + 'href' => $avatar, + ], + [ + 'rel' => 'http://ostatus.org/schema/1.0/subscribe', + 'template' => 'https://'.config_cache('pixelfed.domain.app').'/authorize_interaction?uri={uri}', + ], + ]; + } + + public function generate() + { + return [ + 'subject' => $this->subject, + 'aliases' => $this->aliases, + 'links' => $this->links, + ]; + } } diff --git a/app/Util/Webfinger/WebfingerUrl.php b/app/Util/Webfinger/WebfingerUrl.php index 091d7ce14..b51d536d9 100644 --- a/app/Util/Webfinger/WebfingerUrl.php +++ b/app/Util/Webfinger/WebfingerUrl.php @@ -3,16 +3,28 @@ namespace App\Util\Webfinger; use App\Util\Lexer\Nickname; +use App\Services\InstanceService; class WebfingerUrl { + public static function get($url) + { + $n = Nickname::normalizeProfileUrl($url); + if(!$n || !isset($n['domain'], $n['username'])) { + return false; + } + if(in_array($n['domain'], InstanceService::getBannedDomains())) { + return false; + } + return 'https://' . $n['domain'] . '/.well-known/webfinger?resource=acct:' . $n['username'] . '@' . $n['domain']; + } + public static function generateWebfingerUrl($url) { $url = Nickname::normalizeProfileUrl($url); $domain = $url['domain']; $username = $url['username']; $path = "https://{$domain}/.well-known/webfinger?resource=acct:{$username}@{$domain}"; - return $path; } } diff --git a/composer.json b/composer.json index 54328a674..26b975f41 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "AGPL-3.0-only", "type": "project", "require": { - "php": "^8.1|^8.2", + "php": "^8.2|^8.3", "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", @@ -14,41 +14,43 @@ "ext-mbstring": "*", "ext-openssl": "*", "bacon/bacon-qr-code": "^2.0.3", - "beyondcode/laravel-websockets": "^1.13", "brick/math": "^0.9.3", - "buzz/laravel-h-captcha": "1.0.4", + "buzz/laravel-h-captcha": "^1.0.4", "doctrine/dbal": "^3.0", "intervention/image": "^2.4", "jenssegers/agent": "^2.6", - "laravel/framework": "^10.0", + "laravel-notification-channels/expo": "~1.3.0|~2.0.0", + "laravel-notification-channels/webpush": "^8.0", + "laravel/framework": "^11.0", "laravel/helpers": "^1.1", "laravel/horizon": "^5.0", - "laravel/passport": "^11.0", - "laravel/tinker": "^2.0", + "laravel/passport": "^12.0", + "laravel/tinker": "^2.9", "laravel/ui": "^4.2", "league/flysystem-aws-s3-v3": "^3.0", "league/iso3166": "^2.1|^4.0", + "league/uri": "^7.4", "pbmedia/laravel-ffmpeg": "^8.0", "phpseclib/phpseclib": "~2.0", "pixelfed/fractal": "^0.18.0", "pixelfed/laravel-snowflake": "^2.0", - "pixelfed/zttp": "^0.5", "pragmarx/google2fa": "^8.0", "predis/predis": "^2.0", + "pusher/pusher-php-server": "^7.2", + "resend/resend-php": "^0.13.0", "spatie/laravel-backup": "^8.0.0", - "spatie/laravel-image-optimizer": "^1.7", - "stevebauman/purify": "6.0.*", + "spatie/laravel-image-optimizer": "^1.8.0", + "stevebauman/purify": "^6.2.0", "symfony/http-client": "^6.1", - "symfony/http-kernel": "^6.0.0", "symfony/mailgun-mailer": "^6.1" }, "require-dev": { - "brianium/paratest": "^6.1", - "fakerphp/faker": "^1.20", - "laravel/telescope": "^4.14", - "mockery/mockery": "^1.0", - "nunomaduro/collision": "^6.1", - "phpunit/phpunit": "^9.0" + "fakerphp/faker": "^1.23", + "laravel/pint": "^1.13", + "laravel/telescope": "^5.0", + "mockery/mockery": "^1.6", + "nunomaduro/collision": "^8.1", + "phpunit/phpunit": "^11.0.1" }, "autoload": { "classmap": [ @@ -84,6 +86,9 @@ "post-create-project-cmd": [ "@php artisan key:generate --ansi" ], + "post-update-cmd": [ + "@php artisan vendor:publish --tag=laravel-assets --ansi --force" + ], "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" diff --git a/composer.lock b/composer.lock index 74c453c67..2e8c215d2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "427b8abad9495b7a7bd2a489625a3881", + "content-hash": "0035325cb0240e92fc378e49f76447bd", "packages": [ { "name": "aws/aws-crt-php", - "version": "v1.2.1", + "version": "v1.2.7", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5" + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/1926277fc71d253dfa820271ac5987bdb193ccf5", - "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", "shasum": "" }, "require": { @@ -56,35 +56,35 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.1" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" }, - "time": "2023-03-24T20:22:19+00:00" + "time": "2024-10-18T22:15:13+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.275.7", + "version": "3.336.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "54dcef3349c81b46c0f5f6e54b5f9bfb5db19903" + "reference": "954bfdfc048840ca34afe2a2e1cbcff6681989c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/54dcef3349c81b46c0f5f6e54b5f9bfb5db19903", - "reference": "54dcef3349c81b46c0f5f6e54b5f9bfb5db19903", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/954bfdfc048840ca34afe2a2e1cbcff6681989c4", + "reference": "954bfdfc048840ca34afe2a2e1cbcff6681989c4", "shasum": "" }, "require": { - "aws/aws-crt-php": "^1.0.4", + "aws/aws-crt-php": "^1.2.3", "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", - "guzzlehttp/promises": "^1.4.0", + "guzzlehttp/promises": "^1.4.0 || ^2.0", "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", "mtdowling/jmespath.php": "^2.6", - "php": ">=5.5", - "psr/http-message": "^1.0" + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", @@ -99,9 +99,9 @@ "ext-sockets": "*", "nette/neon": "^2.3", "paragonie/random_compat": ">= 2", - "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5", - "psr/cache": "^1.0", - "psr/simple-cache": "^1.0", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", "sebastian/comparator": "^1.2.3 || ^4.0", "yoast/phpunit-polyfills": "^1.0" }, @@ -124,7 +124,10 @@ ], "psr-4": { "Aws\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/data/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -151,9 +154,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.275.7" + "source": "https://github.com/aws/aws-sdk-php/tree/3.336.2" }, - "time": "2023-07-13T18:21:04+00:00" + "time": "2024-12-20T19:05:10+00:00" }, { "name": "bacon/bacon-qr-code", @@ -209,88 +212,6 @@ }, "time": "2022-12-07T17:46:57+00:00" }, - { - "name": "beyondcode/laravel-websockets", - "version": "1.14.0", - "source": { - "type": "git", - "url": "https://github.com/beyondcode/laravel-websockets.git", - "reference": "9ab87be1d96340979e67b462ea5fd6a8b06e6a02" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/beyondcode/laravel-websockets/zipball/9ab87be1d96340979e67b462ea5fd6a8b06e6a02", - "reference": "9ab87be1d96340979e67b462ea5fd6a8b06e6a02", - "shasum": "" - }, - "require": { - "cboden/ratchet": "^0.4.1", - "ext-json": "*", - "facade/ignition-contracts": "^1.0", - "guzzlehttp/psr7": "^1.7|^2.0", - "illuminate/broadcasting": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/routing": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0", - "php": "^7.2|^8.0", - "pusher/pusher-php-server": "^3.0|^4.0|^5.0|^6.0|^7.0", - "react/dns": "^1.1", - "react/http": "^1.1", - "symfony/http-kernel": "^4.0|^5.0|^6.0", - "symfony/psr-http-message-bridge": "^1.1|^2.0" - }, - "require-dev": { - "mockery/mockery": "^1.3.3", - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0", - "phpunit/phpunit": "^8.0|^9.0|^10.0" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "BeyondCode\\LaravelWebSockets\\WebSocketsServiceProvider" - ], - "aliases": { - "WebSocketRouter": "BeyondCode\\LaravelWebSockets\\Facades\\WebSocketRouter" - } - } - }, - "autoload": { - "psr-4": { - "BeyondCode\\LaravelWebSockets\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marcel Pociot", - "email": "marcel@beyondco.de", - "homepage": "https://beyondcode.de", - "role": "Developer" - }, - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "An easy to use WebSocket server", - "homepage": "https://github.com/beyondcode/laravel-websockets", - "keywords": [ - "beyondcode", - "laravel-websockets" - ], - "support": { - "issues": "https://github.com/beyondcode/laravel-websockets/issues", - "source": "https://github.com/beyondcode/laravel-websockets/tree/1.14.0" - }, - "time": "2023-02-15T10:40:49+00:00" - }, { "name": "brick/math", "version": "0.9.3", @@ -353,32 +274,32 @@ }, { "name": "buzz/laravel-h-captcha", - "version": "v1.0.4", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/thinhbuzz/laravel-h-captcha.git", - "reference": "f2db3734203876ef1f69ba4dc0f4d9d71462f534" + "reference": "53435b15cf2094306d65584e68d4ed63dee1b9b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thinhbuzz/laravel-h-captcha/zipball/f2db3734203876ef1f69ba4dc0f4d9d71462f534", - "reference": "f2db3734203876ef1f69ba4dc0f4d9d71462f534", + "url": "https://api.github.com/repos/thinhbuzz/laravel-h-captcha/zipball/53435b15cf2094306d65584e68d4ed63dee1b9b9", + "reference": "53435b15cf2094306d65584e68d4ed63dee1b9b9", "shasum": "" }, "require": { "guzzlehttp/guzzle": "6.*|7.*", - "illuminate/support": "5.*|6.*|7.*|8.*|9.*|10.*", + "illuminate/support": "5.*|6.*|7.*|8.*|9.*|10.*|11.*", "php": ">=5.4.0" }, "type": "library", "extra": { "laravel": { - "providers": [ - "Buzz\\LaravelHCaptcha\\CaptchaServiceProvider" - ], "aliases": { "Captcha": "Buzz\\LaravelHCaptcha\\CaptchaFacade" - } + }, + "providers": [ + "Buzz\\LaravelHCaptcha\\CaptchaServiceProvider" + ] } }, "autoload": { @@ -401,59 +322,52 @@ "homepage": "https://github.com/thinhbuzz/laravel-h-captcha", "keywords": [ "captcha", - "h captcha", "h-captcha", "hcaptcha", "laravel", "laravel 10", + "laravel 11", "laravel 5", "laravel 6", "laravel 7", "laravel 8", - "laravel 9", - "laravel10", - "laravel5", - "laravel6", - "laravel7", - "laravel8", - "laravel9" + "laravel 9" ], "support": { "issues": "https://github.com/thinhbuzz/laravel-h-captcha/issues", - "source": "https://github.com/thinhbuzz/laravel-h-captcha/tree/v1.0.4" + "source": "https://github.com/thinhbuzz/laravel-h-captcha/tree/v1.0.5" }, - "time": "2023-03-10T05:38:55+00:00" + "time": "2024-03-04T11:23:51+00:00" }, { - "name": "cboden/ratchet", - "version": "v0.4.4", + "name": "carbonphp/carbon-doctrine-types", + "version": "2.1.0", "source": { "type": "git", - "url": "https://github.com/ratchetphp/Ratchet.git", - "reference": "5012dc954541b40c5599d286fd40653f5716a38f" + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/Ratchet/zipball/5012dc954541b40c5599d286fd40653f5716a38f", - "reference": "5012dc954541b40c5599d286fd40653f5716a38f", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", "shasum": "" }, "require": { - "guzzlehttp/psr7": "^1.7|^2.0", - "php": ">=5.4.2", - "ratchet/rfc6455": "^0.3.1", - "react/event-loop": ">=0.4", - "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", - "symfony/http-foundation": "^2.6|^3.0|^4.0|^5.0|^6.0", - "symfony/routing": "^2.6|^3.0|^4.0|^5.0|^6.0" + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" }, "require-dev": { - "phpunit/phpunit": "~4.8" + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" }, "type": "library", "autoload": { "psr-4": { - "Ratchet\\": "src/Ratchet" + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" } }, "notification-url": "https://packagist.org/downloads/", @@ -462,50 +376,57 @@ ], "authors": [ { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "role": "Developer" - }, - { - "name": "Matt Bonneau", - "role": "Developer" + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" } ], - "description": "PHP WebSocket library", - "homepage": "http://socketo.me", + "description": "Types to use Carbon in Doctrine", "keywords": [ - "Ratchet", - "WebSockets", - "server", - "sockets", - "websocket" + "carbon", + "date", + "datetime", + "doctrine", + "time" ], "support": { - "chat": "https://gitter.im/reactphp/reactphp", - "issues": "https://github.com/ratchetphp/Ratchet/issues", - "source": "https://github.com/ratchetphp/Ratchet/tree/v0.4.4" + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" }, - "time": "2021-12-14T00:20:41+00:00" + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2023-12-11T17:09:12+00:00" }, { "name": "dasprid/enum", - "version": "1.0.4", + "version": "1.0.6", "source": { "type": "git", "url": "https://github.com/DASPRiD/Enum.git", - "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f" + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8e6b6ea76eabbf19ea2bf5b67b98e1860474012f", - "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90", + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90", "shasum": "" }, "require": { "php": ">=7.1 <9.0" }, "require-dev": { - "phpunit/phpunit": "^7 | ^8 | ^9", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", "squizlabs/php_codesniffer": "*" }, "type": "library", @@ -533,9 +454,9 @@ ], "support": { "issues": "https://github.com/DASPRiD/Enum/issues", - "source": "https://github.com/DASPRiD/Enum/tree/1.0.4" + "source": "https://github.com/DASPRiD/Enum/tree/1.0.6" }, - "time": "2023-03-01T18:44:03+00:00" + "time": "2024-08-09T14:30:48+00:00" }, { "name": "defuse/php-encryption", @@ -606,16 +527,16 @@ }, { "name": "dflydev/dot-access-data", - "version": "v3.0.2", + "version": "v3.0.3", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "f41715465d65213d644d3141a6a93081be5d3549" + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549", - "reference": "f41715465d65213d644d3141a6a93081be5d3549", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", "shasum": "" }, "require": { @@ -675,9 +596,9 @@ ], "support": { "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", - "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2" + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" }, - "time": "2022-10-27T11:44:00+00:00" + "time": "2024-07-08T12:26:09+00:00" }, { "name": "doctrine/cache", @@ -774,16 +695,16 @@ }, { "name": "doctrine/dbal", - "version": "3.6.4", + "version": "3.9.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f" + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f", - "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", "shasum": "" }, "require": { @@ -798,14 +719,15 @@ "require-dev": { "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", - "jetbrains/phpstorm-stubs": "2022.3", - "phpstan/phpstan": "1.10.14", - "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.7", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "1.12.6", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", - "squizlabs/php_codesniffer": "3.7.2", - "symfony/cache": "^5.4|^6.0", - "symfony/console": "^4.4|^5.4|^6.0", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" }, "suggest": { @@ -866,7 +788,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.6.4" + "source": "https://github.com/doctrine/dbal/tree/3.9.3" }, "funding": [ { @@ -882,33 +804,31 @@ "type": "tidelift" } ], - "time": "2023-06-15T07:40:12+00:00" + "time": "2024-10-10T17:56:43+00:00" }, { "name": "doctrine/deprecations", - "version": "v1.1.1", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3" + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", - "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", - "phpstan/phpstan": "1.4.10 || 1.10.15", - "phpstan/phpstan-phpunit": "^1.0", + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psalm/plugin-phpunit": "0.18.4", - "psr/log": "^1 || ^2 || ^3", - "vimeo/psalm": "4.30.0 || 5.12.0" + "psr/log": "^1 || ^2 || ^3" }, "suggest": { "psr/log": "Allows logging deprecations via PSR-3 logger implementation" @@ -916,7 +836,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + "Doctrine\\Deprecations\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -927,22 +847,22 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/v1.1.1" + "source": "https://github.com/doctrine/deprecations/tree/1.1.4" }, - "time": "2023-06-03T09:27:29+00:00" + "time": "2024-12-07T21:18:45+00:00" }, { "name": "doctrine/event-manager", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32" + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/750671534e0241a7c50ea5b43f67e23eb5c96f32", - "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", "shasum": "" }, "require": { @@ -952,10 +872,10 @@ "doctrine/common": "<2.9" }, "require-dev": { - "doctrine/coding-standard": "^10", + "doctrine/coding-standard": "^12", "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^4.28" + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" }, "type": "library", "autoload": { @@ -1004,7 +924,7 @@ ], "support": { "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.0.0" + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" }, "funding": [ { @@ -1020,20 +940,20 @@ "type": "tidelift" } ], - "time": "2022-10-12T20:59:15+00:00" + "time": "2024-05-22T20:47:39+00:00" }, { "name": "doctrine/inflector", - "version": "2.0.8", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff" + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/f9301a5b2fb1216b2b08f02ba04dc45423db6bff", - "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", "shasum": "" }, "require": { @@ -1095,7 +1015,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.8" + "source": "https://github.com/doctrine/inflector/tree/2.0.10" }, "funding": [ { @@ -1111,31 +1031,31 @@ "type": "tidelift" } ], - "time": "2023-06-16T13:40:37+00:00" + "time": "2024-02-18T20:23:39+00:00" }, { "name": "doctrine/lexer", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "84a527db05647743d50373e0ec53a152f2cde568" + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/84a527db05647743d50373e0ec53a152f2cde568", - "reference": "84a527db05647743d50373e0ec53a152f2cde568", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^10", - "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^9.5", + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^5.0" + "vimeo/psalm": "^5.21" }, "type": "library", "autoload": { @@ -1172,7 +1092,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/3.0.0" + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, "funding": [ { @@ -1188,20 +1108,20 @@ "type": "tidelift" } ], - "time": "2022-12-15T16:57:16+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { "name": "dragonmantank/cron-expression", - "version": "v3.3.2", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8" + "reference": "8c784d071debd117328803d86b2097615b457500" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/782ca5968ab8b954773518e9e49a6f892a34b2a8", - "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", + "reference": "8c784d071debd117328803d86b2097615b457500", "shasum": "" }, "require": { @@ -1214,10 +1134,14 @@ "require-dev": { "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.0", - "phpstan/phpstan-webmozart-assert": "^1.0", "phpunit/phpunit": "^7.0|^8.0|^9.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, "autoload": { "psr-4": { "Cron\\": "src/Cron/" @@ -1241,7 +1165,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.2" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" }, "funding": [ { @@ -1249,20 +1173,20 @@ "type": "github" } ], - "time": "2022-09-10T18:51:20+00:00" + "time": "2024-10-09T13:47:03+00:00" }, { "name": "egulias/email-validator", - "version": "4.0.1", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff" + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/3a85486b709bc384dae8eb78fb2eec649bdb64ff", - "reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e", + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e", "shasum": "" }, "require": { @@ -1271,8 +1195,8 @@ "symfony/polyfill-intl-idn": "^1.26" }, "require-dev": { - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^4.30" + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" }, "suggest": { "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" @@ -1308,7 +1232,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.1" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.2" }, "funding": [ { @@ -1316,32 +1240,32 @@ "type": "github" } ], - "time": "2023-01-14T14:17:03+00:00" + "time": "2023-10-06T06:47:41+00:00" }, { "name": "evenement/evenement", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/igorw/evenement.git", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", "autoload": { - "psr-0": { - "Evenement": "src" + "psr-4": { + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1361,26 +1285,26 @@ ], "support": { "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/master" + "source": "https://github.com/igorw/evenement/tree/v3.0.2" }, - "time": "2017-07-23T21:35:13+00:00" + "time": "2023-08-08T05:53:35+00:00" }, { "name": "ezyang/htmlpurifier", - "version": "v4.16.0", + "version": "v4.18.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8" + "reference": "cb56001e54359df7ae76dc522d08845dc741621b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8", - "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b", + "reference": "cb56001e54359df7ae76dc522d08845dc741621b", "shasum": "" }, "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { "cerdic/css-tidy": "^1.7 || ^2.0", @@ -1422,92 +1346,46 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0" }, - "time": "2022-09-18T07:06:19+00:00" + "time": "2024-11-01T03:51:45+00:00" }, { - "name": "facade/ignition-contracts", - "version": "1.0.2", + "name": "fgrosse/phpasn1", + "version": "v2.5.0", "source": { "type": "git", - "url": "https://github.com/facade/ignition-contracts.git", - "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267" + "url": "https://github.com/fgrosse/PHPASN1.git", + "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/3c921a1cdba35b68a7f0ccffc6dffc1995b18267", - "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267", + "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/42060ed45344789fb9f21f9f1864fc47b9e3507b", + "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b", "shasum": "" }, "require": { - "php": "^7.3|^8.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^v2.15.8", - "phpunit/phpunit": "^9.3.11", - "vimeo/psalm": "^3.17.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Facade\\IgnitionContracts\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://flareapp.io", - "role": "Developer" - } - ], - "description": "Solution contracts for Ignition", - "homepage": "https://github.com/facade/ignition-contracts", - "keywords": [ - "contracts", - "flare", - "ignition" - ], - "support": { - "issues": "https://github.com/facade/ignition-contracts/issues", - "source": "https://github.com/facade/ignition-contracts/tree/1.0.2" - }, - "time": "2020-10-16T08:27:54+00:00" - }, - { - "name": "fig/http-message-util", - "version": "1.1.5", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message-util.git", - "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", - "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", - "shasum": "" - }, - "require": { - "php": "^5.3 || ^7.0 || ^8.0" + "php-coveralls/php-coveralls": "~2.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "suggest": { - "psr/http-message": "The package containing the PSR-7 interfaces" + "ext-bcmath": "BCmath is the fallback extension for big integer calculations", + "ext-curl": "For loading OID information from the web if they have not bee defined statically", + "ext-gmp": "GMP is the preferred extension for big integer calculations", + "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Fig\\Http\\Message\\": "src/" + "FG\\": "lib/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1516,47 +1394,60 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Friedrich Große", + "email": "friedrich.grosse@gmail.com", + "homepage": "https://github.com/FGrosse", + "role": "Author" + }, + { + "name": "All contributors", + "homepage": "https://github.com/FGrosse/PHPASN1/contributors" } ], - "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", + "homepage": "https://github.com/FGrosse/PHPASN1", "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" + "DER", + "asn.1", + "asn1", + "ber", + "binary", + "decoding", + "encoding", + "x.509", + "x.690", + "x509", + "x690" ], "support": { - "issues": "https://github.com/php-fig/http-message-util/issues", - "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + "issues": "https://github.com/fgrosse/PHPASN1/issues", + "source": "https://github.com/fgrosse/PHPASN1/tree/v2.5.0" }, - "time": "2020-11-24T22:02:12+00:00" + "abandoned": true, + "time": "2022-12-19T11:08:26+00:00" }, { "name": "firebase/php-jwt", - "version": "v6.8.0", + "version": "v6.10.2", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "48b0210c51718d682e53210c24d25c5a10a2299b" + "reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/48b0210c51718d682e53210c24d25c5a10a2299b", - "reference": "48b0210c51718d682e53210c24d25c5a10a2299b", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/30c19ed0f3264cb660ea496895cfb6ef7ee3653b", + "reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b", "shasum": "" }, "require": { - "php": "^7.4||^8.0" + "php": "^8.0" }, "require-dev": { - "guzzlehttp/guzzle": "^6.5||^7.4", + "guzzlehttp/guzzle": "^7.4", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "psr/cache": "^1.0||^2.0", + "psr/cache": "^2.0||^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" }, @@ -1594,27 +1485,27 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.8.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.10.2" }, - "time": "2023-06-20T16:45:35+00:00" + "time": "2024-11-24T11:22:49+00:00" }, { "name": "fruitcake/php-cors", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "58571acbaa5f9f462c9c77e911700ac66f446d4e" + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/58571acbaa5f9f462c9c77e911700ac66f446d4e", - "reference": "58571acbaa5f9f462c9c77e911700ac66f446d4e", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6" + "symfony/http-foundation": "^4.4|^5.4|^6|^7" }, "require-dev": { "phpstan/phpstan": "^1.4", @@ -1624,7 +1515,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -1655,7 +1546,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.2.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" }, "funding": [ { @@ -1667,28 +1558,28 @@ "type": "github" } ], - "time": "2022-02-20T15:07:15+00:00" + "time": "2023-10-12T05:21:21+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.1", + "version": "v1.1.3", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", - "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.1" + "phpoption/phpoption": "^1.9.3" }, "require-dev": { - "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" }, "type": "library", "autoload": { @@ -1717,7 +1608,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" }, "funding": [ { @@ -1729,26 +1620,26 @@ "type": "tidelift" } ], - "time": "2023-02-25T20:23:15+00:00" + "time": "2024-07-20T21:45:45+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.7.0", + "version": "7.9.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5" + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5", - "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -1757,11 +1648,11 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "guzzle/client-integration-tests": "3.0.2", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -1839,7 +1730,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.7.0" + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" }, "funding": [ { @@ -1855,33 +1746,37 @@ "type": "tidelift" } ], - "time": "2023-05-21T14:04:53+00:00" + "time": "2024-07-24T11:22:20+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", - "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Promise\\": "src/" } @@ -1918,7 +1813,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.3" + "source": "https://github.com/guzzle/promises/tree/2.0.4" }, "funding": [ { @@ -1934,20 +1829,20 @@ "type": "tidelift" } ], - "time": "2023-05-21T12:31:43+00:00" + "time": "2024-10-17T10:06:22+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.5.0", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6" + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", "shasum": "" }, "require": { @@ -1961,9 +1856,9 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -2034,7 +1929,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.5.0" + "source": "https://github.com/guzzle/psr7/tree/2.7.0" }, "funding": [ { @@ -2050,34 +1945,36 @@ "type": "tidelift" } ], - "time": "2023-04-17T16:11:26+00:00" + "time": "2024-07-18T11:15:46+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.1", + "version": "v1.0.3", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "b945d74a55a25a949158444f09ec0d3c120d69e2" + "reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/b945d74a55a25a949158444f09ec0d3c120d69e2", - "reference": "b945d74a55a25a949158444f09ec0d3c120d69e2", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/ecea8feef63bd4fef1f037ecb288386999ecc11c", + "reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "symfony/polyfill-php80": "^1.17" + "symfony/polyfill-php80": "^1.24" }, "require-dev": { - "phpunit/phpunit": "^8.5.19 || ^9.5.8", + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", "uri-template/tests": "1.0.0" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { @@ -2118,7 +2015,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.1" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.3" }, "funding": [ { @@ -2134,7 +2031,7 @@ "type": "tidelift" } ], - "time": "2021-10-07T12:57:01+00:00" + "time": "2023-12-03T19:50:20+00:00" }, { "name": "intervention/image", @@ -2166,16 +2063,16 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.4-dev" - }, "laravel": { - "providers": [ - "Intervention\\Image\\ImageServiceProvider" - ], "aliases": { "Image": "Intervention\\Image\\Facades\\Image" - } + }, + "providers": [ + "Intervention\\Image\\ImageServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "2.4-dev" } }, "autoload": { @@ -2222,20 +2119,20 @@ }, { "name": "jaybizzle/crawler-detect", - "version": "v1.2.115", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/JayBizzle/Crawler-Detect.git", - "reference": "4531e4a70d55d10cbe7d41ac1ff0d75a5fe2ef1e" + "reference": "be155e11613fa618aa18aee438955588d1092a47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/4531e4a70d55d10cbe7d41ac1ff0d75a5fe2ef1e", - "reference": "4531e4a70d55d10cbe7d41ac1ff0d75a5fe2ef1e", + "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/be155e11613fa618aa18aee438955588d1092a47", + "reference": "be155e11613fa618aa18aee438955588d1092a47", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.1.0" }, "require-dev": { "phpunit/phpunit": "^4.8|^5.5|^6.5|^9.4" @@ -2268,9 +2165,9 @@ ], "support": { "issues": "https://github.com/JayBizzle/Crawler-Detect/issues", - "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.115" + "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.3.0" }, - "time": "2023-06-05T21:32:18+00:00" + "time": "2024-11-25T19:38:36+00:00" }, { "name": "jenssegers/agent", @@ -2356,24 +2253,146 @@ "time": "2020-06-13T08:05:20+00:00" }, { - "name": "laravel/framework", - "version": "v10.15.0", + "name": "laravel-notification-channels/expo", + "version": "v2.0.0", "source": { "type": "git", - "url": "https://github.com/laravel/framework.git", - "reference": "c7599dc92e04532824bafbd226c2936ce6a905b8" + "url": "https://github.com/laravel-notification-channels/expo.git", + "reference": "29d038b6409077ac4c671cc5587a4dc7986260b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/c7599dc92e04532824bafbd226c2936ce6a905b8", - "reference": "c7599dc92e04532824bafbd226c2936ce6a905b8", + "url": "https://api.github.com/repos/laravel-notification-channels/expo/zipball/29d038b6409077ac4c671cc5587a4dc7986260b0", + "reference": "29d038b6409077ac4c671cc5587a4dc7986260b0", "shasum": "" }, "require": { - "brick/math": "^0.9.3|^0.10.2|^0.11", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.1", + "illuminate/contracts": "^11.0", + "illuminate/notifications": "^11.0", + "illuminate/support": "^11.0", + "php": "~8.3" + }, + "require-dev": { + "larastan/larastan": "^2.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^9.0", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-zlib": "Required for compressing payloads exceeding 1 KiB in size." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NotificationChannels\\Expo\\ExpoServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NotificationChannels\\Expo\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Muhammed Sari", + "email": "muhammed@dive.be", + "homepage": "https://dive.be", + "role": "Developer" + } + ], + "description": "Expo Notifications Channel for Laravel", + "homepage": "https://github.com/laravel-notification-channels/expo", + "support": { + "issues": "https://github.com/laravel-notification-channels/expo/issues", + "source": "https://github.com/laravel-notification-channels/expo/tree/v2.0.0" + }, + "time": "2024-03-18T07:49:28+00:00" + }, + { + "name": "laravel-notification-channels/webpush", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/laravel-notification-channels/webpush.git", + "reference": "6e7a60558721f674172664283467a69ab93f3d5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel-notification-channels/webpush/zipball/6e7a60558721f674172664283467a69ab93f3d5e", + "reference": "6e7a60558721f674172664283467a69ab93f3d5e", + "shasum": "" + }, + "require": { + "illuminate/notifications": "^9.0|^10.0|^11.0", + "illuminate/support": "^9.0|^10.0|^11.0", + "minishlink/web-push": "^8.0", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "~1.0", + "orchestra/testbench": "^7.0|^8.0|^9.0", + "phpunit/phpunit": "^9.5|^10.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NotificationChannels\\WebPush\\WebPushServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NotificationChannels\\WebPush\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cretu Eusebiu", + "email": "me@cretueusebiu.com", + "homepage": "http://cretueusebiu.com", + "role": "Developer" + } + ], + "description": "Web Push Notifications driver for Laravel.", + "homepage": "https://github.com/laravel-notification-channels/webpush", + "support": { + "issues": "https://github.com/laravel-notification-channels/webpush/issues", + "source": "https://github.com/laravel-notification-channels/webpush/tree/8.0.0" + }, + "time": "2024-03-16T05:36:52+00:00" + }, + { + "name": "laravel/framework", + "version": "v11.36.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "df06f5163f4550641fdf349ebc04916a61135a64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/df06f5163f4550641fdf349ebc04916a61135a64", + "reference": "df06f5163f4550641fdf349ebc04916a61135a64", + "shasum": "" + }, + "require": { + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", - "dragonmantank/cron-expression": "^3.3.2", + "dragonmantank/cron-expression": "^3.4", "egulias/email-validator": "^3.2.1|^4.0", "ext-ctype": "*", "ext-filter": "*", @@ -2382,39 +2401,46 @@ "ext-openssl": "*", "ext-session": "*", "ext-tokenizer": "*", - "fruitcake/php-cors": "^1.2", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", "guzzlehttp/uri-template": "^1.0", - "laravel/serializable-closure": "^1.3", - "league/commonmark": "^2.2.1", - "league/flysystem": "^3.8.0", + "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.6", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", "monolog/monolog": "^3.0", - "nesbot/carbon": "^2.62.1", - "nunomaduro/termwind": "^1.13", - "php": "^8.1", + "nesbot/carbon": "^2.72.2|^3.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "ramsey/uuid": "^4.7", - "symfony/console": "^6.2", - "symfony/error-handler": "^6.2", - "symfony/finder": "^6.2", - "symfony/http-foundation": "^6.2", - "symfony/http-kernel": "^6.2", - "symfony/mailer": "^6.2", - "symfony/mime": "^6.2", - "symfony/process": "^6.2", - "symfony/routing": "^6.2", - "symfony/uid": "^6.2", - "symfony/var-dumper": "^6.2", + "symfony/console": "^7.0.3", + "symfony/error-handler": "^7.0.3", + "symfony/finder": "^7.0.3", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.0.3", + "symfony/mailer": "^7.0.3", + "symfony/mime": "^7.0.3", + "symfony/polyfill-php83": "^1.31", + "symfony/process": "^7.0.3", + "symfony/routing": "^7.0.3", + "symfony/uid": "^7.0.3", + "symfony/var-dumper": "^7.0.3", "tijsverkoyen/css-to-inline-styles": "^2.2.5", - "vlucas/phpdotenv": "^5.4.1", - "voku/portable-ascii": "^2.0" + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" }, "conflict": { + "mockery/mockery": "1.6.8", "tightenco/collect": "<5.5.33" }, "provide": { "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", "psr/simple-cache-implementation": "1.0|2.0|3.0" }, "replace": { @@ -2423,6 +2449,7 @@ "illuminate/bus": "self.version", "illuminate/cache": "self.version", "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", "illuminate/conditionable": "self.version", "illuminate/config": "self.version", "illuminate/console": "self.version", @@ -2450,35 +2477,38 @@ "illuminate/testing": "self.version", "illuminate/translation": "self.version", "illuminate/validation": "self.version", - "illuminate/view": "self.version" + "illuminate/view": "self.version", + "spatie/once": "*" }, "require-dev": { "ably/ably-php": "^1.0", - "aws/aws-sdk-php": "^3.235.5", - "doctrine/dbal": "^3.5.1", + "aws/aws-sdk-php": "^3.322.9", "ext-gmp": "*", - "fakerphp/faker": "^1.21", - "guzzlehttp/guzzle": "^7.5", - "league/flysystem-aws-s3-v3": "^3.0", - "league/flysystem-ftp": "^3.0", - "league/flysystem-path-prefixing": "^3.3", - "league/flysystem-read-only": "^3.3", - "league/flysystem-sftp-v3": "^3.0", - "mockery/mockery": "^1.5.1", - "orchestra/testbench-core": "^8.4", - "pda/pheanstalk": "^4.0", - "phpstan/phpdoc-parser": "^1.15", - "phpstan/phpstan": "^1.4.7", - "phpunit/phpunit": "^10.0.7", - "predis/predis": "^2.0.2", - "symfony/cache": "^6.2", - "symfony/http-client": "^6.2.4" + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^9.6", + "pda/pheanstalk": "^5.0.6", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^1.11.5", + "phpunit/phpunit": "^10.5.35|^11.3.6", + "predis/predis": "^2.3", + "resend/resend-php": "^0.10.0", + "symfony/cache": "^7.0.3", + "symfony/http-client": "^7.0.3", + "symfony/psr-http-message-bridge": "^7.0.3", + "symfony/translation": "^7.0.3" }, "suggest": { "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", - "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).", - "brianium/paratest": "Required to run tests in parallel (^6.0).", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", "ext-apcu": "Required to use the APC cache driver.", "ext-fileinfo": "Required to use the Filesystem class.", "ext-ftp": "Required to use the Flysystem FTP driver.", @@ -2487,41 +2517,45 @@ "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", "ext-pdo": "Required to use all database features.", "ext-posix": "Required to use all features of the queue worker.", - "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", - "guzzlehttp/guzzle": "Required to use the HTTP Client and the ping methods on schedules (^7.5).", "laravel/tinker": "Required to use the tinker console command (^2.0).", - "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", - "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", - "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.3).", - "league/flysystem-read-only": "Required to use read-only disks (^3.3)", - "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", - "mockery/mockery": "Required to use mocking (^1.5.1).", - "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", - "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", - "phpunit/phpunit": "Required to use assertions and run tests (^9.5.8|^10.0.7).", - "predis/predis": "Required to use the predis connector (^2.0.2).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).", + "predis/predis": "Required to use the predis connector (^2.3).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^6.2).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^6.2).", - "symfony/http-client": "Required to enable support for the Symfony API mail transports (^6.2).", - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.2).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.2).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0)." + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "10.x-dev" + "dev-master": "11.x-dev" } }, "autoload": { "files": [ + "src/Illuminate/Collections/functions.php", "src/Illuminate/Collections/helpers.php", "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], "psr-4": { @@ -2553,28 +2587,29 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2023-07-11T13:43:52+00:00" + "time": "2024-12-17T22:32:08+00:00" }, { "name": "laravel/helpers", - "version": "v1.6.0", + "version": "v1.7.1", "source": { "type": "git", "url": "https://github.com/laravel/helpers.git", - "reference": "4dd0f9436d3911611622a6ced8329a1710576f60" + "reference": "f28907033d7edf8a0525cfb781ab30ce6d531c35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/helpers/zipball/4dd0f9436d3911611622a6ced8329a1710576f60", - "reference": "4dd0f9436d3911611622a6ced8329a1710576f60", + "url": "https://api.github.com/repos/laravel/helpers/zipball/f28907033d7edf8a0525cfb781ab30ce6d531c35", + "reference": "f28907033d7edf8a0525cfb781ab30ce6d531c35", "shasum": "" }, "require": { - "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0", - "php": "^7.1.3|^8.0" + "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "php": "^7.2.0|^8.0" }, "require-dev": { - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^7.0|^8.0|^9.0|^10.0" }, "type": "library", "extra": { @@ -2607,42 +2642,44 @@ "laravel" ], "support": { - "source": "https://github.com/laravel/helpers/tree/v1.6.0" + "source": "https://github.com/laravel/helpers/tree/v1.7.1" }, - "time": "2023-01-09T14:48:11+00:00" + "time": "2024-11-26T14:56:25+00:00" }, { "name": "laravel/horizon", - "version": "v5.18.0", + "version": "v5.30.1", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "b14498a09af826035e46ae8d6b013d0ec849bdb7" + "reference": "77177646679ef2f2acf71d4d4b16036d18002040" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/b14498a09af826035e46ae8d6b013d0ec849bdb7", - "reference": "b14498a09af826035e46ae8d6b013d0ec849bdb7", + "url": "https://api.github.com/repos/laravel/horizon/zipball/77177646679ef2f2acf71d4d4b16036d18002040", + "reference": "77177646679ef2f2acf71d4d4b16036d18002040", "shasum": "" }, "require": { "ext-json": "*", "ext-pcntl": "*", "ext-posix": "*", - "illuminate/contracts": "^8.17|^9.0|^10.0", - "illuminate/queue": "^8.17|^9.0|^10.0", - "illuminate/support": "^8.17|^9.0|^10.0", - "nesbot/carbon": "^2.17", - "php": "^7.3|^8.0", + "illuminate/contracts": "^9.21|^10.0|^11.0", + "illuminate/queue": "^9.21|^10.0|^11.0", + "illuminate/support": "^9.21|^10.0|^11.0", + "nesbot/carbon": "^2.17|^3.0", + "php": "^8.0", "ramsey/uuid": "^4.0", - "symfony/error-handler": "^5.0|^6.0", - "symfony/process": "^5.0|^6.0" + "symfony/console": "^6.0|^7.0", + "symfony/error-handler": "^6.0|^7.0", + "symfony/polyfill-php83": "^1.28", + "symfony/process": "^6.0|^7.0" }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "^6.0|^7.0|^8.0", + "orchestra/testbench": "^7.0|^8.0|^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.0", + "phpunit/phpunit": "^9.0|^10.4", "predis/predis": "^1.1|^2.0" }, "suggest": { @@ -2651,16 +2688,16 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - }, "laravel": { - "providers": [ - "Laravel\\Horizon\\HorizonServiceProvider" - ], "aliases": { "Horizon": "Laravel\\Horizon\\Horizon" - } + }, + "providers": [ + "Laravel\\Horizon\\HorizonServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" } }, "autoload": { @@ -2685,54 +2722,52 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.18.0" + "source": "https://github.com/laravel/horizon/tree/v5.30.1" }, - "time": "2023-06-30T15:11:51+00:00" + "time": "2024-12-13T14:08:51+00:00" }, { "name": "laravel/passport", - "version": "v11.8.8", + "version": "v12.3.1", "source": { "type": "git", "url": "https://github.com/laravel/passport.git", - "reference": "401836130d46c94138a637ada29f9e5b2bf053b6" + "reference": "0d95ca9cc9c80bdf64d04dcf04542720e3d5d55c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/passport/zipball/401836130d46c94138a637ada29f9e5b2bf053b6", - "reference": "401836130d46c94138a637ada29f9e5b2bf053b6", + "url": "https://api.github.com/repos/laravel/passport/zipball/0d95ca9cc9c80bdf64d04dcf04542720e3d5d55c", + "reference": "0d95ca9cc9c80bdf64d04dcf04542720e3d5d55c", "shasum": "" }, "require": { "ext-json": "*", "firebase/php-jwt": "^6.4", - "illuminate/auth": "^9.0|^10.0", - "illuminate/console": "^9.0|^10.0", - "illuminate/container": "^9.0|^10.0", - "illuminate/contracts": "^9.0|^10.0", - "illuminate/cookie": "^9.0|^10.0", - "illuminate/database": "^9.0|^10.0", - "illuminate/encryption": "^9.0|^10.0", - "illuminate/http": "^9.0|^10.0", - "illuminate/support": "^9.0|^10.0", + "illuminate/auth": "^9.21|^10.0|^11.0", + "illuminate/console": "^9.21|^10.0|^11.0", + "illuminate/container": "^9.21|^10.0|^11.0", + "illuminate/contracts": "^9.21|^10.0|^11.0", + "illuminate/cookie": "^9.21|^10.0|^11.0", + "illuminate/database": "^9.21|^10.0|^11.0", + "illuminate/encryption": "^9.21|^10.0|^11.0", + "illuminate/http": "^9.21|^10.0|^11.0", + "illuminate/support": "^9.21|^10.0|^11.0", "lcobucci/jwt": "^4.3|^5.0", "league/oauth2-server": "^8.5.3", "nyholm/psr7": "^1.5", "php": "^8.0", "phpseclib/phpseclib": "^2.0|^3.0", - "symfony/psr-http-message-bridge": "^2.1" + "symfony/console": "^6.0|^7.0", + "symfony/psr-http-message-bridge": "^2.1|^6.0|^7.0" }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "^7.0|^8.0", + "orchestra/testbench": "^7.35|^8.14|^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.3|^10.5" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "11.x-dev" - }, "laravel": { "providers": [ "Laravel\\Passport\\PassportServiceProvider" @@ -2765,35 +2800,95 @@ "issues": "https://github.com/laravel/passport/issues", "source": "https://github.com/laravel/passport" }, - "time": "2023-07-07T06:37:11+00:00" + "time": "2024-11-11T20:15:28+00:00" }, { - "name": "laravel/serializable-closure", - "version": "v1.3.0", + "name": "laravel/prompts", + "version": "v0.3.2", "source": { "type": "git", - "url": "https://github.com/laravel/serializable-closure.git", - "reference": "f23fe9d4e95255dacee1bf3525e0810d1a1b0f37" + "url": "https://github.com/laravel/prompts.git", + "reference": "0e0535747c6b8d6d10adca8b68293cf4517abb0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f23fe9d4e95255dacee1bf3525e0810d1a1b0f37", - "reference": "f23fe9d4e95255dacee1bf3525e0810d1a1b0f37", + "url": "https://api.github.com/repos/laravel/prompts/zipball/0e0535747c6b8d6d10adca8b68293cf4517abb0f", + "reference": "0e0535747c6b8d6d10adca8b68293cf4517abb0f", "shasum": "" }, "require": { - "php": "^7.3|^8.0" + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" }, "require-dev": { - "nesbot/carbon": "^2.61", - "pestphp/pest": "^1.21.3", - "phpstan/phpstan": "^1.8.2", - "symfony/var-dumper": "^5.4.11" + "illuminate/collections": "^10.0|^11.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-mockery": "^1.1" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.2" + }, + "time": "2024-11-12T14:59:47+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/613b2d4998f85564d40497e05e89cb6d9bd1cbe8", + "reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" } }, "autoload": { @@ -2825,42 +2920,40 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2023-01-30T18:31:20+00:00" + "time": "2024-12-16T15:26:28+00:00" }, { "name": "laravel/tinker", - "version": "v2.8.1", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "04a2d3bd0d650c0764f70bf49d1ee39393e4eb10" + "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/04a2d3bd0d650c0764f70bf49d1ee39393e4eb10", - "reference": "04a2d3bd0d650c0764f70bf49d1ee39393e4eb10", + "url": "https://api.github.com/repos/laravel/tinker/zipball/ba4d51eb56de7711b3a37d63aa0643e99a339ae5", + "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5", "shasum": "" }, "require": { - "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "php": "^7.2.5|^8.0", - "psy/psysh": "^0.10.4|^0.11.1", - "symfony/var-dumper": "^4.3.4|^5.0|^6.0" + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" }, "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^8.5.8|^9.3.3" }, "suggest": { - "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0)." + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0)." }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - }, "laravel": { "providers": [ "Laravel\\Tinker\\TinkerServiceProvider" @@ -2891,34 +2984,35 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.8.1" + "source": "https://github.com/laravel/tinker/tree/v2.10.0" }, - "time": "2023-02-15T16:40:09+00:00" + "time": "2024-09-23T13:32:56+00:00" }, { "name": "laravel/ui", - "version": "v4.2.2", + "version": "v4.6.0", "source": { "type": "git", "url": "https://github.com/laravel/ui.git", - "reference": "a58ec468db4a340b33f3426c778784717a2c144b" + "reference": "a34609b15ae0c0512a0cf47a21695a2729cb7f93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/ui/zipball/a58ec468db4a340b33f3426c778784717a2c144b", - "reference": "a58ec468db4a340b33f3426c778784717a2c144b", + "url": "https://api.github.com/repos/laravel/ui/zipball/a34609b15ae0c0512a0cf47a21695a2729cb7f93", + "reference": "a34609b15ae0c0512a0cf47a21695a2729cb7f93", "shasum": "" }, "require": { - "illuminate/console": "^9.21|^10.0", - "illuminate/filesystem": "^9.21|^10.0", - "illuminate/support": "^9.21|^10.0", - "illuminate/validation": "^9.21|^10.0", - "php": "^8.0" + "illuminate/console": "^9.21|^10.0|^11.0", + "illuminate/filesystem": "^9.21|^10.0|^11.0", + "illuminate/support": "^9.21|^10.0|^11.0", + "illuminate/validation": "^9.21|^10.0|^11.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0" }, "require-dev": { - "orchestra/testbench": "^7.0|^8.0", - "phpunit/phpunit": "^9.3" + "orchestra/testbench": "^7.35|^8.15|^9.0", + "phpunit/phpunit": "^9.3|^10.4|^11.0" }, "type": "library", "extra": { @@ -2953,40 +3047,40 @@ "ui" ], "support": { - "source": "https://github.com/laravel/ui/tree/v4.2.2" + "source": "https://github.com/laravel/ui/tree/v4.6.0" }, - "time": "2023-05-09T19:47:28+00:00" + "time": "2024-11-21T15:06:41+00:00" }, { "name": "lcobucci/clock", - "version": "3.0.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/lcobucci/clock.git", - "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc" + "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/039ef98c6b57b101d10bd11d8fdfda12cbd996dc", - "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", + "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "psr/clock": "^1.0" }, "provide": { "psr/clock-implementation": "1.0" }, "require-dev": { - "infection/infection": "^0.26", - "lcobucci/coding-standard": "^9.0", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-deprecation-rules": "^1.1.1", - "phpstan/phpstan-phpunit": "^1.3.2", - "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^9.5.27" + "infection/infection": "^0.29", + "lcobucci/coding-standard": "^11.1.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.10.25", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.13", + "phpstan/phpstan-strict-rules": "^1.5.1", + "phpunit/phpunit": "^11.3.6" }, "type": "library", "autoload": { @@ -3007,7 +3101,7 @@ "description": "Yet another clock abstraction", "support": { "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/3.0.0" + "source": "https://github.com/lcobucci/clock/tree/3.3.1" }, "funding": [ { @@ -3019,44 +3113,42 @@ "type": "patreon" } ], - "time": "2022-12-19T15:00:24+00:00" + "time": "2024-09-24T20:45:14+00:00" }, { "name": "lcobucci/jwt", - "version": "5.0.0", + "version": "5.4.2", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34" + "reference": "ea1ce71cbf9741e445a5914e2f67cdbb484ff712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34", - "reference": "47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/ea1ce71cbf9741e445a5914e2f67cdbb484ff712", + "reference": "ea1ce71cbf9741e445a5914e2f67cdbb484ff712", "shasum": "" }, "require": { - "ext-hash": "*", - "ext-json": "*", "ext-openssl": "*", "ext-sodium": "*", - "php": "~8.1.0 || ~8.2.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "psr/clock": "^1.0" }, "require-dev": { - "infection/infection": "^0.26.19", - "lcobucci/clock": "^3.0", - "lcobucci/coding-standard": "^9.0", - "phpbench/phpbench": "^1.2.8", + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.10.3", - "phpstan/phpstan-deprecation-rules": "^1.1.2", - "phpstan/phpstan-phpunit": "^1.3.8", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^10.0.12" + "phpunit/phpunit": "^11.1" }, "suggest": { - "lcobucci/clock": ">= 3.0" + "lcobucci/clock": ">= 3.2" }, "type": "library", "autoload": { @@ -3082,7 +3174,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.0.0" + "source": "https://github.com/lcobucci/jwt/tree/5.4.2" }, "funding": [ { @@ -3094,20 +3186,20 @@ "type": "patreon" } ], - "time": "2023-02-25T21:35:16+00:00" + "time": "2024-11-07T12:54:35+00:00" }, { "name": "league/commonmark", - "version": "2.4.0", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048" + "reference": "d150f911e0079e90ae3c106734c93137c184f932" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d44a24690f16b8c1808bf13b1bd54ae4c63ea048", - "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d150f911e0079e90ae3c106734c93137c184f932", + "reference": "d150f911e0079e90ae3c106734c93137c184f932", "shasum": "" }, "require": { @@ -3120,8 +3212,8 @@ }, "require-dev": { "cebe/markdown": "^1.0", - "commonmark/cmark": "0.30.0", - "commonmark/commonmark.js": "0.30.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", "erusev/parsedown": "^1.0", @@ -3130,10 +3222,11 @@ "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0" }, @@ -3143,7 +3236,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "2.7-dev" } }, "autoload": { @@ -3200,7 +3293,7 @@ "type": "tidelift" } ], - "time": "2023-03-24T15:16:10+00:00" + "time": "2024-12-07T15:34:16+00:00" }, { "name": "league/config", @@ -3340,16 +3433,16 @@ }, { "name": "league/flysystem", - "version": "3.15.1", + "version": "3.29.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "a141d430414fcb8bf797a18716b09f759a385bed" + "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a141d430414fcb8bf797a18716b09f759a385bed", - "reference": "a141d430414fcb8bf797a18716b09f759a385bed", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", + "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", "shasum": "" }, "require": { @@ -3358,6 +3451,8 @@ "php": "^8.0.2" }, "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", "aws/aws-sdk-php": "3.209.31 || 3.210.0", "guzzlehttp/guzzle": "<7.0", "guzzlehttp/ringphp": "<1.1.1", @@ -3365,20 +3460,23 @@ "symfony/http-client": "<5.2" }, "require-dev": { - "async-aws/s3": "^1.5", - "async-aws/simple-s3": "^1.1", - "aws/aws-sdk-php": "^3.220.0", + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", + "ext-mongodb": "^1.3", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", "microsoft/azure-storage-blob": "^1.1", - "phpseclib/phpseclib": "^3.0.14", - "phpstan/phpstan": "^0.12.26", - "phpunit/phpunit": "^9.5.11", - "sabre/dav": "^4.3.1" + "mongodb/mongodb": "^1.2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" }, "type": "library", "autoload": { @@ -3412,36 +3510,26 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.15.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" }, - "funding": [ - { - "url": "https://ecologi.com/frankdejonge", - "type": "custom" - }, - { - "url": "https://github.com/frankdejonge", - "type": "github" - } - ], - "time": "2023-05-04T09:04:26+00:00" + "time": "2024-10-08T08:58:34+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.15.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a" + "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d8de61ee10b6a607e7996cff388c5a3a663e8c8a", - "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c6ff6d4606e48249b63f269eba7fabdb584e76a9", + "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9", "shasum": "" }, "require": { - "aws/aws-sdk-php": "^3.220.0", + "aws/aws-sdk-php": "^3.295.10", "league/flysystem": "^3.10.0", "league/mime-type-detection": "^1.0.0", "php": "^8.0.2" @@ -3477,33 +3565,22 @@ "storage" ], "support": { - "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues", - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.15.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.29.0" }, - "funding": [ - { - "url": "https://ecologi.com/frankdejonge", - "type": "custom" - }, - { - "url": "https://github.com/frankdejonge", - "type": "github" - } - ], - "time": "2023-05-02T20:02:14+00:00" + "time": "2024-08-17T13:10:48+00:00" }, { "name": "league/flysystem-local", - "version": "3.15.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "543f64c397fefdf9cfeac443ffb6beff602796b3" + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/543f64c397fefdf9cfeac443ffb6beff602796b3", - "reference": "543f64c397fefdf9cfeac443ffb6beff602796b3", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", "shasum": "" }, "require": { @@ -3537,46 +3614,38 @@ "local" ], "support": { - "issues": "https://github.com/thephpleague/flysystem-local/issues", - "source": "https://github.com/thephpleague/flysystem-local/tree/3.15.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" }, - "funding": [ - { - "url": "https://ecologi.com/frankdejonge", - "type": "custom" - }, - { - "url": "https://github.com/frankdejonge", - "type": "github" - } - ], - "time": "2023-05-02T20:02:14+00:00" + "time": "2024-08-09T21:24:39+00:00" }, { "name": "league/iso3166", - "version": "4.3.0", + "version": "4.3.2", "source": { "type": "git", - "url": "https://github.com/thephpleague/iso3166.git", - "reference": "628f1b4992169917f3f59c14020ea4513c63f6db" + "url": "https://github.com/alcohol/iso3166.git", + "reference": "5133fed7d54728222f4058702487dccedda20472" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/iso3166/zipball/628f1b4992169917f3f59c14020ea4513c63f6db", - "reference": "628f1b4992169917f3f59c14020ea4513c63f6db", + "url": "https://api.github.com/repos/alcohol/iso3166/zipball/5133fed7d54728222f4058702487dccedda20472", + "reference": "5133fed7d54728222f4058702487dccedda20472", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^7.3|^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpstan/phpstan": "^1.12.6", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-strict-rules": "^1.6.1", + "phpunit/phpunit": "^9.6.21" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "4.x-dev" } }, "autoload": { @@ -3595,7 +3664,7 @@ } ], "description": "ISO 3166-1 PHP Library", - "homepage": "https://github.com/thephpleague/iso3166", + "homepage": "https://github.com/alcohol/iso3166", "keywords": [ "3166", "3166-1", @@ -3605,33 +3674,39 @@ "library" ], "support": { - "issues": "https://github.com/thephpleague/iso3166/issues", - "source": "https://github.com/thephpleague/iso3166" + "issues": "https://github.com/alcohol/iso3166/issues", + "source": "https://github.com/alcohol/iso3166" }, - "time": "2023-06-05T15:02:58+00:00" + "funding": [ + { + "url": "https://github.com/alcohol", + "type": "github" + } + ], + "time": "2024-10-10T07:39:24+00:00" }, { "name": "league/mime-type-detection", - "version": "1.11.0", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd" + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ff6248ea87a9f116e78edd6002e39e5128a0d4dd", - "reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", "shasum": "" }, "require": { "ext-fileinfo": "*", - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "phpstan/phpstan": "^0.12.68", - "phpunit/phpunit": "^8.5.8 || ^9.3" + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" }, "type": "library", "autoload": { @@ -3652,7 +3727,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.11.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" }, "funding": [ { @@ -3664,20 +3739,20 @@ "type": "tidelift" } ], - "time": "2022-04-17T13:12:02+00:00" + "time": "2024-09-21T08:32:55+00:00" }, { "name": "league/oauth2-server", - "version": "8.5.3", + "version": "8.5.5", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth2-server.git", - "reference": "eb91b4190e7f6169053ebf8ffa352d47e756b2ce" + "reference": "cc8778350f905667e796b3c2364a9d3bd7a73518" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/eb91b4190e7f6169053ebf8ffa352d47e756b2ce", - "reference": "eb91b4190e7f6169053ebf8ffa352d47e756b2ce", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/cc8778350f905667e796b3c2364a9d3bd7a73518", + "reference": "cc8778350f905667e796b3c2364a9d3bd7a73518", "shasum": "" }, "require": { @@ -3686,7 +3761,7 @@ "lcobucci/clock": "^2.2 || ^3.0", "lcobucci/jwt": "^4.3 || ^5.0", "league/event": "^2.2", - "league/uri": "^6.7", + "league/uri": "^6.7 || ^7.0", "php": "^8.0", "psr/http-message": "^1.0.1 || ^2.0" }, @@ -3744,7 +3819,7 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth2-server/issues", - "source": "https://github.com/thephpleague/oauth2-server/tree/8.5.3" + "source": "https://github.com/thephpleague/oauth2-server/tree/8.5.5" }, "funding": [ { @@ -3752,58 +3827,48 @@ "type": "github" } ], - "time": "2023-07-05T23:01:32+00:00" + "time": "2024-12-20T23:06:10+00:00" }, { "name": "league/uri", - "version": "6.8.0", + "version": "7.5.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "a700b4656e4c54371b799ac61e300ab25a2d1d39" + "reference": "81fb5145d2644324614cc532b28efd0215bda430" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/a700b4656e4c54371b799ac61e300ab25a2d1d39", - "reference": "a700b4656e4c54371b799ac61e300ab25a2d1d39", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", "shasum": "" }, "require": { - "ext-json": "*", - "league/uri-interfaces": "^2.3", - "php": "^8.1", - "psr/http-message": "^1.0.1" + "league/uri-interfaces": "^7.5", + "php": "^8.1" }, "conflict": { "league/uri-schemes": "^1.0" }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^v3.9.5", - "nyholm/psr7": "^1.5.1", - "php-http/psr7-integration-tests": "^1.1.1", - "phpbench/phpbench": "^1.2.6", - "phpstan/phpstan": "^1.8.5", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.1.1", - "phpstan/phpstan-strict-rules": "^1.4.3", - "phpunit/phpunit": "^9.5.24", - "psr/http-factory": "^1.0.1" - }, "suggest": { - "ext-fileinfo": "Needed to create Data URI from a filepath", - "ext-intl": "Needed to improve host validation", - "league/uri-components": "Needed to easily manipulate URI objects", - "psr/http-factory": "Needed to use the URI factory" + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { "psr-4": { - "League\\Uri\\": "src" + "League\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3843,8 +3908,8 @@ "support": { "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri/issues", - "source": "https://github.com/thephpleague/uri/tree/6.8.0" + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" }, "funding": [ { @@ -3852,46 +3917,44 @@ "type": "github" } ], - "time": "2022-09-13T19:58:47+00:00" + "time": "2024-12-08T08:40:02+00:00" }, { "name": "league/uri-interfaces", - "version": "2.3.0", + "version": "7.5.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383" + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", - "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", "shasum": "" }, "require": { - "ext-json": "*", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.19", - "phpstan/phpstan": "^0.12.90", - "phpstan/phpstan-phpunit": "^0.12.19", - "phpstan/phpstan-strict-rules": "^0.12.9", - "phpunit/phpunit": "^8.5.15 || ^9.5" + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" }, "suggest": { - "ext-intl": "to use the IDNA feature", - "symfony/intl": "to use the IDNA feature via Symfony Polyfill" + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { "psr-4": { - "League\\Uri\\": "src/" + "League\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3905,17 +3968,32 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interface for URI representation", - "homepage": "http://github.com/thephpleague/uri-interfaces", + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", "rfc3986", "rfc3987", + "rfc6570", "uri", - "url" + "url", + "ws" ], "support": { - "issues": "https://github.com/thephpleague/uri-interfaces/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/2.3.0" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" }, "funding": [ { @@ -3923,27 +4001,94 @@ "type": "github" } ], - "time": "2021-06-28T04:27:21+00:00" + "time": "2024-12-08T08:18:47+00:00" }, { - "name": "mobiledetect/mobiledetectlib", - "version": "2.8.41", + "name": "minishlink/web-push", + "version": "v8.0.0", "source": { "type": "git", - "url": "https://github.com/serbanghita/Mobile-Detect.git", - "reference": "fc9cccd4d3706d5a7537b562b59cc18f9e4c0cb1" + "url": "https://github.com/web-push-libs/web-push-php.git", + "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/fc9cccd4d3706d5a7537b562b59cc18f9e4c0cb1", - "reference": "fc9cccd4d3706d5a7537b562b59cc18f9e4c0cb1", + "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/ec034f1e287cd1e74235e349bd017d71a61e9d8d", + "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^7.0.1|^6.2", + "php": ">=8.0", + "spomky-labs/base64url": "^2.0", + "web-token/jwt-key-mgmt": "^2.0|^3.0.2", + "web-token/jwt-signature": "^2.0|^3.0.2", + "web-token/jwt-signature-algorithm-ecdsa": "^2.0|^3.0.2", + "web-token/jwt-util-ecc": "^2.0|^3.0.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.13.2", + "phpstan/phpstan": "^1.9.8", + "phpunit/phpunit": "^9.5.27" + }, + "suggest": { + "ext-gmp": "Optional for performance." + }, + "type": "library", + "autoload": { + "psr-4": { + "Minishlink\\WebPush\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Louis Lagrange", + "email": "lagrange.louis@gmail.com", + "homepage": "https://github.com/Minishlink" + } + ], + "description": "Web Push library for PHP", + "homepage": "https://github.com/web-push-libs/web-push-php", + "keywords": [ + "Push API", + "WebPush", + "notifications", + "push", + "web" + ], + "support": { + "issues": "https://github.com/web-push-libs/web-push-php/issues", + "source": "https://github.com/web-push-libs/web-push-php/tree/v8.0.0" + }, + "time": "2023-01-10T17:14:44+00:00" + }, + { + "name": "mobiledetect/mobiledetectlib", + "version": "2.8.45", + "source": { + "type": "git", + "url": "https://github.com/serbanghita/Mobile-Detect.git", + "reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/96aaebcf4f50d3d2692ab81d2c5132e425bca266", + "reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266", "shasum": "" }, "require": { "php": ">=5.0.0" }, "require-dev": { - "phpunit/phpunit": "~4.8.35||~5.7" + "phpunit/phpunit": "~4.8.36" }, "type": "library", "autoload": { @@ -3977,22 +4122,28 @@ ], "support": { "issues": "https://github.com/serbanghita/Mobile-Detect/issues", - "source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.41" + "source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.45" }, - "time": "2022-11-08T18:31:26+00:00" + "funding": [ + { + "url": "https://github.com/serbanghita", + "type": "github" + } + ], + "time": "2023-11-07T21:57:25+00:00" }, { "name": "monolog/monolog", - "version": "3.4.0", + "version": "3.8.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "e2392369686d420ca32df3803de28b5d6f76867d" + "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/e2392369686d420ca32df3803de28b5d6f76867d", - "reference": "e2392369686d420ca32df3803de28b5d6f76867d", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/aef6ee73a77a66e404dd6540934a9ef1b3c855b4", + "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4", "shasum": "" }, "require": { @@ -4012,12 +4163,14 @@ "guzzlehttp/psr7": "^2.2", "mongodb/mongodb": "^1.8", "php-amqplib/php-amqplib": "~2.4 || ^3", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-strict-rules": "^1.4", - "phpunit/phpunit": "^10.1", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", "predis/predis": "^1.1 || ^2", - "ruflin/elastica": "^7", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", "symfony/mailer": "^5.4 || ^6", "symfony/mime": "^5.4 || ^6" }, @@ -4068,7 +4221,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.4.0" + "source": "https://github.com/Seldaek/monolog/tree/3.8.1" }, "funding": [ { @@ -4080,29 +4233,29 @@ "type": "tidelift" } ], - "time": "2023-06-21T08:46:11+00:00" + "time": "2024-12-05T17:15:07+00:00" }, { "name": "mtdowling/jmespath.php", - "version": "2.6.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb" + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb", - "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", "shasum": "" }, "require": { - "php": "^5.4 || ^7.0 || ^8.0", + "php": "^7.2.5 || ^8.0", "symfony/polyfill-mbstring": "^1.17" }, "require-dev": { - "composer/xdebug-handler": "^1.4 || ^2.0", - "phpunit/phpunit": "^4.8.36 || ^7.5.15" + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" }, "bin": [ "bin/jp.php" @@ -4110,7 +4263,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -4126,6 +4279,11 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", @@ -4139,53 +4297,53 @@ ], "support": { "issues": "https://github.com/jmespath/jmespath.php/issues", - "source": "https://github.com/jmespath/jmespath.php/tree/2.6.1" + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" }, - "time": "2021-06-14T00:11:39+00:00" + "time": "2024-09-04T18:46:31+00:00" }, { "name": "nesbot/carbon", - "version": "2.68.1", + "version": "3.8.2", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da" + "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4f991ed2a403c85efbc4f23eb4030063fdbe01da", - "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e1268cdbc486d97ce23fef2c666dc3c6b6de9947", + "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947", "shasum": "" }, "require": { + "carbonphp/carbon-doctrine-types": "<100.0", "ext-json": "*", - "php": "^7.1.8 || ^8.0", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3 || ^7.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" + "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" }, "require-dev": { - "doctrine/dbal": "^2.0 || ^3.1.4", - "doctrine/orm": "^2.7", - "friendsofphp/php-cs-fixer": "^3.0", - "kylekatarnls/multi-tester": "^2.0", - "ondrejmirtes/better-reflection": "*", - "phpmd/phpmd": "^2.9", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.99 || ^1.7.14", - "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", - "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", - "squizlabs/php_codesniffer": "^3.4" + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.57.2", + "kylekatarnls/multi-tester": "^2.5.3", + "ondrejmirtes/better-reflection": "^6.25.0.4", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.11.2", + "phpunit/phpunit": "^10.5.20", + "squizlabs/php_codesniffer": "^3.9.0" }, "bin": [ "bin/carbon" ], "type": "library", "extra": { - "branch-alias": { - "dev-3.x": "3.x-dev", - "dev-master": "2.x-dev" - }, "laravel": { "providers": [ "Carbon\\Laravel\\ServiceProvider" @@ -4195,6 +4353,10 @@ "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" } }, "autoload": { @@ -4243,35 +4405,35 @@ "type": "tidelift" } ], - "time": "2023-06-20T18:29:04+00:00" + "time": "2024-11-07T17:46:48+00:00" }, { "name": "nette/schema", - "version": "v1.2.3", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f" + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", - "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", "shasum": "" }, "require": { - "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", - "php": ">=7.1 <8.3" + "nette/utils": "^4.0", + "php": "8.1 - 8.4" }, "require-dev": { - "nette/tester": "^2.3 || ^2.4", + "nette/tester": "^2.5.2", "phpstan/phpstan-nette": "^1.0", - "tracy/tracy": "^2.7" + "tracy/tracy": "^2.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -4303,26 +4465,26 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.2.3" + "source": "https://github.com/nette/schema/tree/v1.3.2" }, - "time": "2022-10-13T01:24:26+00:00" + "time": "2024-10-06T23:10:23+00:00" }, { "name": "nette/utils", - "version": "v4.0.0", + "version": "v4.0.5", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "cacdbf5a91a657ede665c541eda28941d4b09c1e" + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/cacdbf5a91a657ede665c541eda28941d4b09c1e", - "reference": "cacdbf5a91a657ede665c541eda28941d4b09c1e", + "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", "shasum": "" }, "require": { - "php": ">=8.0 <8.3" + "php": "8.0 - 8.4" }, "conflict": { "nette/finder": "<3", @@ -4330,7 +4492,7 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "dev-master", - "nette/tester": "^2.4", + "nette/tester": "^2.5", "phpstan/phpstan": "^1.0", "tracy/tracy": "^2.9" }, @@ -4340,8 +4502,7 @@ "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-json": "to use Nette\\Utils\\Json", "ext-mbstring": "to use Strings::lower() etc...", - "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", - "ext-xml": "to use Strings::length() etc. when mbstring is not available" + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" }, "type": "library", "extra": { @@ -4390,31 +4551,33 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.0" + "source": "https://github.com/nette/utils/tree/v4.0.5" }, - "time": "2023-02-02T10:41:53+00:00" + "time": "2024-08-07T15:39:19+00:00" }, { "name": "nikic/php-parser", - "version": "v4.16.0", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -4422,7 +4585,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -4446,39 +4609,37 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2023-06-25T14:52:30+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "nunomaduro/termwind", - "version": "v1.15.1", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc" + "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/8ab0b32c8caa4a2e09700ea32925441385e4a5dc", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/52915afe6a1044e8b9cee1bcff836fb63acf9cda", + "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^8.0", - "symfony/console": "^5.3.0|^6.0.0" + "php": "^8.2", + "symfony/console": "^7.1.8" }, "require-dev": { - "ergebnis/phpstan-rules": "^1.0.", - "illuminate/console": "^8.0|^9.0", - "illuminate/support": "^8.0|^9.0", - "laravel/pint": "^1.0.0", - "pestphp/pest": "^1.21.0", - "pestphp/pest-plugin-mock": "^1.0", - "phpstan/phpstan": "^1.4.6", - "phpstan/phpstan-strict-rules": "^1.1.0", - "symfony/var-dumper": "^5.2.7|^6.0.0", + "illuminate/console": "^11.33.2", + "laravel/pint": "^1.18.2", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0", + "phpstan/phpstan": "^1.12.11", + "phpstan/phpstan-strict-rules": "^1.6.1", + "symfony/var-dumper": "^7.1.8", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -4487,6 +4648,9 @@ "providers": [ "Termwind\\Laravel\\TermwindServiceProvider" ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -4518,7 +4682,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v1.15.1" + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.0" }, "funding": [ { @@ -4534,20 +4698,20 @@ "type": "github" } ], - "time": "2023-02-08T01:06:31+00:00" + "time": "2024-11-21T10:39:51+00:00" }, { "name": "nyholm/psr7", - "version": "1.8.0", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "3cb4d163b58589e47b35103e8e5e6a6a475b47be" + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/3cb4d163b58589e47b35103e8e5e6a6a475b47be", - "reference": "3cb4d163b58589e47b35103e8e5e6a6a475b47be", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", "shasum": "" }, "require": { @@ -4600,7 +4764,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.8.0" + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" }, "funding": [ { @@ -4612,20 +4776,20 @@ "type": "github" } ], - "time": "2023-05-02T11:26:24+00:00" + "time": "2024-09-09T07:06:30+00:00" }, { "name": "paragonie/constant_time_encoding", - "version": "v2.6.3", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "58c3f47f650c94ec05a151692652a868995d2938" + "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", - "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105", + "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105", "shasum": "" }, "require": { @@ -4679,7 +4843,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2022-06-14T06:56:20+00:00" + "time": "2024-05-08T12:18:48+00:00" }, { "name": "paragonie/random_compat", @@ -4733,30 +4897,35 @@ }, { "name": "paragonie/sodium_compat", - "version": "v1.20.0", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6" + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/e592a3e06d1fa0d43988c7c7d9948ca836f644b6", - "reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a673d5f310477027cead2e2f2b6db5d8368157cb", + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb", "shasum": "" }, "require": { - "paragonie/random_compat": ">=1", - "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" + "php": "^8.1", + "php-64bit": "*" }, "require-dev": { - "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" + "phpunit/phpunit": "^7|^8|^9", + "vimeo/psalm": "^4|^5" }, "suggest": { - "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", - "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "files": [ "autoload.php" @@ -4813,48 +4982,48 @@ ], "support": { "issues": "https://github.com/paragonie/sodium_compat/issues", - "source": "https://github.com/paragonie/sodium_compat/tree/v1.20.0" + "source": "https://github.com/paragonie/sodium_compat/tree/v2.1.0" }, - "time": "2023-04-30T00:54:53+00:00" + "time": "2024-09-04T12:51:01+00:00" }, { "name": "pbmedia/laravel-ffmpeg", - "version": "8.3.0", + "version": "8.6.0", "source": { "type": "git", "url": "https://github.com/protonemedia/laravel-ffmpeg.git", - "reference": "820e7f1290918233a59d85f25bc78796dc3f57bb" + "reference": "f14efc53e8a52b53a237a9910b32e795dafcf8bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/820e7f1290918233a59d85f25bc78796dc3f57bb", - "reference": "820e7f1290918233a59d85f25bc78796dc3f57bb", + "url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/f14efc53e8a52b53a237a9910b32e795dafcf8bc", + "reference": "f14efc53e8a52b53a237a9910b32e795dafcf8bc", "shasum": "" }, "require": { - "illuminate/contracts": "^9.0|^10.0", - "php": "^8.1|^8.2", - "php-ffmpeg/php-ffmpeg": "^1.1", - "ramsey/collection": "^1.0|^2.0" + "illuminate/contracts": "^10.0|^11.0", + "php": "^8.1|^8.2|^8.3|^8.4", + "php-ffmpeg/php-ffmpeg": "^1.2", + "ramsey/collection": "^2.0" }, "require-dev": { "league/flysystem-memory": "^3.10", "mockery/mockery": "^1.4.4", - "nesbot/carbon": "^2.66", - "orchestra/testbench": "^7.0|^8.0", - "phpunit/phpunit": "^9.5.10", - "spatie/image": "^2.2", - "spatie/phpunit-snapshot-assertions": "^4.2" + "nesbot/carbon": "^2.66|^3.0", + "orchestra/testbench": "^8.0|^9.0", + "phpunit/phpunit": "^10.4", + "spatie/image": "^2.2|^3.3", + "spatie/phpunit-snapshot-assertions": "^5.0" }, "type": "library", "extra": { "laravel": { - "providers": [ - "ProtoneMedia\\LaravelFFMpeg\\Support\\ServiceProvider" - ], "aliases": { "FFMpeg": "ProtoneMedia\\LaravelFFMpeg\\Support\\FFMpeg" - } + }, + "providers": [ + "ProtoneMedia\\LaravelFFMpeg\\Support\\ServiceProvider" + ] } }, "autoload": { @@ -4885,7 +5054,7 @@ ], "support": { "issues": "https://github.com/protonemedia/laravel-ffmpeg/issues", - "source": "https://github.com/protonemedia/laravel-ffmpeg/tree/8.3.0" + "source": "https://github.com/protonemedia/laravel-ffmpeg/tree/8.6.0" }, "funding": [ { @@ -4893,33 +5062,33 @@ "type": "github" } ], - "time": "2023-02-15T10:10:46+00:00" + "time": "2024-11-12T16:12:23+00:00" }, { "name": "php-ffmpeg/php-ffmpeg", - "version": "v1.1.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/PHP-FFMpeg/PHP-FFMpeg.git", - "reference": "eace6f174ff6d206ba648483ebe59760f7f6a0e1" + "reference": "5e7b15710a8607e8a3a2d9fbe2c150a99b924fa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-FFMpeg/PHP-FFMpeg/zipball/eace6f174ff6d206ba648483ebe59760f7f6a0e1", - "reference": "eace6f174ff6d206ba648483ebe59760f7f6a0e1", + "url": "https://api.github.com/repos/PHP-FFMpeg/PHP-FFMpeg/zipball/5e7b15710a8607e8a3a2d9fbe2c150a99b924fa5", + "reference": "5e7b15710a8607e8a3a2d9fbe2c150a99b924fa5", "shasum": "" }, "require": { "evenement/evenement": "^3.0", - "php": "^8.0 || ^8.1 || ^8.2", + "php": "^8.0 || ^8.1 || ^8.2 || ^8.3 || ^8.4", "psr/log": "^1.0 || ^2.0 || ^3.0", "spatie/temporary-directory": "^2.0", - "symfony/cache": "^5.4 || ^6.0", - "symfony/process": "^5.4 || ^6.0" + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { "mockery/mockery": "^1.5", - "phpunit/phpunit": "^9.5.10" + "phpunit/phpunit": "^9.5.10 || ^10.0" }, "suggest": { "php-ffmpeg/extras": "A compilation of common audio & video drivers for PHP-FFMpeg" @@ -4980,22 +5149,22 @@ ], "support": { "issues": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/issues", - "source": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/tree/v1.1.0" + "source": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/tree/v1.3.0" }, - "time": "2022-12-09T13:57:05+00:00" + "time": "2024-11-12T15:39:52+00:00" }, { "name": "phpoption/phpoption", - "version": "1.9.1", + "version": "1.9.3", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", - "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", "shasum": "" }, "require": { @@ -5003,13 +5172,13 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" }, "type": "library", "extra": { "bamarni-bin": { "bin-links": true, - "forward-command": true + "forward-command": false }, "branch-alias": { "dev-master": "1.9-dev" @@ -5045,7 +5214,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" }, "funding": [ { @@ -5057,20 +5226,20 @@ "type": "tidelift" } ], - "time": "2023-02-25T19:38:58+00:00" + "time": "2024-07-20T21:41:07+00:00" }, { "name": "phpseclib/phpseclib", - "version": "2.0.44", + "version": "2.0.48", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "149f608243f8133c61926aae26ce67d2b22b37e5" + "reference": "eaa7be704b8b93a6913b69eb7f645a59d7731b61" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/149f608243f8133c61926aae26ce67d2b22b37e5", - "reference": "149f608243f8133c61926aae26ce67d2b22b37e5", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/eaa7be704b8b93a6913b69eb7f645a59d7731b61", + "reference": "eaa7be704b8b93a6913b69eb7f645a59d7731b61", "shasum": "" }, "require": { @@ -5151,7 +5320,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/2.0.44" + "source": "https://github.com/phpseclib/phpseclib/tree/2.0.48" }, "funding": [ { @@ -5167,7 +5336,7 @@ "type": "tidelift" } ], - "time": "2023-06-13T08:41:47+00:00" + "time": "2024-12-14T21:03:54+00:00" }, { "name": "pixelfed/fractal", @@ -5295,75 +5464,26 @@ }, "time": "2019-03-12T05:13:49+00:00" }, - { - "name": "pixelfed/zttp", - "version": "v0.5.0", - "source": { - "type": "git", - "url": "https://github.com/pixelfed/zttp.git", - "reference": "e78af39d75171f360ab4c32eed1c7a71b67b5e3b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pixelfed/zttp/zipball/e78af39d75171f360ab4c32eed1c7a71b67b5e3b", - "reference": "e78af39d75171f360ab4c32eed1c7a71b67b5e3b", - "shasum": "" - }, - "require": { - "guzzlehttp/guzzle": "^6.0|^7.0", - "php": ">=7.0", - "tightenco/collect": "^5.4" - }, - "require-dev": { - "laravel/lumen-framework": "5.5.*", - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "autoload": { - "files": [ - "src/Zttp.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Adam Wathan", - "email": "adam.wathan@gmail.com" - } - ], - "description": "A developer-experience focused HTTP client, optimized for most common use cases.", - "keywords": [ - "Guzzle", - "http" - ], - "support": { - "source": "https://github.com/pixelfed/zttp/tree/v0.5.0" - }, - "time": "2022-08-06T04:58:13+00:00" - }, { "name": "pragmarx/google2fa", - "version": "v8.0.1", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3" + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3", - "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", "shasum": "" }, "require": { - "paragonie/constant_time_encoding": "^1.0|^2.0", + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", "php": "^7.1|^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.18", + "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^7.5.15|^8.5|^9.0" }, "type": "library", @@ -5392,22 +5512,22 @@ ], "support": { "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1" + "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" }, - "time": "2022-06-13T21:57:56+00:00" + "time": "2024-09-05T11:56:40+00:00" }, { "name": "predis/predis", - "version": "v2.2.0", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "33b70b971a32b0d28b4f748b0547593dce316e0d" + "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/33b70b971a32b0d28b4f748b0547593dce316e0d", - "reference": "33b70b971a32b0d28b4f748b0547593dce316e0d", + "url": "https://api.github.com/repos/predis/predis/zipball/bac46bfdb78cd6e9c7926c697012aae740cb9ec9", + "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9", "shasum": "" }, "require": { @@ -5416,7 +5536,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.3", "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^8.0 || ~9.4.4" + "phpunit/phpunit": "^8.0 || ^9.4" }, "suggest": { "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" @@ -5447,7 +5567,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v2.2.0" + "source": "https://github.com/predis/predis/tree/v2.3.0" }, "funding": [ { @@ -5455,7 +5575,7 @@ "type": "github" } ], - "time": "2023-06-14T10:37:31+00:00" + "time": "2024-11-21T20:00:02+00:00" }, { "name": "psr/cache", @@ -5659,16 +5779,16 @@ }, { "name": "psr/http-client", - "version": "1.0.2", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31" + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31", - "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { @@ -5705,26 +5825,26 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/1.0.2" + "source": "https://github.com/php-fig/http-client" }, - "time": "2023-04-10T20:12:12+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { "name": "psr/http-factory", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -5748,7 +5868,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -5760,22 +5880,22 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-04-10T20:10:41+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", - "version": "1.1", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", - "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { @@ -5784,7 +5904,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -5799,7 +5919,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -5813,22 +5933,22 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/1.1" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2023-04-04T09:50:52+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -5863,9 +5983,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "psr/simple-cache", @@ -5920,25 +6040,25 @@ }, { "name": "psy/psysh", - "version": "v0.11.18", + "version": "v0.12.7", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec" + "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4f00ee9e236fa6a48f4560d1300b9c961a70a7ec", - "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", + "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", "shasum": "" }, "require": { "ext-json": "*", "ext-tokenizer": "*", - "nikic/php-parser": "^4.0 || ^3.1", - "php": "^8.0 || ^7.0.8", - "symfony/console": "^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^6.0 || ^5.0 || ^4.0 || ^3.4" + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" @@ -5949,16 +6069,19 @@ "suggest": { "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", "ext-pdo-sqlite": "The doc command requires SQLite to work.", - "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", - "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history." + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." }, "bin": [ "bin/psysh" ], "type": "library", "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, "branch-alias": { - "dev-main": "0.11.x-dev" + "dev-main": "0.12.x-dev" } }, "autoload": { @@ -5990,29 +6113,29 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.11.18" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.7" }, - "time": "2023-05-23T02:31:11+00:00" + "time": "2024-12-10T01:58:33+00:00" }, { "name": "pusher/pusher-php-server", - "version": "7.2.3", + "version": "7.2.6", "source": { "type": "git", "url": "https://github.com/pusher/pusher-http-php.git", - "reference": "416e68dd5f640175ad5982131c42a7a666d1d8e9" + "reference": "d89e9997191d18fb0fe03a956fa3ccfe0af524ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/416e68dd5f640175ad5982131c42a7a666d1d8e9", - "reference": "416e68dd5f640175ad5982131c42a7a666d1d8e9", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/d89e9997191d18fb0fe03a956fa3ccfe0af524ea", + "reference": "d89e9997191d18fb0fe03a956fa3ccfe0af524ea", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", "guzzlehttp/guzzle": "^7.2", - "paragonie/sodium_compat": "^1.6", + "paragonie/sodium_compat": "^1.6|^2.0", "php": "^7.3|^8.0", "psr/log": "^1.0|^2.0|^3.0" }, @@ -6051,9 +6174,9 @@ ], "support": { "issues": "https://github.com/pusher/pusher-http-php/issues", - "source": "https://github.com/pusher/pusher-http-php/tree/7.2.3" + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.6" }, - "time": "2023-05-17T16:00:06+00:00" + "time": "2024-10-18T12:04:31+00:00" }, { "name": "ralouphie/getallheaders", @@ -6190,20 +6313,20 @@ }, { "name": "ramsey/uuid", - "version": "4.7.4", + "version": "4.7.6", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "60a4c63ab724854332900504274f6150ff26d286" + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/60a4c63ab724854332900504274f6150ff26d286", - "reference": "60a4c63ab724854332900504274f6150ff26d286", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", "ext-json": "*", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" @@ -6266,7 +6389,7 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.4" + "source": "https://github.com/ramsey/uuid/tree/4.7.6" }, "funding": [ { @@ -6278,34 +6401,38 @@ "type": "tidelift" } ], - "time": "2023-04-15T23:01:58+00:00" + "time": "2024-04-27T21:32:50+00:00" }, { - "name": "ratchet/rfc6455", - "version": "v0.3.1", + "name": "resend/resend-php", + "version": "v0.13.0", "source": { "type": "git", - "url": "https://github.com/ratchetphp/RFC6455.git", - "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb" + "url": "https://github.com/resend/resend-php.git", + "reference": "c74926e34472fe3e3e21f150f3e3ce56fcbf8298" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/7c964514e93456a52a99a20fcfa0de242a43ccdb", - "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb", + "url": "https://api.github.com/repos/resend/resend-php/zipball/c74926e34472fe3e3e21f150f3e3ce56fcbf8298", + "reference": "c74926e34472fe3e3e21f150f3e3ce56fcbf8298", "shasum": "" }, "require": { - "guzzlehttp/psr7": "^2 || ^1.7", - "php": ">=5.4.2" + "guzzlehttp/guzzle": "^7.5", + "php": "^8.1.0" }, "require-dev": { - "phpunit/phpunit": "^5.7", - "react/socket": "^1.3" + "friendsofphp/php-cs-fixer": "^3.13", + "mockery/mockery": "^1.6", + "pestphp/pest": "^2.0" }, "type": "library", "autoload": { + "files": [ + "src/Resend.php" + ], "psr-4": { - "Ratchet\\RFC6455\\": "src" + "Resend\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -6314,650 +6441,42 @@ ], "authors": [ { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "role": "Developer" - }, - { - "name": "Matt Bonneau", - "role": "Developer" + "name": "Resend and contributors", + "homepage": "https://github.com/resend/resend-php/contributors" } ], - "description": "RFC6455 WebSocket protocol handler", - "homepage": "http://socketo.me", + "description": "Resend PHP library.", + "homepage": "https://resend.com/", "keywords": [ - "WebSockets", - "rfc6455", - "websocket" - ], - "support": { - "chat": "https://gitter.im/reactphp/reactphp", - "issues": "https://github.com/ratchetphp/RFC6455/issues", - "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3.1" - }, - "time": "2021-12-09T23:20:49+00:00" - }, - { - "name": "react/cache", - "version": "v1.2.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/cache.git", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/promise": "^3.0 || ^2.0 || ^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, Promise-based cache interface for ReactPHP", - "keywords": [ - "cache", - "caching", - "promise", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.2.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2022-11-30T15:59:55+00:00" - }, - { - "name": "react/dns", - "version": "v1.11.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/dns.git", - "reference": "3be0fc8f1eb37d6875cd6f0c6c7d0be81435de9f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/3be0fc8f1eb37d6875cd6f0c6c7d0be81435de9f", - "reference": "3be0fc8f1eb37d6875cd6f0c6c7d0be81435de9f", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/cache": "^1.0 || ^0.6 || ^0.5", - "react/event-loop": "^1.2", - "react/promise": "^3.0 || ^2.7 || ^1.2.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.5 || ^4.8.35", - "react/async": "^4 || ^3 || ^2", - "react/promise-timer": "^1.9" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Dns\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async DNS resolver for ReactPHP", - "keywords": [ - "async", - "dns", - "dns-resolver", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.11.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-06-02T12:45:26+00:00" - }, - { - "name": "react/event-loop", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "6e7e587714fff7a83dcc7025aee42ab3b265ae05" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6e7e587714fff7a83dcc7025aee42ab3b265ae05", - "reference": "6e7e587714fff7a83dcc7025aee42ab3b265ae05", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "suggest": { - "ext-pcntl": "For signal handling support when using the StreamSelectLoop" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\EventLoop\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", - "keywords": [ - "asynchronous", - "event-loop" - ], - "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.4.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-05-05T10:11:24+00:00" - }, - { - "name": "react/http", - "version": "v1.9.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/http.git", - "reference": "bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/http/zipball/bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0", - "reference": "bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "fig/http-message-util": "^1.1", - "php": ">=5.3.0", - "psr/http-message": "^1.0", - "react/event-loop": "^1.2", - "react/promise": "^3 || ^2.3 || ^1.2.1", - "react/socket": "^1.12", - "react/stream": "^1.2", - "ringcentral/psr7": "^1.2" - }, - "require-dev": { - "clue/http-proxy-react": "^1.8", - "clue/reactphp-ssh-proxy": "^1.4", - "clue/socks-react": "^1.4", - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", - "react/async": "^4 || ^3 || ^2", - "react/promise-stream": "^1.4", - "react/promise-timer": "^1.9" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Http\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Event-driven, streaming HTTP client and server implementation for ReactPHP", - "keywords": [ - "async", + "api", "client", - "event-driven", - "http", - "http client", - "http server", - "https", - "psr-7", - "reactphp", - "server", - "streaming" + "php", + "resend", + "sdk" ], "support": { - "issues": "https://github.com/reactphp/http/issues", - "source": "https://github.com/reactphp/http/tree/v1.9.0" + "issues": "https://github.com/resend/resend-php/issues", + "source": "https://github.com/resend/resend-php/tree/v0.13.0" }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-04-26T10:29:24+00:00" - }, - { - "name": "react/promise", - "version": "v3.0.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "c86753c76fd3be465d93b308f18d189f01a22be4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/c86753c76fd3be465d93b308f18d189f01a22be4", - "reference": "c86753c76fd3be465d93b308f18d189f01a22be4", - "shasum": "" - }, - "require": { - "php": ">=7.1.0" - }, - "require-dev": { - "phpstan/phpstan": "1.10.20 || 1.4.10", - "phpunit/phpunit": "^9.5 || ^7.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], - "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.0.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-07-11T16:12:49+00:00" - }, - { - "name": "react/socket", - "version": "v1.13.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "cff482bbad5848ecbe8b57da57e4e213b03619aa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/cff482bbad5848ecbe8b57da57e4e213b03619aa", - "reference": "cff482bbad5848ecbe8b57da57e4e213b03619aa", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/dns": "^1.11", - "react/event-loop": "^1.2", - "react/promise": "^3 || ^2.6 || ^1.2.1", - "react/stream": "^1.2" - }, - "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", - "react/async": "^4 || ^3 || ^2", - "react/promise-stream": "^1.4", - "react/promise-timer": "^1.9" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Socket\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", - "keywords": [ - "Connection", - "Socket", - "async", - "reactphp", - "stream" - ], - "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.13.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-06-07T10:28:34+00:00" - }, - { - "name": "react/stream", - "version": "v1.3.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "6fbc9672905c7d5a885f2da2fc696f65840f4a66" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/6fbc9672905c7d5a885f2da2fc696f65840f4a66", - "reference": "6fbc9672905c7d5a885f2da2fc696f65840f4a66", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.8", - "react/event-loop": "^1.2" - }, - "require-dev": { - "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Stream\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", - "keywords": [ - "event-driven", - "io", - "non-blocking", - "pipe", - "reactphp", - "readable", - "stream", - "writable" - ], - "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.3.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-06-16T10:52:11+00:00" - }, - { - "name": "ringcentral/psr7", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/ringcentral/psr7.git", - "reference": "360faaec4b563958b673fb52bbe94e37f14bc686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ringcentral/psr7/zipball/360faaec4b563958b673fb52bbe94e37f14bc686", - "reference": "360faaec4b563958b673fb52bbe94e37f14bc686", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "psr/http-message": "~1.0" - }, - "provide": { - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "RingCentral\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "PSR-7 message implementation", - "keywords": [ - "http", - "message", - "stream", - "uri" - ], - "support": { - "source": "https://github.com/ringcentral/psr7/tree/master" - }, - "time": "2018-05-29T20:21:04+00:00" + "time": "2024-08-15T03:27:29+00:00" }, { "name": "spatie/db-dumper", - "version": "3.4.0", + "version": "3.7.1", "source": { "type": "git", "url": "https://github.com/spatie/db-dumper.git", - "reference": "bbd5ae0f331d47e6534eb307e256c11a65c8e24a" + "reference": "55d4d6710e1ab18c1e7ce2b22b8ad4bea2a30016" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/db-dumper/zipball/bbd5ae0f331d47e6534eb307e256c11a65c8e24a", - "reference": "bbd5ae0f331d47e6534eb307e256c11a65c8e24a", + "url": "https://api.github.com/repos/spatie/db-dumper/zipball/55d4d6710e1ab18c1e7ce2b22b8ad4bea2a30016", + "reference": "55d4d6710e1ab18c1e7ce2b22b8ad4bea2a30016", "shasum": "" }, "require": { "php": "^8.0", - "symfony/process": "^5.0|^6.0" + "symfony/process": "^5.0|^6.0|^7.0" }, "require-dev": { "pestphp/pest": "^1.22" @@ -6990,7 +6509,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/db-dumper/tree/3.4.0" + "source": "https://github.com/spatie/db-dumper/tree/3.7.1" }, "funding": [ { @@ -7002,32 +6521,32 @@ "type": "github" } ], - "time": "2023-06-27T08:34:52+00:00" + "time": "2024-11-18T14:54:31+00:00" }, { "name": "spatie/image-optimizer", - "version": "1.6.4", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/spatie/image-optimizer.git", - "reference": "d997e01ba980b2769ddca2f00badd3b80c2a2512" + "reference": "4fd22035e81d98fffced65a8c20d9ec4daa9671c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/d997e01ba980b2769ddca2f00badd3b80c2a2512", - "reference": "d997e01ba980b2769ddca2f00badd3b80c2a2512", + "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/4fd22035e81d98fffced65a8c20d9ec4daa9671c", + "reference": "4fd22035e81d98fffced65a8c20d9ec4daa9671c", "shasum": "" }, "require": { "ext-fileinfo": "*", "php": "^7.3|^8.0", "psr/log": "^1.0 | ^2.0 | ^3.0", - "symfony/process": "^4.2|^5.0|^6.0" + "symfony/process": "^4.2|^5.0|^6.0|^7.0" }, "require-dev": { "pestphp/pest": "^1.21", "phpunit/phpunit": "^8.5.21|^9.4.4", - "symfony/var-dumper": "^4.2|^5.0|^6.0" + "symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0" }, "type": "library", "autoload": { @@ -7055,50 +6574,50 @@ ], "support": { "issues": "https://github.com/spatie/image-optimizer/issues", - "source": "https://github.com/spatie/image-optimizer/tree/1.6.4" + "source": "https://github.com/spatie/image-optimizer/tree/1.8.0" }, - "time": "2023-03-10T08:43:19+00:00" + "time": "2024-11-04T08:24:54+00:00" }, { "name": "spatie/laravel-backup", - "version": "8.1.11", + "version": "8.8.2", "source": { "type": "git", "url": "https://github.com/spatie/laravel-backup.git", - "reference": "e4f5c3f6783d40a219a02bc99dc4171ecdd6d20c" + "reference": "5b672713283703a74c629ccd67b1d77eb57e24b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/e4f5c3f6783d40a219a02bc99dc4171ecdd6d20c", - "reference": "e4f5c3f6783d40a219a02bc99dc4171ecdd6d20c", + "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/5b672713283703a74c629ccd67b1d77eb57e24b9", + "reference": "5b672713283703a74c629ccd67b1d77eb57e24b9", "shasum": "" }, "require": { "ext-zip": "^1.14.0", - "illuminate/console": "^9.0|^10.0", - "illuminate/contracts": "^9.0|^10.0", - "illuminate/events": "^9.0|^10.0", - "illuminate/filesystem": "^9.0|^10.0", - "illuminate/notifications": "^9.0|^10.0", - "illuminate/support": "^9.0|^10.0", + "illuminate/console": "^10.10.0|^11.0", + "illuminate/contracts": "^10.10.0|^11.0", + "illuminate/events": "^10.10.0|^11.0", + "illuminate/filesystem": "^10.10.0|^11.0", + "illuminate/notifications": "^10.10.0|^11.0", + "illuminate/support": "^10.10.0|^11.0", "league/flysystem": "^3.0", - "php": "^8.0", + "php": "^8.1", "spatie/db-dumper": "^3.0", "spatie/laravel-package-tools": "^1.6.2", - "spatie/laravel-signal-aware-command": "^1.2", + "spatie/laravel-signal-aware-command": "^1.2|^2.0", "spatie/temporary-directory": "^2.0", - "symfony/console": "^6.0", - "symfony/finder": "^6.0" + "symfony/console": "^6.0|^7.0", + "symfony/finder": "^6.0|^7.0" }, "require-dev": { "composer-runtime-api": "^2.0", "ext-pcntl": "*", - "laravel/slack-notification-channel": "^2.5", + "larastan/larastan": "^2.7.0", + "laravel/slack-notification-channel": "^2.5|^3.0", "league/flysystem-aws-s3-v3": "^2.0|^3.0", "mockery/mockery": "^1.4", - "nunomaduro/larastan": "^2.1", - "orchestra/testbench": "^7.0|^8.0", - "pestphp/pest": "^1.20", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^1.20|^2.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.1" @@ -7144,7 +6663,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-backup/issues", - "source": "https://github.com/spatie/laravel-backup/tree/8.1.11" + "source": "https://github.com/spatie/laravel-backup/tree/8.8.2" }, "funding": [ { @@ -7156,30 +6675,30 @@ "type": "other" } ], - "time": "2023-06-02T08:56:10+00:00" + "time": "2024-08-07T11:07:52+00:00" }, { "name": "spatie/laravel-image-optimizer", - "version": "1.7.1", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-image-optimizer.git", - "reference": "cd8945e47b9fd01bc7b770eecd57c56f46c47422" + "reference": "024752cba691fee3cd1800000b6aa3da3b8b2474" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-image-optimizer/zipball/cd8945e47b9fd01bc7b770eecd57c56f46c47422", - "reference": "cd8945e47b9fd01bc7b770eecd57c56f46c47422", + "url": "https://api.github.com/repos/spatie/laravel-image-optimizer/zipball/024752cba691fee3cd1800000b6aa3da3b8b2474", + "reference": "024752cba691fee3cd1800000b6aa3da3b8b2474", "shasum": "" }, "require": { - "laravel/framework": "^8.0|^9.0|^10.0", + "laravel/framework": "^8.0|^9.0|^10.0|^11.0", "php": "^8.0", "spatie/image-optimizer": "^1.2.0" }, "require-dev": { - "orchestra/testbench": "^6.23|^7.0|^8.0", - "phpunit/phpunit": "^9.4" + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0", + "phpunit/phpunit": "^9.4|^10.5" }, "type": "library", "extra": { @@ -7216,7 +6735,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/laravel-image-optimizer/tree/1.7.1" + "source": "https://github.com/spatie/laravel-image-optimizer/tree/1.8.0" }, "funding": [ { @@ -7224,32 +6743,32 @@ "type": "custom" } ], - "time": "2023-01-24T23:44:33+00:00" + "time": "2024-02-29T10:55:08+00:00" }, { "name": "spatie/laravel-package-tools", - "version": "1.15.0", + "version": "1.17.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "efab1844b8826443135201c4443690f032c3d533" + "reference": "9ab30fd24f677e5aa370ea4cf6b41c517d16cf85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/efab1844b8826443135201c4443690f032c3d533", - "reference": "efab1844b8826443135201c4443690f032c3d533", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/9ab30fd24f677e5aa370ea4cf6b41c517d16cf85", + "reference": "9ab30fd24f677e5aa370ea4cf6b41c517d16cf85", "shasum": "" }, "require": { - "illuminate/contracts": "^9.28|^10.0", + "illuminate/contracts": "^9.28|^10.0|^11.0", "php": "^8.0" }, "require-dev": { "mockery/mockery": "^1.5", - "orchestra/testbench": "^7.7|^8.0", - "pestphp/pest": "^1.22", - "phpunit/phpunit": "^9.5.24", - "spatie/pest-plugin-test-time": "^1.1" + "orchestra/testbench": "^7.7|^8.0|^9.0", + "pestphp/pest": "^1.22|^2", + "phpunit/phpunit": "^9.5.24|^10.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" }, "type": "library", "autoload": { @@ -7276,7 +6795,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.15.0" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.17.0" }, "funding": [ { @@ -7284,45 +6803,46 @@ "type": "github" } ], - "time": "2023-04-27T08:09:01+00:00" + "time": "2024-12-09T16:29:14+00:00" }, { "name": "spatie/laravel-signal-aware-command", - "version": "1.3.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-signal-aware-command.git", - "reference": "46cda09a85aef3fd47fb73ddc7081f963e255571" + "reference": "49a5e671c3a3fd992187a777d01385fc6a84759d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-signal-aware-command/zipball/46cda09a85aef3fd47fb73ddc7081f963e255571", - "reference": "46cda09a85aef3fd47fb73ddc7081f963e255571", + "url": "https://api.github.com/repos/spatie/laravel-signal-aware-command/zipball/49a5e671c3a3fd992187a777d01385fc6a84759d", + "reference": "49a5e671c3a3fd992187a777d01385fc6a84759d", "shasum": "" }, "require": { - "illuminate/contracts": "^8.35|^9.0|^10.0", - "php": "^8.0", - "spatie/laravel-package-tools": "^1.4.3" + "illuminate/contracts": "^11.0", + "php": "^8.2", + "spatie/laravel-package-tools": "^1.4.3", + "symfony/console": "^7.0" }, "require-dev": { - "brianium/paratest": "^6.2", + "brianium/paratest": "^6.2|^7.0", "ext-pcntl": "*", - "nunomaduro/collision": "^5.3|^6.0", - "orchestra/testbench": "^6.16|^7.0|^8.0", - "pestphp/pest-plugin-laravel": "^1.3", - "phpunit/phpunit": "^9.5", + "nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0", + "orchestra/testbench": "^9.0", + "pestphp/pest-plugin-laravel": "^1.3|^2.0", + "phpunit/phpunit": "^9.5|^10|^11", "spatie/laravel-ray": "^1.17" }, "type": "library", "extra": { "laravel": { - "providers": [ - "Spatie\\SignalAwareCommand\\SignalAwareCommandServiceProvider" - ], "aliases": { "Signal": "Spatie\\SignalAwareCommand\\Facades\\Signal" - } + }, + "providers": [ + "Spatie\\SignalAwareCommand\\SignalAwareCommandServiceProvider" + ] } }, "autoload": { @@ -7350,7 +6870,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-signal-aware-command/issues", - "source": "https://github.com/spatie/laravel-signal-aware-command/tree/1.3.0" + "source": "https://github.com/spatie/laravel-signal-aware-command/tree/2.0.0" }, "funding": [ { @@ -7358,20 +6878,20 @@ "type": "github" } ], - "time": "2023-01-14T21:10:59+00:00" + "time": "2024-02-05T13:37:25+00:00" }, { "name": "spatie/temporary-directory", - "version": "2.1.2", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/spatie/temporary-directory.git", - "reference": "0c804873f6b4042aa8836839dca683c7d0f71831" + "reference": "76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/0c804873f6b4042aa8836839dca683c7d0f71831", - "reference": "0c804873f6b4042aa8836839dca683c7d0f71831", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a", + "reference": "76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a", "shasum": "" }, "require": { @@ -7407,7 +6927,7 @@ ], "support": { "issues": "https://github.com/spatie/temporary-directory/issues", - "source": "https://github.com/spatie/temporary-directory/tree/2.1.2" + "source": "https://github.com/spatie/temporary-directory/tree/2.2.1" }, "funding": [ { @@ -7419,31 +6939,96 @@ "type": "github" } ], - "time": "2023-04-28T07:47:42+00:00" + "time": "2023-12-25T11:46:58+00:00" }, { - "name": "stevebauman/purify", - "version": "v6.0.1", + "name": "spomky-labs/base64url", + "version": "v2.0.4", "source": { "type": "git", - "url": "https://github.com/stevebauman/purify.git", - "reference": "7b63762b05db9eadc36d7e8b74cf58fa64bfa527" + "url": "https://github.com/Spomky-Labs/base64url.git", + "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stevebauman/purify/zipball/7b63762b05db9eadc36d7e8b74cf58fa64bfa527", - "reference": "7b63762b05db9eadc36d7e8b74cf58fa64bfa527", + "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d", + "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d", "shasum": "" }, "require": { - "ezyang/htmlpurifier": "^4.9.0", - "illuminate/contracts": "~7.0|~8.0|~9.0|~10.0", - "illuminate/support": "~7.0|~8.0|~9.0|~10.0", + "php": ">=7.1" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.11|^0.12", + "phpstan/phpstan-beberlei-assert": "^0.11|^0.12", + "phpstan/phpstan-deprecation-rules": "^0.11|^0.12", + "phpstan/phpstan-phpunit": "^0.11|^0.12", + "phpstan/phpstan-strict-rules": "^0.11|^0.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Base64Url\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky-Labs/base64url/contributors" + } + ], + "description": "Base 64 URL Safe Encoding/Decoding PHP Library", + "homepage": "https://github.com/Spomky-Labs/base64url", + "keywords": [ + "base64", + "rfc4648", + "safe", + "url" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/base64url/issues", + "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2020-11-03T09:10:25+00:00" + }, + { + "name": "stevebauman/purify", + "version": "v6.2.2", + "source": { + "type": "git", + "url": "https://github.com/stevebauman/purify.git", + "reference": "a449299a3d5f5f8ef177e626721b3f69143890a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stevebauman/purify/zipball/a449299a3d5f5f8ef177e626721b3f69143890a4", + "reference": "a449299a3d5f5f8ef177e626721b3f69143890a4", + "shasum": "" + }, + "require": { + "ezyang/htmlpurifier": "^4.17", + "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0", "php": ">=7.4" }, "require-dev": { - "orchestra/testbench": "~5.0|~6.0|~7.0", - "phpunit/phpunit": "~8.0|~9.0" + "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0", + "phpunit/phpunit": "^8.0|^9.0|^10.0" }, "type": "library", "extra": { @@ -7483,37 +7068,38 @@ ], "support": { "issues": "https://github.com/stevebauman/purify/issues", - "source": "https://github.com/stevebauman/purify/tree/v6.0.1" + "source": "https://github.com/stevebauman/purify/tree/v6.2.2" }, - "time": "2023-04-06T21:16:20+00:00" + "time": "2024-09-24T12:27:10+00:00" }, { "name": "symfony/cache", - "version": "v6.3.1", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "52cff7608ef6e38376ac11bd1fbb0a220107f066" + "reference": "e7e983596b744c4539f31e79b0350a6cf5878a20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/52cff7608ef6e38376ac11bd1fbb0a220107f066", - "reference": "52cff7608ef6e38376ac11bd1fbb0a220107f066", + "url": "https://api.github.com/repos/symfony/cache/zipball/e7e983596b744c4539f31e79b0350a6cf5878a20", + "reference": "e7e983596b744c4539f31e79b0350a6cf5878a20", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^2.5|^3", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^6.2.10" + "symfony/var-exporter": "^6.4|^7.0" }, "conflict": { - "doctrine/dbal": "<2.13.1", - "symfony/dependency-injection": "<5.4", - "symfony/http-kernel": "<5.4", - "symfony/var-dumper": "<5.4" + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" }, "provide": { "psr/cache-implementation": "2.0|3.0", @@ -7522,15 +7108,16 @@ }, "require-dev": { "cache/integration-tests": "dev-master", - "doctrine/dbal": "^2.13.1|^3.0", + "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/filesystem": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/messenger": "^5.4|^6.0", - "symfony/var-dumper": "^5.4|^6.0" + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -7565,7 +7152,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.3.1" + "source": "https://github.com/symfony/cache/tree/v7.2.1" }, "funding": [ { @@ -7581,20 +7168,20 @@ "type": "tidelift" } ], - "time": "2023-06-24T11:51:27+00:00" + "time": "2024-12-07T08:08:50+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.3.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "ad945640ccc0ae6e208bcea7d7de4b39b569896b" + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/ad945640ccc0ae6e208bcea7d7de4b39b569896b", - "reference": "ad945640ccc0ae6e208bcea7d7de4b39b569896b", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", "shasum": "" }, "require": { @@ -7604,7 +7191,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -7641,7 +7228,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.1" }, "funding": [ { @@ -7657,47 +7244,124 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { - "name": "symfony/console", - "version": "v6.3.0", + "name": "symfony/clock", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7" + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7", - "reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/console", + "version": "v7.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "shasum": "" + }, + "require": { + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0" + "symfony/string": "^6.4|^7.0" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/lock": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/var-dumper": "^5.4|^6.0" + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -7731,7 +7395,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.3.0" + "source": "https://github.com/symfony/console/tree/v7.2.1" }, "funding": [ { @@ -7747,24 +7411,24 @@ "type": "tidelift" } ], - "time": "2023-05-29T12:49:39+00:00" + "time": "2024-12-11T03:49:26+00:00" }, { "name": "symfony/css-selector", - "version": "v6.3.0", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf" + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf", - "reference": "88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -7796,7 +7460,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.3.0" + "source": "https://github.com/symfony/css-selector/tree/v7.2.0" }, "funding": [ { @@ -7812,20 +7476,20 @@ "type": "tidelift" } ], - "time": "2023-03-20T16:43:42+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.3.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { @@ -7834,7 +7498,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -7863,7 +7527,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -7879,34 +7543,35 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/error-handler", - "version": "v6.3.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "99d2d814a6351461af350ead4d963bd67451236f" + "reference": "6150b89186573046167796fa5f3f76601d5145f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/99d2d814a6351461af350ead4d963bd67451236f", - "reference": "99d2d814a6351461af350ead4d963bd67451236f", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/6150b89186573046167796fa5f3f76601d5145f8", + "reference": "6150b89186573046167796fa5f3f76601d5145f8", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^5.4|^6.0" + "symfony/var-dumper": "^6.4|^7.0" }, "conflict": { - "symfony/deprecation-contracts": "<2.5" + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" }, "require-dev": { "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/serializer": "^5.4|^6.0" + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" }, "bin": [ "Resources/bin/patch-type-declarations" @@ -7937,7 +7602,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.3.0" + "source": "https://github.com/symfony/error-handler/tree/v7.2.1" }, "funding": [ { @@ -7953,28 +7618,28 @@ "type": "tidelift" } ], - "time": "2023-05-10T12:03:13+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.3.0", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa" + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa", - "reference": "3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<5.4", + "symfony/dependency-injection": "<6.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -7983,13 +7648,13 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/error-handler": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/http-foundation": "^5.4|^6.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^5.4|^6.0" + "symfony/stopwatch": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -8017,7 +7682,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.3.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" }, "funding": [ { @@ -8033,20 +7698,20 @@ "type": "tidelift" } ], - "time": "2023-04-21T14:41:17+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.3.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", - "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", "shasum": "" }, "require": { @@ -8056,7 +7721,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -8093,7 +7758,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" }, "funding": [ { @@ -8109,27 +7774,27 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/finder", - "version": "v6.3.0", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "d9b01ba073c44cef617c7907ce2419f8d00d75e2" + "reference": "6de263e5868b9a137602dd1e33e4d48bfae99c49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/d9b01ba073c44cef617c7907ce2419f8d00d75e2", - "reference": "d9b01ba073c44cef617c7907ce2419f8d00d75e2", + "url": "https://api.github.com/repos/symfony/finder/zipball/6de263e5868b9a137602dd1e33e4d48bfae99c49", + "reference": "6de263e5868b9a137602dd1e33e4d48bfae99c49", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.0" + "symfony/filesystem": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -8157,7 +7822,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.3.0" + "source": "https://github.com/symfony/finder/tree/v7.2.0" }, "funding": [ { @@ -8173,27 +7838,27 @@ "type": "tidelift" } ], - "time": "2023-04-02T01:25:41+00:00" + "time": "2024-10-23T06:56:12+00:00" }, { "name": "symfony/http-client", - "version": "v6.3.1", + "version": "v6.4.16", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "1c828a06aef2f5eeba42026dfc532d4fc5406123" + "reference": "60a113666fa67e598abace38e5f46a0954d8833d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/1c828a06aef2f5eeba42026dfc532d4fc5406123", - "reference": "1c828a06aef2f5eeba42026dfc532d4fc5406123", + "url": "https://api.github.com/repos/symfony/http-client/zipball/60a113666fa67e598abace38e5f46a0954d8833d", + "reference": "60a113666fa67e598abace38e5f46a0954d8833d", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3", + "symfony/http-client-contracts": "~3.4.3|^3.5.1", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -8211,14 +7876,15 @@ "amphp/http-client": "^4.2.1", "amphp/http-tunnel": "^1.0", "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4", + "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/stopwatch": "^5.4|^6.0" + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -8249,7 +7915,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.3.1" + "source": "https://github.com/symfony/http-client/tree/v6.4.16" }, "funding": [ { @@ -8265,20 +7931,20 @@ "type": "tidelift" } ], - "time": "2023-06-24T11:51:27+00:00" + "time": "2024-11-27T11:52:33+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.3.0", + "version": "v3.5.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "3b66325d0176b4ec826bffab57c9037d759c31fb" + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/3b66325d0176b4ec826bffab57c9037d759c31fb", - "reference": "3b66325d0176b4ec826bffab57c9037d759c31fb", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", "shasum": "" }, "require": { @@ -8286,12 +7952,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { @@ -8327,7 +7993,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" }, "funding": [ { @@ -8343,40 +8009,41 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-12-07T08:49:48+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.3.1", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e0ad0d153e1c20069250986cd9e9dd1ccebb0d66" + "reference": "e88a66c3997859532bc2ddd6dd8f35aba2711744" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e0ad0d153e1c20069250986cd9e9dd1ccebb0d66", - "reference": "e0ad0d153e1c20069250986cd9e9dd1ccebb0d66", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e88a66c3997859532bc2ddd6dd8f35aba2711744", + "reference": "e88a66c3997859532bc2ddd6dd8f35aba2711744", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-mbstring": "~1.1", "symfony/polyfill-php83": "^1.27" }, "conflict": { - "symfony/cache": "<6.2" + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" }, "require-dev": { - "doctrine/dbal": "^2.13.1|^3.0", + "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", - "symfony/mime": "^5.4|^6.0", - "symfony/rate-limiter": "^5.2|^6.0" + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -8404,7 +8071,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.3.1" + "source": "https://github.com/symfony/http-foundation/tree/v7.2.0" }, "funding": [ { @@ -8420,76 +8087,77 @@ "type": "tidelift" } ], - "time": "2023-06-24T11:51:27+00:00" + "time": "2024-11-13T18:58:46+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.3.1", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "161e16fd2e35fb4881a43bc8b383dfd5be4ac374" + "reference": "d8ae58eecae44c8e66833e76cc50a4ad3c002d97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/161e16fd2e35fb4881a43bc8b383dfd5be4ac374", - "reference": "161e16fd2e35fb4881a43bc8b383dfd5be4ac374", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/d8ae58eecae44c8e66833e76cc50a4ad3c002d97", + "reference": "d8ae58eecae44c8e66833e76cc50a4ad3c002d97", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.3", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/http-foundation": "^6.2.7", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/browser-kit": "<5.4", - "symfony/cache": "<5.4", - "symfony/config": "<6.1", - "symfony/console": "<5.4", - "symfony/dependency-injection": "<6.3", - "symfony/doctrine-bridge": "<5.4", - "symfony/form": "<5.4", - "symfony/http-client": "<5.4", + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/mailer": "<5.4", - "symfony/messenger": "<5.4", - "symfony/translation": "<5.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", "symfony/translation-contracts": "<2.5", - "symfony/twig-bridge": "<5.4", - "symfony/validator": "<5.4", - "symfony/var-dumper": "<6.3", - "twig/twig": "<2.13" + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/clock": "^6.2", - "symfony/config": "^6.1", - "symfony/console": "^5.4|^6.0", - "symfony/css-selector": "^5.4|^6.0", - "symfony/dependency-injection": "^6.3", - "symfony/dom-crawler": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/finder": "^5.4|^6.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^5.4|^6.0", - "symfony/property-access": "^5.4.5|^6.0.5", - "symfony/routing": "^5.4|^6.0", - "symfony/serializer": "^6.3", - "symfony/stopwatch": "^5.4|^6.0", - "symfony/translation": "^5.4|^6.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^5.4|^6.0", - "symfony/validator": "^6.3", - "symfony/var-exporter": "^6.2", - "twig/twig": "^2.13|^3.0.4" + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" }, "type": "library", "autoload": { @@ -8517,7 +8185,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.3.1" + "source": "https://github.com/symfony/http-kernel/tree/v7.2.1" }, "funding": [ { @@ -8533,43 +8201,43 @@ "type": "tidelift" } ], - "time": "2023-06-26T06:07:32+00:00" + "time": "2024-12-11T12:09:10+00:00" }, { "name": "symfony/mailer", - "version": "v6.3.0", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "7b03d9be1dea29bfec0a6c7b603f5072a4c97435" + "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/7b03d9be1dea29bfec0a6c7b603f5072a4c97435", - "reference": "7b03d9be1dea29bfec0a6c7b603f5072a4c97435", + "url": "https://api.github.com/repos/symfony/mailer/zipball/e4d358702fb66e4c8a2af08e90e7271a62de39cc", + "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc", "shasum": "" }, "require": { "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=8.1", + "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/mime": "^6.2", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^7.2", "symfony/service-contracts": "^2.5|^3" }, "conflict": { "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<5.4", - "symfony/messenger": "<6.2", - "symfony/mime": "<6.2", - "symfony/twig-bridge": "<6.2.1" + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0", - "symfony/http-client": "^5.4|^6.0", - "symfony/messenger": "^6.2", - "symfony/twig-bridge": "^6.2" + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -8597,7 +8265,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.3.0" + "source": "https://github.com/symfony/mailer/tree/v7.2.0" }, "funding": [ { @@ -8613,32 +8281,32 @@ "type": "tidelift" } ], - "time": "2023-05-29T12:49:39+00:00" + "time": "2024-11-25T15:21:05+00:00" }, { "name": "symfony/mailgun-mailer", - "version": "v6.3.0", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/mailgun-mailer.git", - "reference": "2fafefe8683a93155aceb6cca622c7cee2e27174" + "reference": "ad4e79798a5eb80af99004a4871b4fe5effe33a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/2fafefe8683a93155aceb6cca622c7cee2e27174", - "reference": "2fafefe8683a93155aceb6cca622c7cee2e27174", + "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/ad4e79798a5eb80af99004a4871b4fe5effe33a3", + "reference": "ad4e79798a5eb80af99004a4871b4fe5effe33a3", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/mailer": "^5.4.21|^6.2.7" + "symfony/mailer": "^5.4.21|^6.2.7|^7.0" }, "conflict": { "symfony/http-foundation": "<6.2" }, "require-dev": { - "symfony/http-client": "^5.4|^6.0", - "symfony/webhook": "^6.3" + "symfony/http-client": "^6.3|^7.0", + "symfony/webhook": "^6.3|^7.0" }, "type": "symfony-mailer-bridge", "autoload": { @@ -8666,7 +8334,7 @@ "description": "Symfony Mailgun Mailer Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailgun-mailer/tree/v6.3.0" + "source": "https://github.com/symfony/mailgun-mailer/tree/v6.4.13" }, "funding": [ { @@ -8682,24 +8350,24 @@ "type": "tidelift" } ], - "time": "2023-05-02T16:15:19+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/mime", - "version": "v6.3.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "7b5d2121858cd6efbed778abce9cfdd7ab1f62ad" + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/7b5d2121858cd6efbed778abce9cfdd7ab1f62ad", - "reference": "7b5d2121858cd6efbed778abce9cfdd7ab1f62ad", + "url": "https://api.github.com/repos/symfony/mime/zipball/7f9617fcf15cb61be30f8b252695ed5e2bfac283", + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -8707,17 +8375,18 @@ "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<5.4", - "symfony/serializer": "<6.2" + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/property-access": "^5.4|^6.0", - "symfony/property-info": "^5.4|^6.0", - "symfony/serializer": "^6.2" + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" }, "type": "library", "autoload": { @@ -8749,7 +8418,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.3.0" + "source": "https://github.com/symfony/mime/tree/v7.2.1" }, "funding": [ { @@ -8765,24 +8434,24 @@ "type": "tidelift" } ], - "time": "2023-04-28T15:57:00+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -8792,12 +8461,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -8831,7 +8497,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -8847,36 +8513,33 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -8912,7 +8575,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -8928,38 +8591,34 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "639084e360537a19f9ee352433b84ce831f3d2da" + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da", - "reference": "639084e360537a19f9ee352433b84ce831f3d2da", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -8999,7 +8658,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" }, "funding": [ { @@ -9015,36 +8674,33 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9083,7 +8739,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -9099,24 +8755,24 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -9126,12 +8782,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9166,7 +8819,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -9182,109 +8835,30 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/polyfill-php72", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9325,7 +8899,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -9341,34 +8915,30 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "508c652ba3ccf69f8c97f251534f229791b52a57" + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/508c652ba3ccf69f8c97f251534f229791b52a57", - "reference": "508c652ba3ccf69f8c97f251534f229791b52a57", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-php80": "^1.14" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9377,7 +8947,10 @@ ], "psr-4": { "Symfony\\Polyfill\\Php83\\": "" - } + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -9402,7 +8975,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" }, "funding": [ { @@ -9418,24 +8991,24 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.27.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166" + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/f3cf1a645c2734236ed1e2e671e273eeb3586166", - "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-uuid": "*" @@ -9445,12 +9018,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9484,7 +9054,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" }, "funding": [ { @@ -9500,24 +9070,24 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v6.3.0", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628" + "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8741e3ed7fe2e91ec099e02446fb86667a0f1628", - "reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628", + "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -9545,7 +9115,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.3.0" + "source": "https://github.com/symfony/process/tree/v7.2.0" }, "funding": [ { @@ -9561,46 +9131,42 @@ "type": "tidelift" } ], - "time": "2023-05-19T08:06:44+00:00" + "time": "2024-11-06T14:24:19+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v2.2.0", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "28a732c05bbad801304ad5a5c674cf2970508993" + "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/28a732c05bbad801304ad5a5c674cf2970508993", - "reference": "28a732c05bbad801304ad5a5c674cf2970508993", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/http-message": "^1.0 || ^2.0", - "symfony/http-foundation": "^5.4 || ^6.0" + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" }, "require-dev": { "nyholm/psr7": "^1.1", - "psr/log": "^1.1 || ^2 || ^3", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/config": "^5.4 || ^6.0", - "symfony/event-dispatcher": "^5.4 || ^6.0", - "symfony/framework-bundle": "^5.4 || ^6.0", - "symfony/http-kernel": "^5.4 || ^6.0", - "symfony/phpunit-bridge": "^6.2" - }, - "suggest": { - "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" }, "type": "symfony-bridge", - "extra": { - "branch-alias": { - "dev-main": "2.2-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Bridge\\PsrHttpMessage\\": "" @@ -9620,11 +9186,11 @@ }, { "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" + "homepage": "https://symfony.com/contributors" } ], "description": "PSR HTTP message bridge", - "homepage": "http://symfony.com", + "homepage": "https://symfony.com", "keywords": [ "http", "http-message", @@ -9632,8 +9198,7 @@ "psr-7" ], "support": { - "issues": "https://github.com/symfony/psr-http-message-bridge/issues", - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.2.0" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.2.0" }, "funding": [ { @@ -9649,39 +9214,38 @@ "type": "tidelift" } ], - "time": "2023-04-21T08:40:19+00:00" + "time": "2024-09-26T08:57:56+00:00" }, { "name": "symfony/routing", - "version": "v6.3.1", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "d37ad1779c38b8eb71996d17dc13030dcb7f9cf5" + "reference": "e10a2450fa957af6c448b9b93c9010a4e4c0725e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/d37ad1779c38b8eb71996d17dc13030dcb7f9cf5", - "reference": "d37ad1779c38b8eb71996d17dc13030dcb7f9cf5", + "url": "https://api.github.com/repos/symfony/routing/zipball/e10a2450fa957af6c448b9b93c9010a4e4c0725e", + "reference": "e10a2450fa957af6c448b9b93c9010a4e4c0725e", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { - "doctrine/annotations": "<1.12", - "symfony/config": "<6.2", - "symfony/dependency-injection": "<5.4", - "symfony/yaml": "<5.4" + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" }, "require-dev": { - "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", - "symfony/config": "^6.2", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/http-foundation": "^5.4|^6.0", - "symfony/yaml": "^5.4|^6.0" + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -9715,7 +9279,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.3.1" + "source": "https://github.com/symfony/routing/tree/v7.2.0" }, "funding": [ { @@ -9731,25 +9295,26 @@ "type": "tidelift" } ], - "time": "2023-06-05T15:30:22+00:00" + "time": "2024-11-25T11:08:51+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.3.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", - "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -9757,7 +9322,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -9797,7 +9362,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -9813,24 +9378,24 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/string", - "version": "v6.3.0", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f" + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f2e190ee75ff0f5eced645ec0be5c66fac81f51f", - "reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", @@ -9840,11 +9405,12 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0", - "symfony/http-client": "^5.4|^6.0", - "symfony/intl": "^6.2", + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0" + "symfony/var-exporter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -9883,7 +9449,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.3.0" + "source": "https://github.com/symfony/string/tree/v7.2.0" }, "funding": [ { @@ -9899,54 +9465,55 @@ "type": "tidelift" } ], - "time": "2023-03-21T21:06:29+00:00" + "time": "2024-11-13T13:31:26+00:00" }, { "name": "symfony/translation", - "version": "v6.3.0", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "f72b2cba8f79dd9d536f534f76874b58ad37876f" + "reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/f72b2cba8f79dd9d536f534f76874b58ad37876f", - "reference": "f72b2cba8f79dd9d536f534f76874b58ad37876f", + "url": "https://api.github.com/repos/symfony/translation/zipball/dc89e16b44048ceecc879054e5b7f38326ab6cc5", + "reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/translation-contracts": "^2.5|^3.0" }, "conflict": { - "symfony/config": "<5.4", - "symfony/console": "<5.4", - "symfony/dependency-injection": "<5.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<5.4", + "symfony/http-kernel": "<6.4", "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<5.4", - "symfony/yaml": "<5.4" + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" }, "provide": { "symfony/translation-implementation": "2.3|3.0" }, "require-dev": { - "nikic/php-parser": "^4.13", + "nikic/php-parser": "^4.18|^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/finder": "^5.4|^6.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/intl": "^5.4|^6.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^5.4|^6.0", + "symfony/routing": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^5.4|^6.0" + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -9977,7 +9544,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.3.0" + "source": "https://github.com/symfony/translation/tree/v7.2.0" }, "funding": [ { @@ -9993,20 +9560,20 @@ "type": "tidelift" } ], - "time": "2023-05-19T12:46:45+00:00" + "time": "2024-11-12T20:47:56+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.3.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "02c24deb352fb0d79db5486c0c79905a85e37e86" + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/02c24deb352fb0d79db5486c0c79905a85e37e86", - "reference": "02c24deb352fb0d79db5486c0c79905a85e37e86", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", "shasum": "" }, "require": { @@ -10015,7 +9582,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -10055,7 +9622,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" }, "funding": [ { @@ -10071,28 +9638,28 @@ "type": "tidelift" } ], - "time": "2023-05-30T17:17:10+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/uid", - "version": "v6.3.0", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "01b0f20b1351d997711c56f1638f7a8c3061e384" + "reference": "2d294d0c48df244c71c105a169d0190bfb080426" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/01b0f20b1351d997711c56f1638f7a8c3061e384", - "reference": "01b0f20b1351d997711c56f1638f7a8c3061e384", + "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426", + "reference": "2d294d0c48df244c71c105a169d0190bfb080426", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^5.4|^6.0" + "symfony/console": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -10129,7 +9696,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.3.0" + "source": "https://github.com/symfony/uid/tree/v7.2.0" }, "funding": [ { @@ -10145,35 +9712,36 @@ "type": "tidelift" } ], - "time": "2023-04-08T07:25:02+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.3.1", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c81268d6960ddb47af17391a27d222bd58cf0515" + "reference": "c6a22929407dec8765d6e2b6ff85b800b245879c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c81268d6960ddb47af17391a27d222bd58cf0515", - "reference": "c81268d6960ddb47af17391a27d222bd58cf0515", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6a22929407dec8765d6e2b6ff85b800b245879c", + "reference": "c6a22929407dec8765d6e2b6ff85b800b245879c", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/console": "<5.4" + "symfony/console": "<6.4" }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/uid": "^5.4|^6.0", - "twig/twig": "^2.13|^3.0.4" + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" }, "bin": [ "Resources/bin/var-dump-server" @@ -10211,7 +9779,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.3.1" + "source": "https://github.com/symfony/var-dumper/tree/v7.2.0" }, "funding": [ { @@ -10227,27 +9795,29 @@ "type": "tidelift" } ], - "time": "2023-06-21T12:08:28+00:00" + "time": "2024-11-08T15:48:14+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.3.0", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "db5416d04269f2827d8c54331ba4cfa42620d350" + "reference": "1a6a89f95a46af0f142874c9d650a6358d13070d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/db5416d04269f2827d8c54331ba4cfa42620d350", - "reference": "db5416d04269f2827d8c54331ba4cfa42620d350", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/1a6a89f95a46af0f142874c9d650a6358d13070d", + "reference": "1a6a89f95a46af0f142874c9d650a6358d13070d", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/var-dumper": "^5.4|^6.0" + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -10285,7 +9855,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.3.0" + "source": "https://github.com/symfony/var-exporter/tree/v7.2.0" }, "funding": [ { @@ -10301,81 +9871,27 @@ "type": "tidelift" } ], - "time": "2023-04-21T08:48:44+00:00" - }, - { - "name": "tightenco/collect", - "version": "v5.6.33", - "source": { - "type": "git", - "url": "https://github.com/tighten/collect.git", - "reference": "d7381736dca44ac17d0805a25191b094e5a22446" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/tighten/collect/zipball/d7381736dca44ac17d0805a25191b094e5a22446", - "reference": "d7381736dca44ac17d0805a25191b094e5a22446", - "shasum": "" - }, - "require": { - "php": ">=7.1.3", - "symfony/var-dumper": ">=3.1.10" - }, - "require-dev": { - "mockery/mockery": "~1.0", - "nesbot/carbon": "~1.20", - "phpunit/phpunit": "~7.0" - }, - "type": "library", - "autoload": { - "files": [ - "src/Collect/Support/helpers.php", - "src/Collect/Support/alias.php" - ], - "psr-4": { - "Tightenco\\Collect\\": "src/Collect" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com" - } - ], - "description": "Collect - Illuminate Collections as a separate package.", - "keywords": [ - "collection", - "laravel" - ], - "support": { - "issues": "https://github.com/tighten/collect/issues", - "source": "https://github.com/tighten/collect/tree/v5.6.33" - }, - "time": "2018-08-09T16:56:26+00:00" + "time": "2024-10-18T07:58:17+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "2.2.6", + "version": "v2.2.7", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c" + "reference": "83ee6f38df0a63106a9e4536e3060458b74ccedb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/c42125b83a4fa63b187fdf29f9c93cb7733da30c", - "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/83ee6f38df0a63106a9e4536e3060458b74ccedb", + "reference": "83ee6f38df0a63106a9e4536e3060458b74ccedb", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^5.5 || ^7.0 || ^8.0", - "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5 || ^8.5.21 || ^9.5.10" @@ -10406,37 +9922,37 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.6" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.2.7" }, - "time": "2023-01-03T09:29:04+00:00" + "time": "2023-12-08T13:03:43+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v5.5.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", - "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.0.2", - "php": "^7.1.3 || ^8.0", - "phpoption/phpoption": "^1.8", - "symfony/polyfill-ctype": "^1.23", - "symfony/polyfill-mbstring": "^1.23.1", - "symfony/polyfill-php80": "^1.23.1" + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-filter": "*", - "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "suggest": { "ext-filter": "Required to use the boolean validator." @@ -10445,10 +9961,10 @@ "extra": { "bamarni-bin": { "bin-links": true, - "forward-command": true + "forward-command": false }, "branch-alias": { - "dev-master": "5.5-dev" + "dev-master": "5.6-dev" } }, "autoload": { @@ -10480,7 +9996,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" }, "funding": [ { @@ -10492,20 +10008,20 @@ "type": "tidelift" } ], - "time": "2022-10-16T01:01:54+00:00" + "time": "2024-07-20T21:52:34+00:00" }, { "name": "voku/portable-ascii", - "version": "2.0.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/voku/portable-ascii.git", - "reference": "b56450eed252f6801410d810c8e1727224ae0743" + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b56450eed252f6801410d810c8e1727224ae0743", - "reference": "b56450eed252f6801410d810c8e1727224ae0743", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", "shasum": "" }, "require": { @@ -10530,7 +10046,7 @@ "authors": [ { "name": "Lars Moelleken", - "homepage": "http://www.moelleken.org/" + "homepage": "https://www.moelleken.org/" } ], "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", @@ -10542,7 +10058,7 @@ ], "support": { "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/2.0.1" + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" }, "funding": [ { @@ -10566,7 +10082,386 @@ "type": "tidelift" } ], - "time": "2022-03-08T17:03:00+00:00" + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "web-token/jwt-core", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-core.git", + "reference": "4d956e786a4e35d54c74787ebff840a0311c5e83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-core/zipball/4d956e786a4e35d54c74787ebff840a0311c5e83", + "reference": "4d956e786a4e35d54c74787ebff840a0311c5e83", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10", + "ext-json": "*", + "ext-mbstring": "*", + "fgrosse/phpasn1": "^2.0", + "paragonie/constant_time_encoding": "^2.4", + "php": ">=8.1" + }, + "conflict": { + "spomky-labs/jose": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Core\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "Core component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-core/tree/3.1.2" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2022-08-04T21:04:09+00:00" + }, + { + "name": "web-token/jwt-key-mgmt", + "version": "3.1.7", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-key-mgmt.git", + "reference": "bf6dec304f2a718d70f7316e498c612317c59e08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-key-mgmt/zipball/bf6dec304f2a718d70f7316e498c612317c59e08", + "reference": "bf6dec304f2a718d70f7316e498c612317c59e08", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "web-token/jwt-core": "^3.0" + }, + "suggest": { + "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", + "php-http/httplug": "To enable JKU/X5U support.", + "php-http/message-factory": "To enable JKU/X5U support.", + "web-token/jwt-util-ecc": "To use EC key analyzers." + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\KeyManagement\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-key-mgmt/contributors" + } + ], + "description": "Key Management component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-key-mgmt/tree/3.1.7" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2023-02-02T17:25:26+00:00" + }, + { + "name": "web-token/jwt-signature", + "version": "3.1.7", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature.git", + "reference": "14b71230d9632564e356b785366ad36880964190" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature/zipball/14b71230d9632564e356b785366ad36880964190", + "reference": "14b71230d9632564e356b785366ad36880964190", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "web-token/jwt-core": "^3.0" + }, + "suggest": { + "web-token/jwt-signature-algorithm-ecdsa": "ECDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-eddsa": "EdDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-experimental": "Experimental Signature Algorithms", + "web-token/jwt-signature-algorithm-hmac": "HMAC Based Signature Algorithms", + "web-token/jwt-signature-algorithm-none": "None Signature Algorithm", + "web-token/jwt-signature-algorithm-rsa": "RSA Based Signature Algorithms" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-signature/contributors" + } + ], + "description": "Signature component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature/tree/3.1.7" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2023-02-02T17:25:26+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-ecdsa", + "version": "3.1.7", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-ecdsa.git", + "reference": "e09159600f19832cf4a68921e7299e564bc0eaf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-ecdsa/zipball/e09159600f19832cf4a68921e7299e564bc0eaf9", + "reference": "e09159600f19832cf4a68921e7299e564bc0eaf9", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=8.1", + "web-token/jwt-signature": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\Algorithm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "ECDSA Based Signature Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-ecdsa/tree/3.1.7" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2022-08-04T21:04:09+00:00" + }, + { + "name": "web-token/jwt-util-ecc", + "version": "3.2.10", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-util-ecc.git", + "reference": "9edf9b76bccf2e1db58fcc49db1d916d929335c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-util-ecc/zipball/9edf9b76bccf2e1db58fcc49db1d916d929335c0", + "reference": "9edf9b76bccf2e1db58fcc49db1d916d929335c0", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12", + "php": ">=8.1" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance", + "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Core\\Util\\Ecc\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "ECC Tools for the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-util-ecc/tree/3.2.10" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2024-01-02T17:55:33+00:00" }, { "name": "webmozart/assert", @@ -10628,180 +10523,18 @@ } ], "packages-dev": [ - { - "name": "brianium/paratest", - "version": "v6.10.0", - "source": { - "type": "git", - "url": "https://github.com/paratestphp/paratest.git", - "reference": "c2243b20bcd99c3f651018d1447144372f39b4fa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/c2243b20bcd99c3f651018d1447144372f39b4fa", - "reference": "c2243b20bcd99c3f651018d1447144372f39b4fa", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-simplexml": "*", - "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1", - "jean85/pretty-package-versions": "^2.0.5", - "php": "^7.3 || ^8.0", - "phpunit/php-code-coverage": "^9.2.25", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-timer": "^5.0.3", - "phpunit/phpunit": "^9.6.4", - "sebastian/environment": "^5.1.5", - "symfony/console": "^5.4.21 || ^6.2.7", - "symfony/process": "^5.4.21 || ^6.2.7" - }, - "require-dev": { - "doctrine/coding-standard": "^10.0.0", - "ext-pcov": "*", - "ext-posix": "*", - "infection/infection": "^0.26.19", - "squizlabs/php_codesniffer": "^3.7.2", - "symfony/filesystem": "^5.4.21 || ^6.2.7", - "vimeo/psalm": "^5.7.7" - }, - "bin": [ - "bin/paratest", - "bin/paratest.bat", - "bin/paratest_for_phpstorm" - ], - "type": "library", - "autoload": { - "psr-4": { - "ParaTest\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Brian Scaturro", - "email": "scaturrob@gmail.com", - "role": "Developer" - }, - { - "name": "Filippo Tessarotto", - "email": "zoeslam@gmail.com", - "role": "Developer" - } - ], - "description": "Parallel testing for PHP", - "homepage": "https://github.com/paratestphp/paratest", - "keywords": [ - "concurrent", - "parallel", - "phpunit", - "testing" - ], - "support": { - "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.10.0" - }, - "funding": [ - { - "url": "https://github.com/sponsors/Slamdunk", - "type": "github" - }, - { - "url": "https://paypal.me/filippotessarotto", - "type": "paypal" - } - ], - "time": "2023-05-25T13:47:58+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "require-dev": { - "doctrine/coding-standard": "^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2022-12-30T00:23:10+00:00" - }, { "name": "fakerphp/faker", - "version": "v1.23.0", + "version": "v1.24.1", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01" + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e3daa170d00fde61ea7719ef47bb09bb8f1d9b01", - "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", "shasum": "" }, "require": { @@ -10827,11 +10560,6 @@ "ext-mbstring": "Required for multibyte Unicode string functionality." }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "v1.21-dev" - } - }, "autoload": { "psr-4": { "Faker\\": "src/Faker/" @@ -10854,93 +10582,32 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.23.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" }, - "time": "2023-06-12T08:44:38+00:00" - }, - { - "name": "fidry/cpu-core-counter", - "version": "0.5.1", - "source": { - "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/b58e5a3933e541dc286cc91fc4f3898bbc6f1623", - "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "fidry/makefile": "^0.2.0", - "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^9.5.26 || ^8.5.31", - "theofidry/php-cs-fixer-config": "^1.0", - "webmozarts/strict-phpunit": "^7.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Fidry\\CpuCoreCounter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Théo FIDRY", - "email": "theo.fidry@gmail.com" - } - ], - "description": "Tiny utility to get the number of CPU cores.", - "keywords": [ - "CPU", - "core" - ], - "support": { - "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/0.5.1" - }, - "funding": [ - { - "url": "https://github.com/theofidry", - "type": "github" - } - ], - "time": "2022-12-24T12:35:10+00:00" + "time": "2024-11-21T13:46:39+00:00" }, { "name": "filp/whoops", - "version": "2.15.3", + "version": "2.16.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "c83e88a30524f9360b11f585f71e6b17313b7187" + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/c83e88a30524f9360b11f585f71e6b17313b7187", - "reference": "c83e88a30524f9360b11f585f71e6b17313b7187", + "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2", + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0 || ^8.0", + "php": "^7.1 || ^8.0", "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { - "mockery/mockery": "^0.9 || ^1.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" }, "suggest": { "symfony/var-dumper": "Pretty print complex values better with var-dumper available", @@ -10980,7 +10647,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.15.3" + "source": "https://github.com/filp/whoops/tree/2.16.0" }, "funding": [ { @@ -10988,7 +10655,7 @@ "type": "github" } ], - "time": "2023-07-13T12:00:00+00:00" + "time": "2024-09-25T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -11042,39 +10709,44 @@ "time": "2020-07-09T08:09:16+00:00" }, { - "name": "jean85/pretty-package-versions", - "version": "2.0.5", + "name": "laravel/pint", + "version": "v1.18.3", "source": { "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + "url": "https://github.com/laravel/pint.git", + "reference": "cef51821608239040ab841ad6e1c6ae502ae3026" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "url": "https://api.github.com/repos/laravel/pint/zipball/cef51821608239040ab841ad6e1c6ae502ae3026", + "reference": "cef51821608239040ab841ad6e1c6ae502ae3026", "shasum": "" }, "require": { - "composer-runtime-api": "^2.0.0", - "php": "^7.1|^8.0" + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.17", - "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^0.12.66", - "phpunit/phpunit": "^7.5|^8.5|^9.4", - "vimeo/psalm": "^4.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } + "friendsofphp/php-cs-fixer": "^3.65.0", + "illuminate/view": "^10.48.24", + "larastan/larastan": "^2.9.11", + "laravel-zero/framework": "^10.4.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^1.17.0", + "pestphp/pest": "^2.36.0" }, + "bin": [ + "builds/pint" + ], + "type": "project", "autoload": { "psr-4": { - "Jean85\\": "src/" + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" } }, "notification-url": "https://packagist.org/downloads/", @@ -11083,56 +10755,56 @@ ], "authors": [ { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" } ], - "description": "A library to get pretty versions strings of installed dependencies", + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", "keywords": [ - "composer", - "package", - "release", - "versions" + "format", + "formatter", + "lint", + "linter", + "php" ], "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" }, - "time": "2021-10-08T21:21:46+00:00" + "time": "2024-11-26T15:34:00+00:00" }, { "name": "laravel/telescope", - "version": "v4.15.2", + "version": "v5.2.6", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "5d74ae4c9f269b756d7877ad1527770c59846e14" + "reference": "7ee46fbea8e3b01108575c8edf7377abddfe8bb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/5d74ae4c9f269b756d7877ad1527770c59846e14", - "reference": "5d74ae4c9f269b756d7877ad1527770c59846e14", + "url": "https://api.github.com/repos/laravel/telescope/zipball/7ee46fbea8e3b01108575c8edf7377abddfe8bb9", + "reference": "7ee46fbea8e3b01108575c8edf7377abddfe8bb9", "shasum": "" }, "require": { "ext-json": "*", - "laravel/framework": "^8.37|^9.0|^10.0", + "laravel/framework": "^8.37|^9.0|^10.0|^11.0", "php": "^8.0", - "symfony/var-dumper": "^5.0|^6.0" + "symfony/console": "^5.3|^6.0|^7.0", + "symfony/var-dumper": "^5.0|^6.0|^7.0" }, "require-dev": { "ext-gd": "*", "guzzlehttp/guzzle": "^6.0|^7.0", - "laravel/octane": "^1.4", - "orchestra/testbench": "^6.0|^7.0|^8.0", + "laravel/octane": "^1.4|^2.0|dev-develop", + "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.0|^10.5" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - }, "laravel": { "providers": [ "Laravel\\Telescope\\TelescopeServiceProvider" @@ -11167,43 +10839,37 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v4.15.2" + "source": "https://github.com/laravel/telescope/tree/v5.2.6" }, - "time": "2023-07-13T20:06:27+00:00" + "time": "2024-11-25T20:34:58+00:00" }, { "name": "mockery/mockery", - "version": "1.6.2", + "version": "1.6.12", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "13a7fa2642c76c58fa2806ef7f565344c817a191" + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/13a7fa2642c76c58fa2806ef7f565344c817a191", - "reference": "13a7fa2642c76c58fa2806ef7f565344c817a191", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", "shasum": "" }, "require": { "hamcrest/hamcrest-php": "^2.0.1", "lib-pcre": ">=7.0", - "php": "^7.4 || ^8.0" + "php": ">=7.3" }, "conflict": { "phpunit/phpunit": "<8.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.3", - "psalm/plugin-phpunit": "^0.18", - "vimeo/psalm": "^5.9" + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.6.x-dev" - } - }, "autoload": { "files": [ "library/helpers.php", @@ -11221,12 +10887,20 @@ { "name": "Pádraic Brady", "email": "padraic.brady@gmail.com", - "homepage": "http://blog.astrumfutura.com" + "homepage": "https://github.com/padraic", + "role": "Author" }, { "name": "Dave Marshall", "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "http://davedevelopment.co.uk" + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" } ], "description": "Mockery is a simple yet flexible PHP mock object framework", @@ -11244,23 +10918,26 @@ "testing" ], "support": { + "docs": "https://docs.mockery.io/", "issues": "https://github.com/mockery/mockery/issues", - "source": "https://github.com/mockery/mockery/tree/1.6.2" + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" }, - "time": "2023-06-07T09:07:52+00:00" + "time": "2024-05-16T03:13:13+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "" }, "require": { @@ -11268,11 +10945,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -11298,7 +10976,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" }, "funding": [ { @@ -11306,49 +10984,58 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-11-08T17:47:46+00:00" }, { "name": "nunomaduro/collision", - "version": "v6.4.0", + "version": "v8.5.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "f05978827b9343cba381ca05b8c7deee346b6015" + "reference": "f5c101b929c958e849a633283adff296ed5f38f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/f05978827b9343cba381ca05b8c7deee346b6015", - "reference": "f05978827b9343cba381ca05b8c7deee346b6015", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/f5c101b929c958e849a633283adff296ed5f38f5", + "reference": "f5c101b929c958e849a633283adff296ed5f38f5", "shasum": "" }, "require": { - "filp/whoops": "^2.14.5", - "php": "^8.0.0", - "symfony/console": "^6.0.2" + "filp/whoops": "^2.16.0", + "nunomaduro/termwind": "^2.1.0", + "php": "^8.2.0", + "symfony/console": "^7.1.5" + }, + "conflict": { + "laravel/framework": "<11.0.0 || >=12.0.0", + "phpunit/phpunit": "<10.5.1 || >=12.0.0" }, "require-dev": { - "brianium/paratest": "^6.4.1", - "laravel/framework": "^9.26.1", - "laravel/pint": "^1.1.1", - "nunomaduro/larastan": "^1.0.3", - "nunomaduro/mock-final-classes": "^1.1.0", - "orchestra/testbench": "^7.7", - "phpunit/phpunit": "^9.5.23", - "spatie/ignition": "^1.4.1" + "larastan/larastan": "^2.9.8", + "laravel/framework": "^11.28.0", + "laravel/pint": "^1.18.1", + "laravel/sail": "^1.36.0", + "laravel/sanctum": "^4.0.3", + "laravel/tinker": "^2.10.0", + "orchestra/testbench-core": "^9.5.3", + "pestphp/pest": "^2.36.0 || ^3.4.0", + "sebastian/environment": "^6.1.0 || ^7.2.0" }, "type": "library", "extra": { - "branch-alias": { - "dev-develop": "6.x-dev" - }, "laravel": { "providers": [ "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" } }, "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], "psr-4": { "NunoMaduro\\Collision\\": "src/" } @@ -11394,24 +11081,25 @@ "type": "patreon" } ], - "time": "2023-01-03T12:54:54+00:00" + "time": "2024-10-15T16:06:32+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -11452,9 +11140,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -11509,35 +11203,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.26", + "version": "11.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" + "reference": "418c59fd080954f8c4aa5631d9502ecda2387118" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", + "reference": "418c59fd080954f8c4aa5631d9502ecda2387118", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "nikic/php-parser": "^5.3.1", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.5.0" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -11546,7 +11240,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "11.0.x-dev" } }, "autoload": { @@ -11574,7 +11268,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8" }, "funding": [ { @@ -11582,32 +11277,32 @@ "type": "github" } ], - "time": "2023-03-06T12:58:08+00:00" + "time": "2024-12-11T12:34:27+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -11634,7 +11329,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" }, "funding": [ { @@ -11642,28 +11338,28 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2024-08-27T05:02:59+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "suggest": { "ext-pcntl": "*" @@ -11671,7 +11367,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -11697,7 +11393,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" }, "funding": [ { @@ -11705,32 +11402,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2024-07-03T05:07:44+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -11756,7 +11453,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" }, "funding": [ { @@ -11764,32 +11462,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2024-07-03T05:08:43+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -11815,7 +11513,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" }, "funding": [ { @@ -11823,54 +11522,52 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2024-07-03T05:09:35+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.10", + "version": "11.5.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328" + "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a6d351645c3fe5a30f5e86be6577d946af65a328", - "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2b94d4f2450b9869fa64a46fd8a6a41997aef56a", + "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", - "sebastian/version": "^3.0.2" + "myclabs/deep-copy": "^1.12.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.7", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.1", + "sebastian/comparator": "^6.2.1", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.3.0", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.0", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" }, "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" @@ -11878,7 +11575,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-main": "11.5-dev" } }, "autoload": { @@ -11910,7 +11607,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.10" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.1" }, "funding": [ { @@ -11926,32 +11623,32 @@ "type": "tidelift" } ], - "time": "2023-07-10T04:04:23+00:00" + "time": "2024-12-11T10:52:48+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -11974,7 +11671,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" }, "funding": [ { @@ -11982,32 +11680,32 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-07-03T04:41:36+00:00" }, { "name": "sebastian/code-unit", - "version": "1.0.8", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", + "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -12030,7 +11728,8 @@ "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" }, "funding": [ { @@ -12038,32 +11737,32 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2024-12-12T09:59:06+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -12085,7 +11784,8 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" }, "funding": [ { @@ -12093,34 +11793,36 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2024-07-03T04:45:54+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "6.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/43d129d6a0f81c78bee378b46688293eb7ea3739", + "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.2-dev" } }, "autoload": { @@ -12159,7 +11861,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.2.1" }, "funding": [ { @@ -12167,33 +11870,33 @@ "type": "github" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2024-10-31T05:30:08+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -12216,7 +11919,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" }, "funding": [ { @@ -12224,33 +11928,33 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2024-07-03T04:49:50+00:00" }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3", + "phpunit/phpunit": "^11.0", "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -12282,7 +11986,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" }, "funding": [ { @@ -12290,27 +11995,27 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-07-03T04:53:05+00:00" }, { "name": "sebastian/environment", - "version": "5.1.5", + "version": "7.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "suggest": { "ext-posix": "*" @@ -12318,7 +12023,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "7.2-dev" } }, "autoload": { @@ -12337,7 +12042,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -12345,7 +12050,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" }, "funding": [ { @@ -12353,34 +12059,34 @@ "type": "github" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2024-07-03T04:54:44+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "6.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -12422,7 +12128,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" }, "funding": [ { @@ -12430,38 +12137,35 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-12-05T09:17:50+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.5", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -12480,13 +12184,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" }, "funding": [ { @@ -12494,33 +12199,33 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2024-07-03T04:57:36+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -12543,7 +12248,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" }, "funding": [ { @@ -12551,34 +12257,34 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2024-07-03T04:58:38+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -12600,7 +12306,8 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" }, "funding": [ { @@ -12608,32 +12315,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2024-07-03T05:00:13+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -12655,7 +12362,8 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" }, "funding": [ { @@ -12663,32 +12371,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2024-07-03T05:01:32+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -12718,7 +12426,8 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" }, "funding": [ { @@ -12726,87 +12435,32 @@ "type": "github" } ], - "time": "2023-02-03T06:07:39+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-07-03T05:10:34+00:00" }, { "name": "sebastian/type", - "version": "3.2.1", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -12829,7 +12483,8 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" }, "funding": [ { @@ -12837,29 +12492,29 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2024-09-17T13:12:04+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -12882,7 +12537,8 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" }, "funding": [ { @@ -12890,20 +12546,72 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2024-10-09T05:16:32+00:00" }, { - "name": "theseer/tokenizer", - "version": "1.2.1", + "name": "staabm/side-effects-detector", + "version": "1.0.5", "source": { "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -12932,7 +12640,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -12940,16 +12648,16 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.1|^8.2", + "php": "^8.2|^8.3", "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", @@ -12958,6 +12666,6 @@ "ext-mbstring": "*", "ext-openssl": "*" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/config/api.php b/config/api.php new file mode 100644 index 000000000..13003bc26 --- /dev/null +++ b/config/api.php @@ -0,0 +1,18 @@ + [ + 'v1Dot1' => [ + 'accounts' => [ + 'usernameToId' => [ + 'enabled' => env('PF_API_RL_V1DOT1_ACCT_U2ID_ENABLED', true), + 'limit' => env('PF_API_RL_V1DOT1_ACCT_U2ID_LIMIT', 30), + 'decay' => env('PF_API_RL_V1DOT1_ACCT_U2ID_DECAY', 120), + 'ip_enabled' => env('PF_API_RL_V1DOT1_ACCT_U2ID_BY_IP_ENABLED', false), + 'ip_limit' => env('PF_API_RL_V1DOT1_ACCT_U2ID_BY_IP_LIMIT', 30), + 'ip_decay' => env('PF_API_RL_V1DOT1_ACCT_U2ID_BY_IP_DECAY', 120), + ] + ] + ] + ] +]; diff --git a/config/autospam.php b/config/autospam.php index 39975ec9b..bc0ce4681 100644 --- a/config/autospam.php +++ b/config/autospam.php @@ -33,5 +33,10 @@ return [ 'nlp' => [ 'enabled' => false, 'spam_sample_limit' => env('PF_AUTOSPAM_NLP_SPAM_SAMPLE_LIMIT', 200), + ], + + 'live_filters' => [ + 'enabled' => env('PF_AUTOSPAM_LIVE_FILTERS_ENABLED', false), + 'filters' => env('PF_AUTOSPAM_LIVE_FILTERS_CSV', ''), ] ]; diff --git a/config/cache.php b/config/cache.php index b2a854623..88129848e 100644 --- a/config/cache.php +++ b/config/cache.php @@ -36,17 +36,20 @@ return [ 'array' => [ 'driver' => 'array', + 'serialize' => false, ], 'database' => [ 'driver' => 'database', 'table' => 'cache', 'connection' => null, + 'lock_connection' => null, ], 'file' => [ 'driver' => 'file', 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), ], 'memcached' => [ @@ -70,7 +73,8 @@ return [ 'redis' => [ 'driver' => 'redis', - 'client' => env('REDIS_CLIENT', 'phpredis'), + 'lock_connection' => 'default', + 'client' => env('REDIS_CLIENT', 'predis'), 'default' => [ 'scheme' => env('REDIS_SCHEME', 'tcp'), @@ -83,6 +87,25 @@ return [ ], + 'redis:session' => [ + 'driver' => 'redis', + 'connection' => 'default', + 'prefix' => 'pf_session', + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + ], /* @@ -101,4 +124,5 @@ return [ str_slug(env('APP_NAME', 'laravel'), '_').'_cache' ), + 'limiter' => env('CACHE_LIMITER_DRIVER', 'redis'), ]; diff --git a/config/cors.php b/config/cors.php index 92b4b8e8c..1e81a015c 100644 --- a/config/cors.php +++ b/config/cors.php @@ -22,7 +22,9 @@ return [ * Example: ['api/*'] */ 'paths' => [ - '.well-known/*' + '.well-known/*', + 'api/*', + 'oauth/*' ], /* @@ -48,7 +50,8 @@ return [ /* * Sets the Access-Control-Expose-Headers response header with these headers. */ - 'exposed_headers' => [], + // TODO: Add support for rate-limit related headers + 'exposed_headers' => ['Link'], /* * Sets the Access-Control-Max-Age response header when > 0. @@ -59,4 +62,4 @@ return [ * Sets the Access-Control-Allow-Credentials header. */ 'supports_credentials' => false, -]; \ No newline at end of file +]; diff --git a/config/database.php b/config/database.php index 38a197dac..92d6a2ba2 100644 --- a/config/database.php +++ b/config/database.php @@ -60,6 +60,26 @@ return [ ] ], + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + 'pgsql' => [ 'driver' => 'pgsql', 'host' => env('DB_HOST', '127.0.0.1'), diff --git a/config/federation.php b/config/federation.php index 773d3d16b..124935ec8 100644 --- a/config/federation.php +++ b/config/federation.php @@ -30,6 +30,8 @@ return [ 'ingest' => [ 'store_notes_without_followers' => env('AP_INGEST_STORE_NOTES_WITHOUT_FOLLOWERS', false), ], + + 'authorized_fetch' => env('AUTHORIZED_FETCH', false), ], 'atom' => [ @@ -49,7 +51,7 @@ return [ ], 'network_timeline' => env('PF_NETWORK_TIMELINE', true), - 'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 2), + 'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 90), 'custom_emoji' => [ 'enabled' => env('CUSTOM_EMOJI', false), @@ -57,4 +59,6 @@ return [ // max size in bytes, default is 2mb 'max_size' => env('CUSTOM_EMOJI_MAX_SIZE', 2000000), ], + + 'migration' => env('PF_ACCT_MIGRATION_ENABLED', true), ]; diff --git a/config/filesystems.php b/config/filesystems.php index 80e63ed99..00254e938 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -72,7 +72,7 @@ return [ 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION'), 'bucket' => env('AWS_BUCKET'), - 'visibility' => 'public', + 'visibility' => env('AWS_VISIBILITY', 'public'), 'url' => env('AWS_URL'), 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), diff --git a/config/groups.php b/config/groups.php new file mode 100644 index 000000000..24513e502 --- /dev/null +++ b/config/groups.php @@ -0,0 +1,13 @@ + env('GROUPS_ENABLED', false), + 'federation' => env('GROUPS_FEDERATION', true), + + 'acl' => [ + 'create_group' => [ + 'admins' => env('GROUPS_ACL_CREATE_ADMINS', true), + 'users' => env('GROUPS_ACL_CREATE_USERS', true), + ] + ] +]; diff --git a/config/horizon.php b/config/horizon.php index f9cfd960e..5f7b31c13 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -2,201 +2,206 @@ return [ - /* - |-------------------------------------------------------------------------- - | Horizon Domain - |-------------------------------------------------------------------------- - | - | This is the subdomain where Horizon will be accessible from. If this - | setting is null, Horizon will reside under the same domain as the - | application. Otherwise, this value will serve as the subdomain. - | - */ + /* + |-------------------------------------------------------------------------- + | Horizon Domain + |-------------------------------------------------------------------------- + | + | This is the subdomain where Horizon will be accessible from. If this + | setting is null, Horizon will reside under the same domain as the + | application. Otherwise, this value will serve as the subdomain. + | + */ - 'domain' => null, + 'domain' => null, - /* - |-------------------------------------------------------------------------- - | Horizon Path - |-------------------------------------------------------------------------- - | - | This is the URI path where Horizon will be accessible from. Feel free - | to change this path to anything you like. Note that the URI will not - | affect the paths of its internal API that aren't exposed to users. - | - */ + /* + |-------------------------------------------------------------------------- + | Horizon Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Horizon will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ - 'path' => 'horizon', + 'path' => 'horizon', - /* - |-------------------------------------------------------------------------- - | Horizon Redis Connection - |-------------------------------------------------------------------------- - | - | This is the name of the Redis connection where Horizon will store the - | meta information required for it to function. It includes the list - | of supervisors, failed jobs, job metrics, and other information. - | - */ + /* + |-------------------------------------------------------------------------- + | Horizon Redis Connection + |-------------------------------------------------------------------------- + | + | This is the name of the Redis connection where Horizon will store the + | meta information required for it to function. It includes the list + | of supervisors, failed jobs, job metrics, and other information. + | + */ - 'use' => 'default', + 'use' => 'default', - /* - |-------------------------------------------------------------------------- - | Horizon Redis Prefix - |-------------------------------------------------------------------------- - | - | This prefix will be used when storing all Horizon data in Redis. You - | may modify the prefix when you are running multiple installations - | of Horizon on the same server so that they don't have problems. - | - */ + /* + |-------------------------------------------------------------------------- + | Horizon Redis Prefix + |-------------------------------------------------------------------------- + | + | This prefix will be used when storing all Horizon data in Redis. You + | may modify the prefix when you are running multiple installations + | of Horizon on the same server so that they don't have problems. + | + */ - 'prefix' => env('HORIZON_PREFIX', 'horizon-'), + 'prefix' => env('HORIZON_PREFIX', 'horizon-'), - /* - |-------------------------------------------------------------------------- - | Horizon Route Middleware - |-------------------------------------------------------------------------- - | - | These middleware will get attached onto each Horizon route, giving you - | the chance to add your own middleware to this list or change any of - | the existing middleware. Or, you can simply stick with this list. - | - */ + /* + |-------------------------------------------------------------------------- + | Horizon Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will get attached onto each Horizon route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ - 'middleware' => ['web'], + 'middleware' => ['web'], - /* - |-------------------------------------------------------------------------- - | Queue Wait Time Thresholds - |-------------------------------------------------------------------------- - | - | This option allows you to configure when the LongWaitDetected event - | will be fired. Every connection / queue combination may have its - | own, unique threshold (in seconds) before this event is fired. - | - */ + /* + |-------------------------------------------------------------------------- + | Queue Wait Time Thresholds + |-------------------------------------------------------------------------- + | + | This option allows you to configure when the LongWaitDetected event + | will be fired. Every connection / queue combination may have its + | own, unique threshold (in seconds) before this event is fired. + | + */ - 'waits' => [ - 'redis:feed' => 30, - 'redis:follow' => 30, - 'redis:shared' => 30, - 'redis:default' => 30, - 'redis:inbox' => 30, - 'redis:low' => 30, - 'redis:high' => 30, - 'redis:delete' => 30, - 'redis:story' => 30, - 'redis:mmo' => 30, - ], + 'waits' => [ + 'redis:feed' => 30, + 'redis:follow' => 30, + 'redis:shared' => 30, + 'redis:default' => 30, + 'redis:inbox' => 30, + 'redis:low' => 30, + 'redis:high' => 30, + 'redis:delete' => 30, + 'redis:story' => 30, + 'redis:mmo' => 30, + 'redis:intbg' => 30, + 'redis:adelete' => 30, + 'redis:groups' => 30, + 'redis:move' => 30, + 'redis:pushnotify' => 30, + ], - /* - |-------------------------------------------------------------------------- - | Job Trimming Times - |-------------------------------------------------------------------------- - | - | Here you can configure for how long (in minutes) you desire Horizon to - | persist the recent and failed jobs. Typically, recent jobs are kept - | for one hour while all failed jobs are stored for an entire week. - | - */ + /* + |-------------------------------------------------------------------------- + | Job Trimming Times + |-------------------------------------------------------------------------- + | + | Here you can configure for how long (in minutes) you desire Horizon to + | persist the recent and failed jobs. Typically, recent jobs are kept + | for one hour while all failed jobs are stored for an entire week. + | + */ - 'trim' => [ - 'recent' => 60, - 'pending' => 60, - 'completed' => 60, - 'recent_failed' => 10080, - 'failed' => 10080, - 'monitored' => 10080, - ], + 'trim' => [ + 'recent' => 60, + 'pending' => 60, + 'completed' => 60, + 'recent_failed' => 10080, + 'failed' => 10080, + 'monitored' => 10080, + ], - /* - |-------------------------------------------------------------------------- - | Metrics - |-------------------------------------------------------------------------- - | - | Here you can configure how many snapshots should be kept to display in - | the metrics graph. This will get used in combination with Horizon's - | `horizon:snapshot` schedule to define how long to retain metrics. - | - */ + /* + |-------------------------------------------------------------------------- + | Metrics + |-------------------------------------------------------------------------- + | + | Here you can configure how many snapshots should be kept to display in + | the metrics graph. This will get used in combination with Horizon's + | `horizon:snapshot` schedule to define how long to retain metrics. + | + */ - 'metrics' => [ - 'trim_snapshots' => [ - 'job' => 24, - 'queue' => 24, - ], - ], + 'metrics' => [ + 'trim_snapshots' => [ + 'job' => 24, + 'queue' => 24, + ], + ], - /* - |-------------------------------------------------------------------------- - | Fast Termination - |-------------------------------------------------------------------------- - | - | When this option is enabled, Horizon's "terminate" command will not - | wait on all of the workers to terminate unless the --wait option - | is provided. Fast termination can shorten deployment delay by - | allowing a new instance of Horizon to start while the last - | instance will continue to terminate each of its workers. - | - */ + /* + |-------------------------------------------------------------------------- + | Fast Termination + |-------------------------------------------------------------------------- + | + | When this option is enabled, Horizon's "terminate" command will not + | wait on all of the workers to terminate unless the --wait option + | is provided. Fast termination can shorten deployment delay by + | allowing a new instance of Horizon to start while the last + | instance will continue to terminate each of its workers. + | + */ - 'fast_termination' => false, + 'fast_termination' => false, - /* - |-------------------------------------------------------------------------- - | Memory Limit (MB) - |-------------------------------------------------------------------------- - | - | This value describes the maximum amount of memory the Horizon worker - | may consume before it is terminated and restarted. You should set - | this value according to the resources available to your server. - | - */ + /* + |-------------------------------------------------------------------------- + | Memory Limit (MB) + |-------------------------------------------------------------------------- + | + | This value describes the maximum amount of memory the Horizon worker + | may consume before it is terminated and restarted. You should set + | this value according to the resources available to your server. + | + */ - 'memory_limit' => env('HORIZON_MEMORY_LIMIT', 64), + 'memory_limit' => env('HORIZON_MEMORY_LIMIT', 64), - /* - |-------------------------------------------------------------------------- - | Queue Worker Configuration - |-------------------------------------------------------------------------- - | - | Here you may define the queue worker settings used by your application - | in all environments. These supervisors and settings handle all your - | queued jobs and will be provisioned by Horizon during deployment. - | - */ + /* + |-------------------------------------------------------------------------- + | Queue Worker Configuration + |-------------------------------------------------------------------------- + | + | Here you may define the queue worker settings used by your application + | in all environments. These supervisors and settings handle all your + | queued jobs and will be provisioned by Horizon during deployment. + | + */ - 'environments' => [ - 'production' => [ - 'supervisor-1' => [ - 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo'], - 'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'), - 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), - 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20), - 'memory' => env('HORIZON_SUPERVISOR_MEMORY', 64), - 'tries' => env('HORIZON_SUPERVISOR_TRIES', 3), - 'nice' => env('HORIZON_SUPERVISOR_NICE', 0), - 'timeout' => env('HORIZON_SUPERVISOR_TIMEOUT', 300), - ], - ], + 'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete', 'move', 'pushnotify'], + 'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'), + 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), + 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20), + 'memory' => env('HORIZON_SUPERVISOR_MEMORY', 64), + 'tries' => env('HORIZON_SUPERVISOR_TRIES', 3), + 'nice' => env('HORIZON_SUPERVISOR_NICE', 0), + 'timeout' => env('HORIZON_SUPERVISOR_TIMEOUT', 300), + ], + ], - 'local' => [ - 'supervisor-1' => [ - 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo'], - 'balance' => 'auto', - 'minProcesses' => 1, - 'maxProcesses' => 20, - 'memory' => 128, - 'tries' => 3, - 'nice' => 0, - 'timeout' => 300 - ], - ], - ], + 'local' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete', 'move', 'pushnotify'], + 'balance' => 'auto', + 'minProcesses' => 1, + 'maxProcesses' => 20, + 'memory' => 128, + 'tries' => 3, + 'nice' => 0, + 'timeout' => 300 + ], + ], + ], - 'darkmode' => env('HORIZON_DARKMODE', false), + 'darkmode' => env('HORIZON_DARKMODE', false), ]; diff --git a/config/import.php b/config/import.php index 2d1af28e1..f754da490 100644 --- a/config/import.php +++ b/config/import.php @@ -39,6 +39,12 @@ return [ // Limit to specific user ids, in comma separated format 'user_ids' => env('PF_IMPORT_IG_PERM_ONLY_USER_IDS', null), + ], + + 'storage' => [ + 'cloud' => [ + 'enabled' => env('PF_IMPORT_IG_CLOUD_STORAGE', env('PF_ENABLE_CLOUD', false)), + ] ] ] ]; diff --git a/config/instance.php b/config/instance.php index 5161ecb80..18d887873 100644 --- a/config/instance.php +++ b/config/instance.php @@ -1,132 +1,189 @@ env('FORCE_HTTPS_URLS', true), + 'force_https_urls' => env('FORCE_HTTPS_URLS', true), - 'description' => env('INSTANCE_DESCRIPTION', 'Pixelfed - Photo sharing for everyone'), + 'description' => env('INSTANCE_DESCRIPTION', 'Pixelfed - Photo sharing for everyone'), - 'contact' => [ - 'enabled' => env('INSTANCE_CONTACT_FORM', false), - 'max_per_day' => env('INSTANCE_CONTACT_MAX_PER_DAY', 1), - ], + 'contact' => [ + 'enabled' => env('INSTANCE_CONTACT_FORM', false), + 'max_per_day' => env('INSTANCE_CONTACT_MAX_PER_DAY', 1), + ], - 'discover' => [ - 'public' => env('INSTANCE_DISCOVER_PUBLIC', false), - 'loops' => [ - 'enabled' => env('EXP_LOOPS', false), - ], - 'tags' => [ - 'is_public' => env('INSTANCE_PUBLIC_HASHTAGS', false) - ], - ], + 'discover' => [ + 'public' => env('INSTANCE_DISCOVER_PUBLIC', false), + 'loops' => [ + 'enabled' => env('EXP_LOOPS', false), + ], + 'tags' => [ + 'is_public' => env('INSTANCE_PUBLIC_HASHTAGS', false), + ], + 'beagle_api' => env('PF_INSTANCE_USE_BEAGLE_API', true), + ], - 'email' => env('INSTANCE_CONTACT_EMAIL'), + 'email' => env('INSTANCE_CONTACT_EMAIL'), - 'timeline' => [ - 'home' => [ - 'cached' => env('PF_HOME_TIMELINE_CACHE', false), - 'cache_ttl' => env('PF_HOME_TIMELINE_CACHE_TTL', 900) - ], + 'timeline' => [ + 'home' => [ + 'cached' => env('PF_HOME_TIMELINE_CACHE', false), + 'cache_ttl' => env('PF_HOME_TIMELINE_CACHE_TTL', 900), + ], - 'local' => [ - 'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false) - ], + 'local' => [ + 'cached' => env('INSTANCE_PUBLIC_TIMELINE_CACHED', false), + 'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false), + ], - 'network' => [ - 'cached' => env('PF_NETWORK_TIMELINE') ? env('INSTANCE_NETWORK_TIMELINE_CACHED', true) : false, - 'cache_dropoff' => env('INSTANCE_NETWORK_TIMELINE_CACHE_DROPOFF', 100), - 'max_hours_old' => env('INSTANCE_NETWORK_TIMELINE_CACHE_MAX_HOUR_INGEST', 6) - ] - ], + 'network' => [ + 'cached' => env('PF_NETWORK_TIMELINE') ? env('INSTANCE_NETWORK_TIMELINE_CACHED', false) : false, + 'cache_dropoff' => env('INSTANCE_NETWORK_TIMELINE_CACHE_DROPOFF', 100), + 'max_hours_old' => env('INSTANCE_NETWORK_TIMELINE_CACHE_MAX_HOUR_INGEST', 2160), + ], + ], - 'page' => [ - '404' => [ - 'header' => env('PAGE_404_HEADER', 'Sorry, this page isn\'t available.'), - 'body' => env('PAGE_404_BODY', 'The link you followed may be broken, or the page may have been removed. Go back to Pixelfed.') - ], - '503' => [ - 'header' => env('PAGE_503_HEADER', 'Service Unavailable'), - 'body' => env('PAGE_503_BODY', 'Our service is in maintenance mode, please try again later.') - ] - ], + 'page' => [ + '404' => [ + 'header' => env('PAGE_404_HEADER', 'Sorry, this page isn\'t available.'), + 'body' => env('PAGE_404_BODY', 'The link you followed may be broken, or the page may have been removed. Go back to Pixelfed.'), + ], + '503' => [ + 'header' => env('PAGE_503_HEADER', 'Service Unavailable'), + 'body' => env('PAGE_503_BODY', 'Our service is in maintenance mode, please try again later.'), + ], + ], - 'username' => [ - 'banned' => env('BANNED_USERNAMES'), - 'remote' => [ - 'formats' => ['@', 'from', 'custom'], - 'format' => in_array(env('USERNAME_REMOTE_FORMAT', '@'), ['@','from','custom']) ? env('USERNAME_REMOTE_FORMAT', '@') : '@', - 'custom' => env('USERNAME_REMOTE_CUSTOM_TEXT', null) - ] - ], + 'username' => [ + 'banned' => env('BANNED_USERNAMES'), + 'remote' => [ + 'formats' => ['@', 'from', 'custom'], + 'format' => in_array(env('USERNAME_REMOTE_FORMAT', '@'), ['@', 'from', 'custom']) ? env('USERNAME_REMOTE_FORMAT', '@') : '@', + 'custom' => env('USERNAME_REMOTE_CUSTOM_TEXT', null), + ], + ], - 'polls' => [ - 'enabled' => false - ], + 'polls' => [ + 'enabled' => false, + ], - 'stories' => [ - 'enabled' => env('STORIES_ENABLED', false), - ], + 'stories' => [ + 'enabled' => env('STORIES_ENABLED', false), + ], - 'restricted' => [ - 'enabled' => env('RESTRICTED_INSTANCE', false), - 'level' => 1 - ], + 'restricted' => [ + 'enabled' => env('RESTRICTED_INSTANCE', false), + 'level' => 1, + ], - 'oauth' => [ - 'token_expiration' => env('OAUTH_TOKEN_DAYS', 365), - 'refresh_expiration' => env('OAUTH_REFRESH_DAYS', 400), - 'pat' => [ - 'enabled' => env('OAUTH_PAT_ENABLED', false), - 'id' => env('OAUTH_PAT_ID'), - ] - ], + 'oauth' => [ + 'token_expiration' => env('OAUTH_TOKEN_DAYS', 365), + 'refresh_expiration' => env('OAUTH_REFRESH_DAYS', 400), + 'pat' => [ + 'enabled' => env('OAUTH_PAT_ENABLED', false), + 'id' => env('OAUTH_PAT_ID'), + ], + ], - 'label' => [ - 'covid' => [ - 'enabled' => env('ENABLE_COVID_LABEL', true), - 'url' => env('COVID_LABEL_URL', 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/advice-for-public'), - 'org' => env('COVID_LABEL_ORG', 'visit the WHO website') - ] - ], + 'label' => [ + 'covid' => [ + 'enabled' => env('ENABLE_COVID_LABEL', true), + 'url' => env('COVID_LABEL_URL', 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/advice-for-public'), + 'org' => env('COVID_LABEL_ORG', 'visit the WHO website'), + ], + ], - 'enable_cc' => env('ENABLE_CONFIG_CACHE', true), + 'enable_cc' => env('ENABLE_CONFIG_CACHE', true), - 'has_legal_notice' => env('INSTANCE_LEGAL_NOTICE', false), + 'has_legal_notice' => env('INSTANCE_LEGAL_NOTICE', false), - 'embed' => [ - 'profile' => env('INSTANCE_PROFILE_EMBEDS', true), - 'post' => env('INSTANCE_POST_EMBEDS', true), - ], + 'embed' => [ + 'profile' => env('INSTANCE_PROFILE_EMBEDS', true), + 'post' => env('INSTANCE_POST_EMBEDS', true), + ], - 'hide_nsfw_on_public_feeds' => env('PF_HIDE_NSFW_ON_PUBLIC_FEEDS', false), + 'hide_nsfw_on_public_feeds' => env('PF_HIDE_NSFW_ON_PUBLIC_FEEDS', false), - 'avatar' => [ - 'local_to_cloud' => env('PF_LOCAL_AVATAR_TO_CLOUD', false) - ], + 'avatar' => [ + 'local_to_cloud' => env('PF_LOCAL_AVATAR_TO_CLOUD', false), + ], - 'admin_invites' => [ - 'enabled' => env('PF_ADMIN_INVITES_ENABLED', true) - ], + 'admin_invites' => [ + 'enabled' => env('PF_ADMIN_INVITES_ENABLED', true), + ], - 'user_filters' => [ - 'max_user_blocks' => env('PF_MAX_USER_BLOCKS', 50), - 'max_user_mutes' => env('PF_MAX_USER_MUTES', 50) - ], + 'user_filters' => [ + 'max_user_blocks' => env('PF_MAX_USER_BLOCKS', 50), + 'max_user_mutes' => env('PF_MAX_USER_MUTES', 50), + 'max_domain_blocks' => env('PF_MAX_DOMAIN_BLOCKS', 50), + ], - 'reports' => [ - 'email' => [ - 'enabled' => env('INSTANCE_REPORTS_EMAIL_ENABLED', false), - 'to' => env('INSTANCE_REPORTS_EMAIL_ADDRESSES'), - 'autospam' => env('INSTANCE_REPORTS_EMAIL_AUTOSPAM', false) - ] - ], + 'reports' => [ + 'email' => [ + 'enabled' => env('INSTANCE_REPORTS_EMAIL_ENABLED', false), + 'to' => env('INSTANCE_REPORTS_EMAIL_ADDRESSES'), + 'autospam' => env('INSTANCE_REPORTS_EMAIL_AUTOSPAM', false), + ], + ], - 'landing' => [ - 'show_directory' => env('INSTANCE_LANDING_SHOW_DIRECTORY', true), - 'show_explore' => env('INSTANCE_LANDING_SHOW_EXPLORE', true), - ], + 'landing' => [ + 'show_directory' => env('INSTANCE_LANDING_SHOW_DIRECTORY', true), + 'show_explore' => env('INSTANCE_LANDING_SHOW_EXPLORE', true), + ], - 'banner' => [ - 'blurhash' => env('INSTANCE_BANNER_BLURHASH', 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt') - ] + 'banner' => [ + 'blurhash' => env('INSTANCE_BANNER_BLURHASH', 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt'), + ], + + 'parental_controls' => [ + 'enabled' => env('INSTANCE_PARENTAL_CONTROLS', false), + + 'limits' => [ + 'respect_open_registration' => env('INSTANCE_PARENTAL_CONTROLS_RESPECT_OPENREG', true), + 'max_children' => env('INSTANCE_PARENTAL_CONTROLS_MAX_CHILDREN', 1), + 'auto_verify_email' => true, + ], + ], + + 'software-update' => [ + 'disable_failed_warning' => env('INSTANCE_SOFTWARE_UPDATE_DISABLE_FAILED_WARNING', false), + ], + + 'notifications' => [ + 'gc' => [ + 'enabled' => env('INSTANCE_NOTIFY_AUTO_GC', false), + 'delete_after_days' => env('INSTANCE_NOTIFY_AUTO_GC_DEL_AFTER_DAYS', 365), + ], + + 'nag' => [ + 'enabled' => (bool) env('INSTANCE_NOTIFY_APP_GATEWAY', true), + 'api_key' => env('PIXELFED_PUSHGATEWAY_KEY', false), + 'endpoint' => 'push.pixelfed.net', + ], + ], + + 'curated_registration' => [ + 'enabled' => env('INSTANCE_CUR_REG', false), + + 'resend_confirmation_limit' => env('INSTANCE_CUR_REG_RESEND_LIMIT', 5), + + 'captcha_enabled' => env('INSTANCE_CUR_REG_CAPTCHA', env('CAPTCHA_ENABLED', false)), + + 'state' => [ + 'fallback_on_closed_reg' => true, + 'only_enabled_on_closed_reg' => env('INSTANCE_CUR_REG_STATE_ONLY_ON_CLOSED', true), + ], + + 'notify' => [ + 'admin' => [ + 'on_verify_email' => [ + 'enabled' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY', false), + 'bundle' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_BUNDLE', false), + 'max_per_day' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_MPD', 10), + 'cc_addresses' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_CC'), + ], + 'on_user_response' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_USER_RESPONSE', false), + ], + ], + ], + + 'show_peers' => env('INSTANCE_SHOW_PEERS', false), ]; diff --git a/config/mail.php b/config/mail.php index 4baa90ec8..423f61736 100644 --- a/config/mail.php +++ b/config/mail.php @@ -4,45 +4,93 @@ return [ /* |-------------------------------------------------------------------------- - | Mail Driver + | Default Mailer |-------------------------------------------------------------------------- | - | Laravel supports both SMTP and PHP's "mail" function as drivers for the - | sending of e-mail. You may specify which one you're using throughout - | your application here. By default, Laravel is setup for SMTP mail. - | - | Supported: "smtp", "sendmail", "mailgun", "mandrill", "ses", - | "sparkpost", "log", "array" + | This option controls the default mailer that is used to send any email + | messages sent by your application. Alternative mailers may be setup + | and used as needed; however, this mailer will be used by default. | */ - 'driver' => env('MAIL_DRIVER', 'smtp'), + 'default' => env('MAIL_DRIVER', 'smtp'), /* |-------------------------------------------------------------------------- - | SMTP Host Address + | Mailer Configurations |-------------------------------------------------------------------------- | - | Here you may provide the host address of the SMTP server used by your - | applications. A default option is provided that is compatible with - | the Mailgun mail service which will provide reliable deliveries. + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers to be used while + | sending an e-mail. You will specify which one you are using for your + | mailers below. You are free to add additional mailers as required. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "log", "array", "failover" | */ - 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'mailers' => [ + 'smtp' => [ + 'transport' => 'smtp', + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN'), + 'verify_peer' => env('MAIL_SMTP_VERIFY_PEER', true), + ], - /* - |-------------------------------------------------------------------------- - | SMTP Host Port - |-------------------------------------------------------------------------- - | - | This is the SMTP port used by your application to deliver e-mails to - | users of the application. Like the host we have set this value to - | stay compatible with the Mailgun e-mail application by default. - | - */ + 'ses' => [ + 'transport' => 'ses', + ], - 'port' => env('MAIL_PORT', 587), + 'mailgun' => [ + 'transport' => 'mailgun', + 'client' => [ + 'timeout' => 5, + ], + ], + + 'postmark' => [ + 'transport' => 'postmark', + 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + 'client' => [ + 'timeout' => 5, + ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'log', + ], + ], + ], /* |-------------------------------------------------------------------------- @@ -57,63 +105,9 @@ return [ 'from' => [ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', 'Example'), + 'name' => env('MAIL_FROM_NAME', 'Example'), ], - /* - |-------------------------------------------------------------------------- - | E-Mail Encryption Protocol - |-------------------------------------------------------------------------- - | - | Here you may specify the encryption protocol that should be used when - | the application send e-mail messages. A sensible default using the - | transport layer security protocol should provide great security. - | - */ - - 'encryption' => env('MAIL_ENCRYPTION', 'tls'), - - /* - |-------------------------------------------------------------------------- - | SMTP Server Username - |-------------------------------------------------------------------------- - | - | If your SMTP server requires a username for authentication, you should - | set it here. This will get used to authenticate with your server on - | connection. You may also set the "password" value below this one. - | - */ - - 'username' => env('MAIL_USERNAME'), - 'password' => env('MAIL_PASSWORD'), - - - /* - |-------------------------------------------------------------------------- - | SMTP EHLO Domain - |-------------------------------------------------------------------------- - | - | Some SMTP servers require to present a known domain in order to - | allow sending through its relay. (ie: Google Workspace) - | This will use the MAIL_SMTP_EHLO env variable to avoid the 421 error - | if not present by authenticating the sender domain instead the host. - | - */ - 'local_domain' => env('MAIL_EHLO_DOMAIN'), - - /* - |-------------------------------------------------------------------------- - | Sendmail System Path - |-------------------------------------------------------------------------- - | - | When using the "sendmail" driver to send e-mails, we will need to know - | the path to where Sendmail lives on this server. A default path has - | been provided here, which will work well on most of your systems. - | - */ - - 'sendmail' => '/usr/sbin/sendmail -bs', - /* |-------------------------------------------------------------------------- | Markdown Mail Settings diff --git a/config/pixelfed.php b/config/pixelfed.php index fc7da598a..9e7898f88 100644 --- a/config/pixelfed.php +++ b/config/pixelfed.php @@ -23,7 +23,7 @@ return [ | This value is the version of your Pixelfed instance. | */ - 'version' => '0.11.9', + 'version' => '0.12.4', /* |-------------------------------------------------------------------------- @@ -195,7 +195,7 @@ return [ | Max User Limit |-------------------------------------------------------------------------- | - | Allow a maximum number of user accounts. Default: off + | Allow a maximum number of user accounts. Default: enabled w/ 1000 max users | */ 'max_users' => env('PF_MAX_USERS', 1000), diff --git a/config/security.php b/config/security.php index a8f92360d..929c05214 100644 --- a/config/security.php +++ b/config/security.php @@ -5,5 +5,18 @@ return [ 'verify_dns' => env('PF_SECURITY_URL_VERIFY_DNS', false), 'trusted_domains' => env('PF_SECURITY_URL_TRUSTED_DOMAINS', 'pixelfed.social,pixelfed.art,mastodon.social'), + ], + + 'forgot-email' => [ + 'enabled' => env('PF_AUTH_ALLOW_EMAIL_FORGOT', true), + + 'limits' => [ + 'max' => [ + 'hourly' => env('PF_AUTH_FORGOT_EMAIL_MAX_HOURLY', 50), + 'daily' => env('PF_AUTH_FORGOT_EMAIL_MAX_DAILY', 100), + 'weekly' => env('PF_AUTH_FORGOT_EMAIL_MAX_WEEKLY', 200), + 'monthly' => env('PF_AUTH_FORGOT_EMAIL_MAX_MONTHLY', 500), + ] + ] ] ]; diff --git a/config/services.php b/config/services.php index 58db77fe5..a1e56ac99 100644 --- a/config/services.php +++ b/config/services.php @@ -17,10 +17,12 @@ return [ 'mailgun' => [ 'domain' => env('MAILGUN_DOMAIN'), 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + 'scheme' => 'https', ], 'ses' => [ - 'key' => env('SES_KEY'), + 'key' => env('SES_KEY'), 'secret' => env('SES_SECRET'), 'region' => env('SES_REGION', 'us-east-1'), ], @@ -30,9 +32,16 @@ return [ ], 'stripe' => [ - 'model' => App\User::class, - 'key' => env('STRIPE_KEY'), + 'model' => App\User::class, + 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), ], + 'expo' => [ + 'access_token' => env('EXPO_ACCESS_TOKEN'), + ], + + 'resend' => [ + 'key' => env('RESEND_KEY'), + ], ]; diff --git a/config/session.php b/config/session.php index 1b692e3a4..d3e982bd4 100644 --- a/config/session.php +++ b/config/session.php @@ -70,7 +70,7 @@ return [ | */ - 'connection' => null, + 'connection' => env('SESSION_CONNECTION'), /* |-------------------------------------------------------------------------- @@ -96,7 +96,7 @@ return [ | */ - 'store' => null, + 'store' => env('SESSION_STORE'), /* |-------------------------------------------------------------------------- @@ -109,7 +109,7 @@ return [ | */ - 'lottery' => [2, 1000], + 'lottery' => [2, 100], /* |-------------------------------------------------------------------------- @@ -161,7 +161,7 @@ return [ | */ - 'secure' => true, + 'secure' => env('SESSION_SECURE_COOKIE', true), /* |-------------------------------------------------------------------------- @@ -183,12 +183,25 @@ return [ | | This option determines how your cookies behave when cross-site requests | take place, and can be used to mitigate CSRF attacks. By default, we - | do not enable this as other CSRF protection services are in place. + | will set this value to "lax" since this is a secure default value. | - | Supported: "lax", "strict" + | Supported: "lax", "strict", "none", null | */ - 'same_site' => null, + 'same_site' => env('SESSION_SAME_SITE_COOKIES', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => false, ]; diff --git a/config/webpush.php b/config/webpush.php new file mode 100644 index 000000000..bef3e6653 --- /dev/null +++ b/config/webpush.php @@ -0,0 +1,48 @@ + [ + 'subject' => env('VAPID_SUBJECT'), + 'public_key' => env('VAPID_PUBLIC_KEY'), + 'private_key' => env('VAPID_PRIVATE_KEY'), + 'pem_file' => env('VAPID_PEM_FILE'), + ], + + /** + * This is model that will be used to for push subscriptions. + */ + 'model' => \NotificationChannels\WebPush\PushSubscription::class, + + /** + * This is the name of the table that will be created by the migration and + * used by the PushSubscription model shipped with this package. + */ + 'table_name' => env('WEBPUSH_DB_TABLE', 'push_subscriptions'), + + /** + * This is the database connection that will be used by the migration and + * the PushSubscription model shipped with this package. + */ + 'database_connection' => env('WEBPUSH_DB_CONNECTION', env('DB_CONNECTION', 'mysql')), + + /** + * The Guzzle client options used by Minishlink\WebPush. + */ + 'client_options' => [], + + /** + * Google Cloud Messaging. + * + * @deprecated + */ + 'gcm' => [ + 'key' => env('GCM_KEY'), + 'sender_id' => env('GCM_SENDER_ID'), + ], + +]; diff --git a/contrib/docker-nginx.conf b/contrib/docker-nginx.conf deleted file mode 100644 index 9d0a199e6..000000000 --- a/contrib/docker-nginx.conf +++ /dev/null @@ -1,35 +0,0 @@ -upstream fe { - server 127.0.0.1:8080; -} - -server { - server_name real.domain; - listen [::]:443 ssl ipv6only=on; - listen 443 ssl; - ssl_certificate /etc/letsencrypt/live/real.domain/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/real.domain/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - - location / { - proxy_set_header Host $host; - 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 $scheme; - proxy_set_header X-Forwarded-Host $http_x_forwarded_host; - proxy_set_header X-Forwarded-Port $http_x_forwarded_port; - proxy_redirect off; - proxy_pass http://fe/; - } -} - -server { - if ($host = real.domain) { - return 301 https://$host$request_uri; - } - - listen 80; - listen [::]:80; - server_name real.domain; - return 404; -} \ No newline at end of file diff --git a/contrib/docker/Dockerfile.apache b/contrib/docker/Dockerfile.apache deleted file mode 100644 index 9c33aee17..000000000 --- a/contrib/docker/Dockerfile.apache +++ /dev/null @@ -1,100 +0,0 @@ -FROM php:8.1-apache-bullseye - -ENV COMPOSER_MEMORY_LIMIT=-1 -ARG DEBIAN_FRONTEND=noninteractive -WORKDIR /var/www/ - -# Get Composer binary -COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer - -# Install package dependencies -RUN apt-get update \ - && apt-get upgrade -y \ -# && apt-get install -y --no-install-recommends apt-utils \ - && apt-get install -y --no-install-recommends \ -## Standard - locales \ - locales-all \ - git \ - gosu \ - zip \ - unzip \ - libzip-dev \ - libcurl4-openssl-dev \ -## Image Optimization - optipng \ - pngquant \ - jpegoptim \ - gifsicle \ -## Image Processing - libjpeg62-turbo-dev \ - libpng-dev \ - libmagickwand-dev \ -# Required for GD - libxpm4 \ - libxpm-dev \ - libwebp6 \ - libwebp-dev \ -## Video Processing - ffmpeg \ -## Database -# libpq-dev \ -# libsqlite3-dev \ - mariadb-client \ -# Locales Update - && sed -i '/en_US/s/^#//g' /etc/locale.gen \ - && locale-gen \ - && update-locale \ -# Install PHP extensions - && docker-php-source extract \ -#PHP Imagemagick extensions - && pecl install imagick \ - && docker-php-ext-enable imagick \ -# PHP GD extensions - && docker-php-ext-configure gd \ - --with-freetype \ - --with-jpeg \ - --with-webp \ - --with-xpm \ - && docker-php-ext-install -j$(nproc) gd \ -#PHP Redis extensions - && pecl install redis \ - && docker-php-ext-enable redis \ -#PHP Database extensions - && docker-php-ext-install pdo_mysql \ -#pdo_pgsql pdo_sqlite \ -#PHP extensions (dependencies) - && docker-php-ext-configure intl \ - && docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl \ -#APACHE Bootstrap - && a2enmod rewrite remoteip \ - && {\ - echo RemoteIPHeader X-Real-IP ;\ - echo RemoteIPTrustedProxy 10.0.0.0/8 ;\ - echo RemoteIPTrustedProxy 172.16.0.0/12 ;\ - echo RemoteIPTrustedProxy 192.168.0.0/16 ;\ - echo SetEnvIf X-Forwarded-Proto "https" HTTPS=on ;\ - } > /etc/apache2/conf-available/remoteip.conf \ - && a2enconf remoteip \ -#Cleanup - && docker-php-source delete \ - && apt-get autoremove --purge -y \ - && apt-get clean \ - && rm -rf /var/cache/apt \ - && rm -rf /var/lib/apt/lists/ - -# Use the default production configuration -COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini" - -COPY . /var/www/ -# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 -RUN cp -r storage storage.skel \ - && composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader \ - && rm -rf html && ln -s public html \ - && chown -R www-data:www-data /var/www - -RUN php artisan horizon:publish - -VOLUME /var/www/storage /var/www/bootstrap - -CMD ["/var/www/contrib/docker/start.apache.sh"] diff --git a/contrib/docker/Dockerfile.fpm b/contrib/docker/Dockerfile.fpm deleted file mode 100644 index 0b8e5c113..000000000 --- a/contrib/docker/Dockerfile.fpm +++ /dev/null @@ -1,90 +0,0 @@ -FROM php:8.1-fpm-bullseye - -ENV COMPOSER_MEMORY_LIMIT=-1 -ARG DEBIAN_FRONTEND=noninteractive -WORKDIR /var/www/ - -# Get Composer binary -COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer - -# Install package dependencies -RUN apt-get update \ - && apt-get upgrade -y \ -# && apt-get install -y --no-install-recommends apt-utils \ - && apt-get install -y --no-install-recommends \ -## Standard - locales \ - locales-all \ - git \ - gosu \ - zip \ - unzip \ - libzip-dev \ - libcurl4-openssl-dev \ -## Image Optimization - optipng \ - pngquant \ - jpegoptim \ - gifsicle \ -## Image Processing - libjpeg62-turbo-dev \ - libpng-dev \ - libmagickwand-dev \ -# Required for GD - libxpm4 \ - libxpm-dev \ - libwebp6 \ - libwebp-dev \ -## Video Processing - ffmpeg \ -## Database -# libpq-dev \ -# libsqlite3-dev \ - mariadb-client \ -# Locales Update - && sed -i '/en_US/s/^#//g' /etc/locale.gen \ - && locale-gen \ - && update-locale \ -# Install PHP extensions - && docker-php-source extract \ -#PHP Imagemagick extensions - && pecl install imagick \ - && docker-php-ext-enable imagick \ -# PHP GD extensions - && docker-php-ext-configure gd \ - --with-freetype \ - --with-jpeg \ - --with-webp \ - --with-xpm \ - && docker-php-ext-install -j$(nproc) gd \ -#PHP Redis extensions - && pecl install redis \ - && docker-php-ext-enable redis \ -#PHP Database extensions - && docker-php-ext-install pdo_mysql \ -#pdo_pgsql pdo_sqlite \ -#PHP extensions (dependencies) - && docker-php-ext-configure intl \ - && docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl \ -#Cleanup - && docker-php-source delete \ - && apt-get autoremove --purge -y \ - && apt-get clean \ - && rm -rf /var/cache/apt \ - && rm -rf /var/lib/apt/lists/ - -# Use the default production configuration -COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini" - -COPY . /var/www/ -# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 -RUN cp -r storage storage.skel \ - && composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader \ - && rm -rf html && ln -s public html \ - && chown -R www-data:www-data /var/www - -RUN php artisan horizon:publish - -VOLUME /var/www/storage /var/www/bootstrap - -CMD ["/var/www/contrib/docker/start.fpm.sh"] diff --git a/contrib/docker/start.apache.sh b/contrib/docker/start.apache.sh deleted file mode 100755 index 4fb19e476..000000000 --- a/contrib/docker/start.apache.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# Create the storage tree if needed and fix permissions -cp -r storage.skel/* storage/ -chown -R www-data:www-data storage/ bootstrap/ - -# Refresh the environment -php artisan config:cache -php artisan storage:link -php artisan horizon:publish -php artisan route:cache -php artisan view:cache - -# Finally run Apache -apache2-foreground diff --git a/contrib/docker/start.fpm.sh b/contrib/docker/start.fpm.sh deleted file mode 100755 index 199489fc6..000000000 --- a/contrib/docker/start.fpm.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# Create the storage tree if needed and fix permissions -cp -r storage.skel/* storage/ -chown -R www-data:www-data storage/ bootstrap/ - -# Refresh the environment -php artisan config:cache -php artisan storage:link -php artisan horizon:publish -php artisan route:cache -php artisan view:cache - -# Finally run FPM -php-fpm diff --git a/contrib/nginx.conf b/contrib/nginx.conf deleted file mode 100644 index 0f86ea9e7..000000000 --- a/contrib/nginx.conf +++ /dev/null @@ -1,67 +0,0 @@ -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name pixelfed.example; # change this to your fqdn - root /home/pixelfed/public; # path to repo/public - - ssl_certificate /etc/nginx/ssl/server.crt; # generate your own - ssl_certificate_key /etc/nginx/ssl/server.key; # or use letsencrypt - - ssl_protocols TLSv1.2; - ssl_ciphers EECDH+AESGCM:EECDH+CHACHA20:EECDH+AES; - ssl_prefer_server_ciphers on; - - #add_header X-Frame-Options "SAMEORIGIN"; - add_header X-XSS-Protection "1; mode=block"; - add_header X-Content-Type-Options "nosniff"; - - index index.php; - - charset utf-8; - client_max_body_size 15M; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location = /favicon.ico { access_log off; log_not_found off; } - location = /robots.txt { access_log off; log_not_found off; } - - error_page 404 /index.php; - - location ~ \.php$ { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param QUERY_STRING $query_string; - fastcgi_param REQUEST_METHOD $request_method; - fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param CONTENT_LENGTH $content_length; - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - fastcgi_param REQUEST_URI $request_uri; - fastcgi_param DOCUMENT_URI $document_uri; - fastcgi_param DOCUMENT_ROOT $document_root; - fastcgi_param SERVER_PROTOCOL $server_protocol; - fastcgi_param GATEWAY_INTERFACE CGI/1.1; - fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; - fastcgi_param REMOTE_ADDR $remote_addr; - fastcgi_param REMOTE_PORT $remote_port; - fastcgi_param SERVER_ADDR $server_addr; - fastcgi_param SERVER_PORT $server_port; - fastcgi_param SERVER_NAME $server_name; - fastcgi_param HTTPS $https if_not_empty; - fastcgi_param REDIRECT_STATUS 200; - fastcgi_param HTTP_PROXY ""; - } - - location ~ /\.(?!well-known).* { - deny all; - } -} - -server { # Redirect http to https - server_name pixelfed.example; # change this to your fqdn - listen 80; - listen [::]:80; - return 301 https://$host$request_uri; -} diff --git a/database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php b/database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php new file mode 100644 index 000000000..7b93b406a --- /dev/null +++ b/database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php @@ -0,0 +1,31 @@ +string('id', 100)->primary(); + $table->unsignedBigInteger('user_id')->index(); + $table->unsignedBigInteger('client_id'); + $table->text('scopes')->nullable(); + $table->boolean('revoked'); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_auth_codes'); + } +}; diff --git a/database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php b/database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php new file mode 100644 index 000000000..598798eef --- /dev/null +++ b/database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php @@ -0,0 +1,33 @@ +string('id', 100)->primary(); + $table->unsignedBigInteger('user_id')->nullable()->index(); + $table->unsignedBigInteger('client_id'); + $table->string('name')->nullable(); + $table->text('scopes')->nullable(); + $table->boolean('revoked'); + $table->timestamps(); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_access_tokens'); + } +}; diff --git a/database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php b/database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php new file mode 100644 index 000000000..b007904ce --- /dev/null +++ b/database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php @@ -0,0 +1,29 @@ +string('id', 100)->primary(); + $table->string('access_token_id', 100)->index(); + $table->boolean('revoked'); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_refresh_tokens'); + } +}; diff --git a/database/migrations/2016_06_01_000004_create_oauth_clients_table.php b/database/migrations/2016_06_01_000004_create_oauth_clients_table.php new file mode 100644 index 000000000..776ccfab2 --- /dev/null +++ b/database/migrations/2016_06_01_000004_create_oauth_clients_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + $table->unsignedBigInteger('user_id')->nullable()->index(); + $table->string('name'); + $table->string('secret', 100)->nullable(); + $table->string('provider')->nullable(); + $table->text('redirect'); + $table->boolean('personal_access_client'); + $table->boolean('password_client'); + $table->boolean('revoked'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_clients'); + } +}; diff --git a/database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php b/database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php new file mode 100644 index 000000000..7c9d1e8f1 --- /dev/null +++ b/database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php @@ -0,0 +1,28 @@ +bigIncrements('id'); + $table->unsignedBigInteger('client_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_personal_access_clients'); + } +}; diff --git a/database/migrations/2018_08_08_100000_create_telescope_entries_table.php b/database/migrations/2018_08_08_100000_create_telescope_entries_table.php new file mode 100644 index 000000000..700a83f09 --- /dev/null +++ b/database/migrations/2018_08_08_100000_create_telescope_entries_table.php @@ -0,0 +1,70 @@ +getConnection()); + + $schema->create('telescope_entries', function (Blueprint $table) { + $table->bigIncrements('sequence'); + $table->uuid('uuid'); + $table->uuid('batch_id'); + $table->string('family_hash')->nullable(); + $table->boolean('should_display_on_index')->default(true); + $table->string('type', 20); + $table->longText('content'); + $table->dateTime('created_at')->nullable(); + + $table->unique('uuid'); + $table->index('batch_id'); + $table->index('family_hash'); + $table->index('created_at'); + $table->index(['type', 'should_display_on_index']); + }); + + $schema->create('telescope_entries_tags', function (Blueprint $table) { + $table->uuid('entry_uuid'); + $table->string('tag'); + + $table->primary(['entry_uuid', 'tag']); + $table->index('tag'); + + $table->foreign('entry_uuid') + ->references('uuid') + ->on('telescope_entries') + ->onDelete('cascade'); + }); + + $schema->create('telescope_monitoring', function (Blueprint $table) { + $table->string('tag')->primary(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $schema = Schema::connection($this->getConnection()); + + $schema->dropIfExists('telescope_entries_tags'); + $schema->dropIfExists('telescope_entries'); + $schema->dropIfExists('telescope_monitoring'); + } +}; diff --git a/database/migrations/2018_08_12_042648_update_status_table_change_caption_to_text.php b/database/migrations/2018_08_12_042648_update_status_table_change_caption_to_text.php index 34d67e8c4..28484b42c 100644 --- a/database/migrations/2018_08_12_042648_update_status_table_change_caption_to_text.php +++ b/database/migrations/2018_08_12_042648_update_status_table_change_caption_to_text.php @@ -5,13 +5,6 @@ use Illuminate\Support\Facades\Schema; class UpdateStatusTableChangeCaptionToText extends Migration { - public function __construct() - { - DB::getDoctrineSchemaManager() - ->getDatabasePlatform() - ->registerDoctrineTypeMapping('enum', 'string'); - } - /** * Run the migrations. * diff --git a/database/migrations/2018_09_30_051108_create_direct_messages_table.php b/database/migrations/2018_09_30_051108_create_direct_messages_table.php index 63b305f4d..97db01e3c 100644 --- a/database/migrations/2018_09_30_051108_create_direct_messages_table.php +++ b/database/migrations/2018_09_30_051108_create_direct_messages_table.php @@ -19,7 +19,7 @@ class CreateDirectMessagesTable extends Migration $table->bigInteger('from_id')->unsigned()->index(); $table->string('from_profile_ids')->nullable(); $table->boolean('group_message')->default(false); - $table->bigInteger('status_id')->unsigned()->integer(); + $table->bigInteger('status_id')->unsigned(); $table->unique(['to_id', 'from_id', 'status_id']); $table->timestamp('read_at')->nullable(); $table->timestamps(); diff --git a/database/migrations/2018_12_22_055940_add_account_status_to_profiles_table.php b/database/migrations/2018_12_22_055940_add_account_status_to_profiles_table.php index 04a88060e..097e86753 100644 --- a/database/migrations/2018_12_22_055940_add_account_status_to_profiles_table.php +++ b/database/migrations/2018_12_22_055940_add_account_status_to_profiles_table.php @@ -54,12 +54,14 @@ class AddAccountStatusToProfilesTable extends Migration $table->string('hub_url')->nullable(); }); - Schema::table('stories', function (Blueprint $table) { - $table->dropColumn('id'); - }); - Schema::table('stories', function (Blueprint $table) { - $table->bigIncrements('bigIncrements')->first(); - }); + if (Schema::hasTable('stories')) { + Schema::table('stories', function (Blueprint $table) { + $table->dropColumn('id'); + }); + Schema::table('stories', function (Blueprint $table) { + $table->bigIncrements('bigIncrements')->first(); + }); + } Schema::table('profiles', function (Blueprint $table) { $table->dropColumn('status'); diff --git a/database/migrations/2019_01_12_054413_stories.php b/database/migrations/2019_01_12_054413_stories.php index a61c447de..f58a8cf38 100644 --- a/database/migrations/2019_01_12_054413_stories.php +++ b/database/migrations/2019_01_12_054413_stories.php @@ -60,13 +60,7 @@ class Stories extends Migration { Schema::dropIfExists('story_items'); Schema::dropIfExists('story_views'); - - Schema::table('stories', function (Blueprint $table) { - $table->dropColumn(['title','preview_photo','local_only','is_live','broadcast_url','broadcast_key']); - }); - - Schema::table('story_reactions', function (Blueprint $table) { - $table->dropColumn('story_id'); - }); + Schema::dropIfExists('story_reactions'); + Schema::dropIfExists('stories'); } } diff --git a/database/migrations/2019_03_12_043935_add_snowflakeids_to_users_table.php b/database/migrations/2019_03_12_043935_add_snowflakeids_to_users_table.php index 3815090ab..8c5dfad93 100644 --- a/database/migrations/2019_03_12_043935_add_snowflakeids_to_users_table.php +++ b/database/migrations/2019_03_12_043935_add_snowflakeids_to_users_table.php @@ -6,11 +6,6 @@ use Illuminate\Database\Migrations\Migration; class AddSnowflakeidsToUsersTable extends Migration { - public function __construct() - { - DB::getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); - } - /** * Run the migrations. * @@ -19,8 +14,8 @@ class AddSnowflakeidsToUsersTable extends Migration public function up() { Schema::table('statuses', function (Blueprint $table) { - $table->dropPrimary('id'); $table->bigInteger('id')->unsigned()->primary()->change(); + $table->dropPrimary('id'); }); } diff --git a/database/migrations/2019_04_16_184644_add_layout_to_profiles_table.php b/database/migrations/2019_04_16_184644_add_layout_to_profiles_table.php index ed47eb17c..17328dea5 100644 --- a/database/migrations/2019_04_16_184644_add_layout_to_profiles_table.php +++ b/database/migrations/2019_04_16_184644_add_layout_to_profiles_table.php @@ -6,11 +6,6 @@ use Illuminate\Database\Migrations\Migration; class AddLayoutToProfilesTable extends Migration { - public function __construct() - { - DB::getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); - } - /** * Run the migrations. * diff --git a/database/migrations/2019_04_25_200411_add_snowflake_ids_to_collections_table.php b/database/migrations/2019_04_25_200411_add_snowflake_ids_to_collections_table.php index 10392de1f..a0e88ce7e 100644 --- a/database/migrations/2019_04_25_200411_add_snowflake_ids_to_collections_table.php +++ b/database/migrations/2019_04_25_200411_add_snowflake_ids_to_collections_table.php @@ -6,11 +6,6 @@ use Illuminate\Database\Migrations\Migration; class AddSnowflakeIdsToCollectionsTable extends Migration { - public function __construct() - { - DB::getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); - } - /** * Run the migrations. * @@ -19,13 +14,13 @@ class AddSnowflakeIdsToCollectionsTable extends Migration public function up() { Schema::table('collections', function (Blueprint $table) { - $table->dropPrimary('id'); $table->bigInteger('id')->unsigned()->primary()->change(); + $table->dropPrimary('id'); }); Schema::table('collection_items', function (Blueprint $table) { - $table->dropPrimary('id'); $table->bigInteger('id')->unsigned()->primary()->change(); + $table->dropPrimary('id'); }); } diff --git a/database/migrations/2019_08_12_074612_add_unique_to_statuses_table.php b/database/migrations/2019_08_12_074612_add_unique_to_statuses_table.php index 8d47e6d4f..933ce23af 100644 --- a/database/migrations/2019_08_12_074612_add_unique_to_statuses_table.php +++ b/database/migrations/2019_08_12_074612_add_unique_to_statuses_table.php @@ -6,11 +6,6 @@ use Illuminate\Database\Migrations\Migration; class AddUniqueToStatusesTable extends Migration { - public function __construct() - { - DB::getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); - } - /** * Run the migrations. * diff --git a/database/migrations/2019_09_09_032757_add_object_id_to_statuses_table.php b/database/migrations/2019_09_09_032757_add_object_id_to_statuses_table.php index 3cdf9e25a..1832cdeee 100644 --- a/database/migrations/2019_09_09_032757_add_object_id_to_statuses_table.php +++ b/database/migrations/2019_09_09_032757_add_object_id_to_statuses_table.php @@ -6,11 +6,6 @@ use Illuminate\Database\Migrations\Migration; class AddObjectIdToStatusesTable extends Migration { - public function __construct() - { - DB::getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); - } - /** * Run the migrations. * diff --git a/database/migrations/2019_12_10_023604_create_newsroom_table.php b/database/migrations/2019_12_10_023604_create_newsroom_table.php index 2651d5c4d..b463f5624 100644 --- a/database/migrations/2019_12_10_023604_create_newsroom_table.php +++ b/database/migrations/2019_12_10_023604_create_newsroom_table.php @@ -40,6 +40,6 @@ class CreateNewsroomTable extends Migration */ public function down() { - Schema::dropIfExists('site_news'); + Schema::dropIfExists('newsroom'); } } diff --git a/database/migrations/2019_12_25_042317_update_stories_table.php b/database/migrations/2019_12_25_042317_update_stories_table.php index da778225e..37f63c8ed 100644 --- a/database/migrations/2019_12_25_042317_update_stories_table.php +++ b/database/migrations/2019_12_25_042317_update_stories_table.php @@ -6,10 +6,6 @@ use Illuminate\Support\Facades\Schema; class UpdateStoriesTable extends Migration { - public function __construct() - { - DB::getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); - } /** * Run the migrations. * diff --git a/database/migrations/2021_01_14_034521_add_cache_locks_table.php b/database/migrations/2021_01_14_034521_add_cache_locks_table.php index 121c69a37..07889b490 100644 --- a/database/migrations/2021_01_14_034521_add_cache_locks_table.php +++ b/database/migrations/2021_01_14_034521_add_cache_locks_table.php @@ -27,6 +27,6 @@ class AddCacheLocksTable extends Migration */ public function down() { - Schema::dropTable('cache_locks'); + Schema::dropIfExists('cache_locks'); } } diff --git a/database/migrations/2021_07_23_062326_add_compose_settings_to_user_settings_table.php b/database/migrations/2021_07_23_062326_add_compose_settings_to_user_settings_table.php index 58837cab3..9cbb317c5 100644 --- a/database/migrations/2021_07_23_062326_add_compose_settings_to_user_settings_table.php +++ b/database/migrations/2021_07_23_062326_add_compose_settings_to_user_settings_table.php @@ -33,14 +33,25 @@ class AddComposeSettingsToUserSettingsTable extends Migration public function down() { Schema::table('user_settings', function (Blueprint $table) { - $table->dropColumn('compose_settings'); + if (Schema::hasColumn('user_settings', 'compose_settings')) { + $table->dropColumn('compose_settings'); + } }); Schema::table('media', function (Blueprint $table) { $table->string('caption')->change(); - $table->dropIndex('profile_id'); - $table->dropIndex('mime'); - $table->dropIndex('license'); + + $indexes = Schema::getIndexes('media'); + $indexesFound = collect($indexes)->map(function($i) { return $i['name']; })->toArray(); + if (in_array('media_profile_id_index', $indexesFound)) { + $table->dropIndex('media_profile_id_index'); + } + if (in_array('media_mime_index', $indexesFound)) { + $table->dropIndex('media_mime_index'); + } + if (in_array('media_license_index', $indexesFound)) { + $table->dropIndex('media_license_index'); + } }); } } diff --git a/database/migrations/2021_08_04_100435_create_group_roles_table.php b/database/migrations/2021_08_04_100435_create_group_roles_table.php new file mode 100644 index 000000000..c2b0d0ff4 --- /dev/null +++ b/database/migrations/2021_08_04_100435_create_group_roles_table.php @@ -0,0 +1,36 @@ +id(); + $table->bigInteger('group_id')->unsigned()->index(); + $table->string('name'); + $table->string('slug')->nullable(); + $table->text('abilities')->nullable(); + $table->unique(['group_id', 'slug']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_roles'); + } +} diff --git a/database/migrations/2021_08_16_100034_create_group_interactions_table.php b/database/migrations/2021_08_16_100034_create_group_interactions_table.php new file mode 100644 index 000000000..adc32d1d1 --- /dev/null +++ b/database/migrations/2021_08_16_100034_create_group_interactions_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->bigInteger('group_id')->unsigned()->index(); + $table->bigInteger('profile_id')->unsigned()->index(); + $table->string('type')->nullable()->index(); + $table->string('item_type')->nullable()->index(); + $table->string('item_id')->nullable()->index(); + $table->json('metadata')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_interactions'); + } +} diff --git a/database/migrations/2021_08_17_073839_create_group_reports_table.php b/database/migrations/2021_08_17_073839_create_group_reports_table.php new file mode 100644 index 000000000..93ed00d63 --- /dev/null +++ b/database/migrations/2021_08_17_073839_create_group_reports_table.php @@ -0,0 +1,39 @@ +id(); + $table->bigInteger('group_id')->unsigned()->index(); + $table->bigInteger('profile_id')->unsigned()->index(); + $table->string('type')->nullable()->index(); + $table->string('item_type')->nullable()->index(); + $table->string('item_id')->nullable()->index(); + $table->json('metadata')->nullable(); + $table->boolean('open')->default(true)->index(); + $table->unique(['group_id', 'profile_id', 'item_type', 'item_id']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_reports'); + } +} diff --git a/database/migrations/2021_08_23_062246_update_stories_table_fix_expires_at_column.php b/database/migrations/2021_08_23_062246_update_stories_table_fix_expires_at_column.php index 61ae60c01..26af256a7 100644 --- a/database/migrations/2021_08_23_062246_update_stories_table_fix_expires_at_column.php +++ b/database/migrations/2021_08_23_062246_update_stories_table_fix_expires_at_column.php @@ -14,12 +14,11 @@ class UpdateStoriesTableFixExpiresAtColumn extends Migration public function up() { Schema::table('stories', function (Blueprint $table) { - $sm = Schema::getConnection()->getDoctrineSchemaManager(); - $doctrineTable = $sm->listTableDetails('stories'); - - if($doctrineTable->hasIndex('stories_expires_at_index')) { - $table->dropIndex('stories_expires_at_index'); - } + $indexes = Schema::getIndexes('stories'); + $indexesFound = collect($indexes)->map(function($i) { return $i['name']; })->toArray(); + if (in_array('stories_expires_at_index', $indexesFound)) { + $table->dropIndex('stories_expires_at_index'); + } $table->timestamp('expires_at')->default(null)->index()->nullable()->change(); $table->boolean('can_reply')->default(true); $table->boolean('can_react')->default(true); @@ -37,12 +36,11 @@ class UpdateStoriesTableFixExpiresAtColumn extends Migration public function down() { Schema::table('stories', function (Blueprint $table) { - $sm = Schema::getConnection()->getDoctrineSchemaManager(); - $doctrineTable = $sm->listTableDetails('stories'); - - if($doctrineTable->hasIndex('stories_expires_at_index')) { - $table->dropIndex('stories_expires_at_index'); - } + $indexes = Schema::getIndexes('stories'); + $indexesFound = collect($indexes)->map(function($i) { return $i['name']; })->toArray(); + if (in_array('stories_expires_at_index', $indexesFound)) { + $table->dropIndex('stories_expires_at_index'); + } $table->timestamp('expires_at')->default(null)->index()->nullable()->change(); $table->dropColumn('can_reply'); $table->dropColumn('can_react'); diff --git a/database/migrations/2021_09_26_112423_create_group_blocks_table.php b/database/migrations/2021_09_26_112423_create_group_blocks_table.php new file mode 100644 index 000000000..320fcf985 --- /dev/null +++ b/database/migrations/2021_09_26_112423_create_group_blocks_table.php @@ -0,0 +1,40 @@ +bigIncrements('id'); + $table->bigInteger('group_id')->unsigned()->index(); + $table->bigInteger('admin_id')->unsigned()->nullable(); + $table->bigInteger('profile_id')->nullable()->unsigned()->index(); + $table->bigInteger('instance_id')->nullable()->unsigned()->index(); + $table->string('name')->nullable()->index(); + $table->string('reason')->nullable(); + $table->boolean('is_user')->index(); + $table->boolean('moderated')->default(false)->index(); + $table->unique(['group_id', 'profile_id', 'instance_id']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_blocks'); + } +} diff --git a/database/migrations/2021_09_29_023230_create_group_limits_table.php b/database/migrations/2021_09_29_023230_create_group_limits_table.php new file mode 100644 index 000000000..67ca7bec8 --- /dev/null +++ b/database/migrations/2021_09_29_023230_create_group_limits_table.php @@ -0,0 +1,36 @@ +id(); + $table->bigInteger('group_id')->unsigned()->index(); + $table->bigInteger('profile_id')->unsigned()->index(); + $table->json('limits')->nullable(); + $table->json('metadata')->nullable(); + $table->unique(['group_id', 'profile_id']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_limits'); + } +} diff --git a/database/migrations/2021_10_01_083917_create_group_categories_table.php b/database/migrations/2021_10_01_083917_create_group_categories_table.php new file mode 100644 index 000000000..481ddf5ef --- /dev/null +++ b/database/migrations/2021_10_01_083917_create_group_categories_table.php @@ -0,0 +1,102 @@ +id(); + $table->string('name')->unique()->index(); + $table->string('slug')->unique()->index(); + $table->boolean('active')->default(true)->index(); + $table->tinyInteger('order')->unsigned()->nullable(); + $table->json('metadata')->nullable(); + $table->timestamps(); + }); + + $default = [ + 'General', + 'Photography', + 'Fediverse', + 'CompSci & Programming', + 'Causes & Movements', + 'Humor', + 'Science & Tech', + 'Travel', + 'Buy & Sell', + 'Business', + 'Style', + 'Animals', + 'Sports & Fitness', + 'Education', + 'Arts', + 'Entertainment', + 'Faith & Spirituality', + 'Relationships & Identity', + 'Parenting', + 'Hobbies & Interests', + 'Food & Drink', + 'Vehicles & Commutes', + 'Civics & Community', + ]; + + for ($i=1; $i <= 23; $i++) { + $cat = new GroupCategory; + $cat->name = $default[$i - 1]; + $cat->slug = str_slug($cat->name); + $cat->active = true; + $cat->order = $i; + $cat->save(); + } + + Schema::table('groups', function (Blueprint $table) { + $table->unsignedInteger('category_id')->default(1)->index()->after('id'); + $table->unsignedInteger('member_count')->nullable(); + $table->boolean('recommended')->default(false)->index(); + $table->boolean('discoverable')->default(false)->index(); + $table->boolean('activitypub')->default(false); + $table->boolean('is_nsfw')->default(false); + $table->boolean('dms')->default(false); + $table->boolean('autospam')->default(false); + $table->boolean('verified')->default(false); + $table->timestamp('last_active_at')->nullable(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_categories'); + + Schema::table('groups', function (Blueprint $table) { + $table->dropColumn('category_id'); + $table->dropColumn('member_count'); + $table->dropColumn('recommended'); + $table->dropColumn('activitypub'); + $table->dropColumn('is_nsfw'); + $table->dropColumn('discoverable'); + $table->dropColumn('dms'); + $table->dropColumn('autospam'); + $table->dropColumn('verified'); + $table->dropColumn('last_active_at'); + $table->dropColumn('deleted_at'); + }); + } +} diff --git a/database/migrations/2021_10_09_004230_create_group_hashtags_table.php b/database/migrations/2021_10_09_004230_create_group_hashtags_table.php new file mode 100644 index 000000000..1d05dabb9 --- /dev/null +++ b/database/migrations/2021_10_09_004230_create_group_hashtags_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->string('name')->unique()->index(); + $table->string('formatted')->nullable(); + $table->boolean('recommended')->default(false); + $table->boolean('sensitive')->default(false); + $table->boolean('banned')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_hashtags'); + } +} diff --git a/database/migrations/2021_10_09_004436_create_group_post_hashtags_table.php b/database/migrations/2021_10_09_004436_create_group_post_hashtags_table.php new file mode 100644 index 000000000..08014e399 --- /dev/null +++ b/database/migrations/2021_10_09_004436_create_group_post_hashtags_table.php @@ -0,0 +1,41 @@ +bigIncrements('id'); + $table->bigInteger('hashtag_id')->unsigned()->index(); + $table->bigInteger('group_id')->unsigned()->index(); + $table->bigInteger('profile_id')->unsigned(); + $table->bigInteger('status_id')->unsigned()->nullable(); + $table->string('status_visibility')->nullable(); + $table->boolean('nsfw')->default(false); + $table->unique(['hashtag_id', 'group_id', 'profile_id', 'status_id'], 'group_post_hashtags_gda_unique'); + $table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade'); + $table->foreign('profile_id')->references('id')->on('profiles')->onDelete('cascade'); + $table->foreign('hashtag_id')->references('id')->on('group_hashtags')->onDelete('cascade'); + $table->foreign('status_id')->references('id')->on('group_posts')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_post_hashtags'); + } +} diff --git a/database/migrations/2021_10_13_002033_create_group_stores_table.php b/database/migrations/2021_10_13_002033_create_group_stores_table.php new file mode 100644 index 000000000..efdf0a966 --- /dev/null +++ b/database/migrations/2021_10_13_002033_create_group_stores_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->bigInteger('group_id')->unsigned()->nullable()->index(); + $table->string('store_key')->index(); + $table->json('store_value')->nullable(); + $table->json('metadata')->nullable(); + $table->unique(['group_id', 'store_key']); + $table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_stores'); + } +} diff --git a/database/migrations/2021_10_13_002041_create_group_events_table.php b/database/migrations/2021_10_13_002041_create_group_events_table.php new file mode 100644 index 000000000..166c35cf0 --- /dev/null +++ b/database/migrations/2021_10_13_002041_create_group_events_table.php @@ -0,0 +1,44 @@ +bigIncrements('id'); + $table->bigInteger('group_id')->unsigned()->nullable()->index(); + $table->bigInteger('profile_id')->unsigned()->nullable()->index(); + $table->string('name')->nullable(); + $table->string('type')->index(); + $table->json('tags')->nullable(); + $table->json('location')->nullable(); + $table->text('description')->nullable(); + $table->json('metadata')->nullable(); + $table->boolean('open')->default(false)->index(); + $table->boolean('comments_open')->default(false); + $table->boolean('show_guest_list')->default(false); + $table->timestamp('start_at')->nullable(); + $table->timestamp('end_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_events'); + } +} diff --git a/database/migrations/2021_10_13_002124_create_group_activity_graphs_table.php b/database/migrations/2021_10_13_002124_create_group_activity_graphs_table.php new file mode 100644 index 000000000..13fef7240 --- /dev/null +++ b/database/migrations/2021_10_13_002124_create_group_activity_graphs_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->bigInteger('instance_id')->nullable()->index(); + $table->bigInteger('actor_id')->nullable()->index(); + $table->string('verb')->nullable()->index(); + $table->string('id_url')->nullable()->unique()->index(); + $table->json('payload')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('group_activity_graphs'); + } +} diff --git a/database/migrations/2022_10_07_055133_remove_old_compound_index_from_statuses_table.php b/database/migrations/2022_10_07_055133_remove_old_compound_index_from_statuses_table.php index ae9b84bbc..e3f943dd8 100644 --- a/database/migrations/2022_10_07_055133_remove_old_compound_index_from_statuses_table.php +++ b/database/migrations/2022_10_07_055133_remove_old_compound_index_from_statuses_table.php @@ -14,8 +14,9 @@ class RemoveOldCompoundIndexFromStatusesTable extends Migration public function up() { Schema::table('statuses', function (Blueprint $table) { - $sc = Schema::getConnection()->getDoctrineSchemaManager(); - if(array_key_exists('statuses_in_reply_to_id_reblog_of_id_index', $sc->listTableIndexes('statuses'))) { + $indexes = Schema::getIndexes('statuses'); + $indexesFound = collect($indexes)->map(function($i) { return $i['name']; })->toArray(); + if (in_array('statuses_in_reply_to_id_reblog_of_id_index', $indexesFound)) { $table->dropIndex('statuses_in_reply_to_id_reblog_of_id_index'); } }); diff --git a/database/migrations/2022_11_30_123940_update_avatars_table_remove_cdn_url_unique_constraint.php b/database/migrations/2022_11_30_123940_update_avatars_table_remove_cdn_url_unique_constraint.php index 423ff1b85..a3767fec0 100644 --- a/database/migrations/2022_11_30_123940_update_avatars_table_remove_cdn_url_unique_constraint.php +++ b/database/migrations/2022_11_30_123940_update_avatars_table_remove_cdn_url_unique_constraint.php @@ -14,9 +14,9 @@ return new class extends Migration public function up() { Schema::table('avatars', function (Blueprint $table) { - $sm = Schema::getConnection()->getDoctrineSchemaManager(); - $indexesFound = $sm->listTableIndexes('avatars'); - if(array_key_exists("avatars_cdn_url_unique", $indexesFound)) { + $indexes = Schema::getIndexes('avatars'); + $indexesFound = collect($indexes)->map(function($i) { return $i['name']; })->toArray(); + if (in_array('avatars_cdn_url_unique', $indexesFound)) { $table->dropUnique('avatars_cdn_url_unique'); } }); diff --git a/database/migrations/2023_02_04_053028_fix_cloud_media_paths.php b/database/migrations/2023_02_04_053028_fix_cloud_media_paths.php index b45ad7f80..31b16de1e 100644 --- a/database/migrations/2023_02_04_053028_fix_cloud_media_paths.php +++ b/database/migrations/2023_02_04_053028_fix_cloud_media_paths.php @@ -19,7 +19,7 @@ return new class extends Migration public function up() { ini_set('memory_limit', '-1'); - if(config_cache('pixelfed.cloud_storage') == false) { + if((bool) config_cache('pixelfed.cloud_storage') == false) { return; } diff --git a/database/migrations/2023_11_13_062429_add_followers_count_index_to_profiles_table.php b/database/migrations/2023_11_13_062429_add_followers_count_index_to_profiles_table.php new file mode 100644 index 000000000..bcc97577c --- /dev/null +++ b/database/migrations/2023_11_13_062429_add_followers_count_index_to_profiles_table.php @@ -0,0 +1,34 @@ +index('followers_count', 'profiles_followers_count_index'); + $table->index('following_count', 'profiles_following_count_index'); + $table->index('status_count', 'profiles_status_count_index'); + $table->index('is_private', 'profiles_is_private_index'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('profiles', function (Blueprint $table) { + $table->dropIndex('profiles_followers_count_index'); + $table->dropIndex('profiles_following_count_index'); + $table->dropIndex('profiles_status_count_index'); + $table->dropIndex('profiles_is_private_index'); + }); + } +}; diff --git a/database/migrations/2023_11_16_124107_create_hashtag_related_table.php b/database/migrations/2023_11_16_124107_create_hashtag_related_table.php new file mode 100644 index 000000000..33d7494d8 --- /dev/null +++ b/database/migrations/2023_11_16_124107_create_hashtag_related_table.php @@ -0,0 +1,33 @@ +bigIncrements('id'); + $table->bigInteger('hashtag_id')->unsigned()->unique()->index(); + $table->json('related_tags')->nullable(); + $table->bigInteger('agg_score')->unsigned()->nullable()->index(); + $table->timestamp('last_calculated_at')->nullable()->index(); + $table->timestamp('last_moderated_at')->nullable()->index(); + $table->boolean('skip_refresh')->default(false)->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('hashtag_related'); + } +}; diff --git a/database/migrations/2023_11_26_082439_add_state_and_score_to_places_table.php b/database/migrations/2023_11_26_082439_add_state_and_score_to_places_table.php new file mode 100644 index 000000000..d7f95aa3f --- /dev/null +++ b/database/migrations/2023_11_26_082439_add_state_and_score_to_places_table.php @@ -0,0 +1,34 @@ +string('state')->nullable()->index()->after('name'); + $table->tinyInteger('score')->default(0)->index()->after('long'); + $table->unsignedBigInteger('cached_post_count')->nullable(); + $table->timestamp('last_checked_at')->nullable()->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('places', function (Blueprint $table) { + $table->dropColumn('state'); + $table->dropColumn('score'); + $table->dropColumn('cached_post_count'); + $table->dropColumn('last_checked_at'); + }); + } +}; diff --git a/database/migrations/2023_12_04_041631_create_push_subscriptions_table.php b/database/migrations/2023_12_04_041631_create_push_subscriptions_table.php new file mode 100644 index 000000000..550a98f6a --- /dev/null +++ b/database/migrations/2023_12_04_041631_create_push_subscriptions_table.php @@ -0,0 +1,36 @@ +create(config('webpush.table_name'), function (Blueprint $table) { + $table->bigIncrements('id'); + $table->morphs('subscribable'); + $table->string('endpoint', 500)->unique(); + $table->string('public_key')->nullable(); + $table->string('auth_token')->nullable(); + $table->string('content_encoding')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::connection(config('webpush.database_connection'))->dropIfExists(config('webpush.table_name')); + } +} diff --git a/database/migrations/2023_12_05_092152_add_active_deliver_to_instances_table.php b/database/migrations/2023_12_05_092152_add_active_deliver_to_instances_table.php new file mode 100644 index 000000000..d6e413768 --- /dev/null +++ b/database/migrations/2023_12_05_092152_add_active_deliver_to_instances_table.php @@ -0,0 +1,36 @@ +boolean('active_deliver')->nullable()->index()->after('domain'); + $table->boolean('valid_nodeinfo')->nullable(); + $table->timestamp('nodeinfo_last_fetched')->nullable(); + $table->boolean('delivery_timeout')->default(false); + $table->timestamp('delivery_next_after')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instances', function (Blueprint $table) { + $table->dropColumn('active_deliver'); + $table->dropColumn('valid_nodeinfo'); + $table->dropColumn('nodeinfo_last_fetched'); + $table->dropColumn('delivery_timeout'); + $table->dropColumn('delivery_next_after'); + }); + } +}; diff --git a/database/migrations/2023_12_08_074345_add_direct_object_urls_to_statuses_table.php b/database/migrations/2023_12_08_074345_add_direct_object_urls_to_statuses_table.php new file mode 100644 index 000000000..5211c658f --- /dev/null +++ b/database/migrations/2023_12_08_074345_add_direct_object_urls_to_statuses_table.php @@ -0,0 +1,33 @@ +whereNotNull('url')->whereNull('object_url')->lazyById(50, 'id') as $status) { + try { + $status->object_url = $status->url; + $status->uri = $status->url; + $status->save(); + } catch (Exception | UniqueConstraintViolationException $e) { + continue; + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + } +}; diff --git a/database/migrations/2023_12_13_060425_add_uploaded_to_s3_to_import_posts_table.php b/database/migrations/2023_12_13_060425_add_uploaded_to_s3_to_import_posts_table.php new file mode 100644 index 000000000..c7d5cdcbd --- /dev/null +++ b/database/migrations/2023_12_13_060425_add_uploaded_to_s3_to_import_posts_table.php @@ -0,0 +1,28 @@ +boolean('uploaded_to_s3')->default(false)->index()->after('skip_missing_media'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('import_posts', function (Blueprint $table) { + $table->dropColumn('uploaded_to_s3'); + }); + } +}; diff --git a/database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php b/database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php new file mode 100644 index 000000000..16f8f3fb2 --- /dev/null +++ b/database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php @@ -0,0 +1,29 @@ +id(); + $table->unsignedBigInteger('profile_id')->index(); + $table->string('domain')->index(); + $table->unique(['profile_id', 'domain'], 'user_domain_blocks_by_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_domain_blocks'); + } +}; diff --git a/database/migrations/2023_12_19_081928_create_job_batches_table.php b/database/migrations/2023_12_19_081928_create_job_batches_table.php new file mode 100644 index 000000000..50e38c20f --- /dev/null +++ b/database/migrations/2023_12_19_081928_create_job_batches_table.php @@ -0,0 +1,35 @@ +string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_batches'); + } +}; diff --git a/database/migrations/2023_12_21_103223_purge_deleted_status_hashtags.php b/database/migrations/2023_12_21_103223_purge_deleted_status_hashtags.php new file mode 100644 index 000000000..bf2acc34e --- /dev/null +++ b/database/migrations/2023_12_21_103223_purge_deleted_status_hashtags.php @@ -0,0 +1,25 @@ +lazyById(200)->each->deleteQuietly(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/database/migrations/2023_12_21_104103_create_default_domain_blocks_table.php b/database/migrations/2023_12_21_104103_create_default_domain_blocks_table.php new file mode 100644 index 000000000..72e61bda5 --- /dev/null +++ b/database/migrations/2023_12_21_104103_create_default_domain_blocks_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('domain')->unique()->index(); + $table->text('note')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('default_domain_blocks'); + } +}; diff --git a/database/migrations/2023_12_27_081801_create_user_roles_table.php b/database/migrations/2023_12_27_081801_create_user_roles_table.php new file mode 100644 index 000000000..59b8ab390 --- /dev/null +++ b/database/migrations/2023_12_27_081801_create_user_roles_table.php @@ -0,0 +1,31 @@ +id(); + $table->unsignedBigInteger('profile_id')->unique()->index(); + $table->unsignedInteger('user_id')->unique()->index(); + $table->json('roles')->nullable(); + $table->json('meta')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_roles'); + } +}; diff --git a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php new file mode 100644 index 000000000..f32fe599c --- /dev/null +++ b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php @@ -0,0 +1,40 @@ +boolean('has_roles')->default(false); + $table->unsignedInteger('parent_id')->nullable(); + $table->tinyInteger('role_id')->unsigned()->nullable()->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + if (Schema::hasColumn('users', 'has_roles')) { + $table->dropColumn('has_roles'); + } + + if (Schema::hasColumn('users', 'role_id')) { + $table->dropColumn('role_id'); + } + + if (Schema::hasColumn('users', 'parent_id')) { + $table->dropColumn('parent_id'); + } + }); + } +}; diff --git a/database/migrations/2024_01_09_052419_create_parental_controls_table.php b/database/migrations/2024_01_09_052419_create_parental_controls_table.php new file mode 100644 index 000000000..9974c6241 --- /dev/null +++ b/database/migrations/2024_01_09_052419_create_parental_controls_table.php @@ -0,0 +1,53 @@ +id(); + $table->unsignedInteger('parent_id')->index(); + $table->unsignedInteger('child_id')->unique()->index()->nullable(); + $table->string('email')->unique()->nullable(); + $table->string('verify_code')->nullable(); + $table->timestamp('email_sent_at')->nullable(); + $table->timestamp('email_verified_at')->nullable(); + $table->json('permissions')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + + Schema::table('user_roles', function (Blueprint $table) { + $indexes = Schema::getIndexes('user_roles'); + $indexesFound = collect($indexes)->map(function($i) { return $i['name']; })->toArray(); + if (in_array('user_roles_profile_id_unique', $indexesFound)) { + $table->dropUnique('user_roles_profile_id_unique'); + } + $table->unsignedBigInteger('profile_id')->unique()->nullable()->index()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('parental_controls'); + + Schema::table('user_roles', function (Blueprint $table) { + $indexes = Schema::getIndexes('user_roles'); + $indexesFound = collect($indexes)->map(function($i) { return $i['name']; })->toArray(); + if (in_array('user_roles_profile_id_unique', $indexesFound)) { + $table->dropUnique('user_roles_profile_id_unique'); + } + $table->unsignedBigInteger('profile_id')->unique()->index()->change(); + }); + } +}; diff --git a/database/migrations/2024_01_16_073327_create_curated_registers_table.php b/database/migrations/2024_01_16_073327_create_curated_registers_table.php new file mode 100644 index 000000000..8f665cdc3 --- /dev/null +++ b/database/migrations/2024_01_16_073327_create_curated_registers_table.php @@ -0,0 +1,44 @@ +id(); + $table->string('email')->unique()->nullable()->index(); + $table->string('username')->unique()->nullable()->index(); + $table->string('password')->nullable(); + $table->string('ip_address')->nullable(); + $table->string('verify_code')->nullable(); + $table->text('reason_to_join')->nullable(); + $table->unsignedBigInteger('invited_by')->nullable()->index(); + $table->boolean('is_approved')->default(0)->index(); + $table->boolean('is_rejected')->default(0)->index(); + $table->boolean('is_awaiting_more_info')->default(0)->index(); + $table->boolean('is_closed')->default(0)->index(); + $table->json('autofollow_account_ids')->nullable(); + $table->json('admin_notes')->nullable(); + $table->unsignedInteger('approved_by_admin_id')->nullable(); + $table->timestamp('email_verified_at')->nullable(); + $table->timestamp('admin_notified_at')->nullable(); + $table->timestamp('action_taken_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('curated_registers'); + } +}; diff --git a/database/migrations/2024_01_20_091352_create_curated_register_activities_table.php b/database/migrations/2024_01_20_091352_create_curated_register_activities_table.php new file mode 100644 index 000000000..410804750 --- /dev/null +++ b/database/migrations/2024_01_20_091352_create_curated_register_activities_table.php @@ -0,0 +1,42 @@ +id(); + $table->unsignedInteger('register_id')->nullable()->index(); + $table->unsignedInteger('admin_id')->nullable(); + $table->unsignedInteger('reply_to_id')->nullable()->index(); + $table->string('secret_code')->nullable(); + $table->string('type')->nullable()->index(); + $table->string('title')->nullable(); + $table->string('link')->nullable(); + $table->text('message')->nullable(); + $table->json('metadata')->nullable(); + $table->boolean('from_admin')->default(false)->index(); + $table->boolean('from_user')->default(false)->index(); + $table->boolean('admin_only_view')->default(true); + $table->boolean('action_required')->default(false); + $table->timestamp('admin_notified_at')->nullable(); + $table->timestamp('action_taken_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('curated_register_activities'); + } +}; diff --git a/database/migrations/2024_01_22_090048_create_user_email_forgots_table.php b/database/migrations/2024_01_22_090048_create_user_email_forgots_table.php new file mode 100644 index 000000000..845b63934 --- /dev/null +++ b/database/migrations/2024_01_22_090048_create_user_email_forgots_table.php @@ -0,0 +1,32 @@ +id(); + $table->unsignedInteger('user_id')->index(); + $table->string('ip_address')->nullable(); + $table->string('user_agent')->nullable(); + $table->string('referrer')->nullable(); + $table->timestamp('email_sent_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_email_forgots'); + } +}; diff --git a/database/migrations/2024_02_24_093824_add_has_responded_to_curated_registers_table.php b/database/migrations/2024_02_24_093824_add_has_responded_to_curated_registers_table.php new file mode 100644 index 000000000..9453a73d7 --- /dev/null +++ b/database/migrations/2024_02_24_093824_add_has_responded_to_curated_registers_table.php @@ -0,0 +1,36 @@ +boolean('user_has_responded')->default(false)->index()->after('is_awaiting_more_info'); + }); + + CuratedRegisterActivity::whereFromUser(true)->get()->each(function($cra) { + $cr = CuratedRegister::find($cra->register_id); + $cr->user_has_responded = true; + $cr->save(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('curated_registers', function (Blueprint $table) { + $table->dropColumn('user_has_responded'); + }); + } +}; diff --git a/database/migrations/2024_02_24_105641_create_curated_register_templates_table.php b/database/migrations/2024_02_24_105641_create_curated_register_templates_table.php new file mode 100644 index 000000000..4829fefb2 --- /dev/null +++ b/database/migrations/2024_02_24_105641_create_curated_register_templates_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name')->nullable(); + $table->text('description')->nullable(); + $table->text('content')->nullable(); + $table->boolean('is_active')->default(false)->index(); + $table->tinyInteger('order')->default(10)->unsigned()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('curated_register_templates'); + } +}; diff --git a/database/migrations/2024_03_02_094235_create_profile_migrations_table.php b/database/migrations/2024_03_02_094235_create_profile_migrations_table.php new file mode 100644 index 000000000..2eafaa43b --- /dev/null +++ b/database/migrations/2024_03_02_094235_create_profile_migrations_table.php @@ -0,0 +1,31 @@ +id(); + $table->unsignedBigInteger('profile_id'); + $table->string('acct')->nullable(); + $table->unsignedBigInteger('followers_count')->default(0); + $table->unsignedBigInteger('target_profile_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('profile_migrations'); + } +}; diff --git a/database/migrations/2024_03_08_122947_add_shared_inbox_attribute_to_instances_table.php b/database/migrations/2024_03_08_122947_add_shared_inbox_attribute_to_instances_table.php new file mode 100644 index 000000000..a3d69f271 --- /dev/null +++ b/database/migrations/2024_03_08_122947_add_shared_inbox_attribute_to_instances_table.php @@ -0,0 +1,28 @@ +string('shared_inbox')->nullable()->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instances', function (Blueprint $table) { + $table->dropColumn('shared_inbox'); + }); + } +}; diff --git a/database/migrations/2024_03_08_123356_add_shared_inboxes_to_instances_table.php b/database/migrations/2024_03_08_123356_add_shared_inboxes_to_instances_table.php new file mode 100644 index 000000000..ff7eebc69 --- /dev/null +++ b/database/migrations/2024_03_08_123356_add_shared_inboxes_to_instances_table.php @@ -0,0 +1,32 @@ +domain)->whereNotNull('sharedInbox')->first(); + if($si && $si->sharedInbox) { + $instance->shared_inbox = $si->sharedInbox; + $instance->save(); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + + } +}; diff --git a/database/migrations/2024_05_20_062706_update_group_posts_table.php b/database/migrations/2024_05_20_062706_update_group_posts_table.php new file mode 100644 index 000000000..828727395 --- /dev/null +++ b/database/migrations/2024_05_20_062706_update_group_posts_table.php @@ -0,0 +1,52 @@ +dropUnique(['status_id']); + } + $table->dropColumn('status_id'); + $table->dropColumn('reply_child_id'); + $table->dropColumn('in_reply_to_id'); + $table->dropColumn('reblog_of_id'); + $table->text('caption')->nullable(); + $table->string('visibility')->nullable(); + $table->boolean('is_nsfw')->default(false); + $table->unsignedInteger('likes_count')->default(0); + $table->text('cw_summary')->nullable(); + $table->json('media_ids')->nullable(); + $table->boolean('comments_disabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('group_posts', function (Blueprint $table) { + $table->bigInteger('status_id')->unsigned()->unique()->nullable(); + $table->bigInteger('reply_child_id')->unsigned()->nullable(); + $table->bigInteger('in_reply_to_id')->unsigned()->nullable(); + $table->bigInteger('reblog_of_id')->unsigned()->nullable(); + $table->dropColumn('caption'); + $table->dropColumn('is_nsfw'); + $table->dropColumn('visibility'); + $table->dropColumn('likes_count'); + $table->dropColumn('cw_summary'); + $table->dropColumn('media_ids'); + $table->dropColumn('comments_disabled'); + }); + } +}; diff --git a/database/migrations/2024_05_20_063638_create_group_comments_table.php b/database/migrations/2024_05_20_063638_create_group_comments_table.php new file mode 100644 index 000000000..ad49f58c8 --- /dev/null +++ b/database/migrations/2024_05_20_063638_create_group_comments_table.php @@ -0,0 +1,43 @@ +bigIncrements('id'); + $table->unsignedBigInteger('group_id')->index(); + $table->unsignedBigInteger('profile_id')->nullable(); + $table->unsignedBigInteger('status_id')->nullable()->index(); + $table->unsignedBigInteger('in_reply_to_id')->nullable()->index(); + $table->string('remote_url')->nullable()->unique()->index(); + $table->text('caption')->nullable(); + $table->boolean('is_nsfw')->default(false); + $table->string('visibility')->nullable(); + $table->unsignedInteger('likes_count')->default(0); + $table->unsignedInteger('replies_count')->default(0); + $table->text('cw_summary')->nullable(); + $table->json('media_ids')->nullable(); + $table->string('status')->nullable(); + $table->string('type')->default('text')->nullable(); + $table->boolean('local')->default(false); + $table->json('metadata')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('group_comments'); + } +}; diff --git a/database/migrations/2024_05_20_073054_create_group_likes_table.php b/database/migrations/2024_05_20_073054_create_group_likes_table.php new file mode 100644 index 000000000..162ef7458 --- /dev/null +++ b/database/migrations/2024_05_20_073054_create_group_likes_table.php @@ -0,0 +1,33 @@ +bigIncrements('id'); + $table->unsignedBigInteger('group_id'); + $table->unsignedBigInteger('profile_id')->index(); + $table->unsignedBigInteger('status_id')->nullable(); + $table->unsignedBigInteger('comment_id')->nullable(); + $table->boolean('local')->default(true); + $table->unique(['group_id', 'profile_id', 'status_id', 'comment_id'], 'group_likes_gpsc_unique'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('group_likes'); + } +}; diff --git a/database/migrations/2024_05_20_083159_create_group_media_table.php b/database/migrations/2024_05_20_083159_create_group_media_table.php new file mode 100644 index 000000000..732856097 --- /dev/null +++ b/database/migrations/2024_05_20_083159_create_group_media_table.php @@ -0,0 +1,50 @@ +bigIncrements('id'); + $table->unsignedBigInteger('group_id'); + $table->unsignedBigInteger('profile_id'); + $table->unsignedBigInteger('status_id')->nullable()->index(); + $table->string('media_path')->unique(); + $table->text('thumbnail_url')->nullable(); + $table->text('cdn_url')->nullable(); + $table->text('url')->nullable(); + $table->string('mime')->nullable(); + $table->unsignedInteger('size')->nullable(); + $table->text('cw_summary')->nullable(); + $table->string('license')->nullable(); + $table->string('blurhash')->nullable(); + $table->tinyInteger('order')->unsigned()->default(1); + $table->unsignedInteger('width')->nullable(); + $table->unsignedInteger('height')->nullable(); + $table->boolean('local_user')->default(true); + $table->boolean('is_cached')->default(false); + $table->boolean('is_comment')->default(false)->index(); + $table->json('metadata')->nullable(); + $table->string('version')->default(1); + $table->boolean('skip_optimize')->default(false); + $table->timestamp('processed_at')->nullable(); + $table->timestamp('thumbnail_generated')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('group_media'); + } +}; diff --git a/database/migrations/2024_05_31_090555_update_instances_table_add_index_to_nodeinfo_last_fetched_at.php b/database/migrations/2024_05_31_090555_update_instances_table_add_index_to_nodeinfo_last_fetched_at.php new file mode 100644 index 000000000..a5eb3b921 --- /dev/null +++ b/database/migrations/2024_05_31_090555_update_instances_table_add_index_to_nodeinfo_last_fetched_at.php @@ -0,0 +1,36 @@ +map(function($i) { return $i['name']; })->toArray(); + if (!in_array('instances_nodeinfo_last_fetched_index', $indexesFound)) { + $table->index('nodeinfo_last_fetched'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instances', function (Blueprint $table) { + $indexes = Schema::getIndexes('instances'); + $indexesFound = collect($indexes)->map(function($i) { return $i['name']; })->toArray(); + if (in_array('instances_nodeinfo_last_fetched_index', $indexesFound)) { + $table->dropIndex('instances_nodeinfo_last_fetched_index'); + } + }); + } +}; diff --git a/database/migrations/2024_06_03_232204_add_url_index_to_statuses_table.php b/database/migrations/2024_06_03_232204_add_url_index_to_statuses_table.php new file mode 100644 index 000000000..98ce0d5e9 --- /dev/null +++ b/database/migrations/2024_06_03_232204_add_url_index_to_statuses_table.php @@ -0,0 +1,36 @@ +map(function($i) { return $i['name']; })->toArray(); + if (!in_array('statuses_url_index', $indexesFound)) { + $table->index('url'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('statuses', function (Blueprint $table) { + $indexes = Schema::getIndexes('statuses'); + $indexesFound = collect($indexes)->map(function($i) { return $i['name']; })->toArray(); + if (in_array('statuses_url_index', $indexesFound)) { + $table->dropIndex('statuses_url_index'); + } + }); + } +}; diff --git a/database/migrations/2024_06_19_084835_add_total_local_posts_to_config_cache.php b/database/migrations/2024_06_19_084835_add_total_local_posts_to_config_cache.php new file mode 100644 index 000000000..35f00f60a --- /dev/null +++ b/database/migrations/2024_06_19_084835_add_total_local_posts_to_config_cache.php @@ -0,0 +1,34 @@ +whereNull(['url', 'deleted_at'])->count(); + $res = [ + 'count' => $count + ]; + Storage::put('total_local_posts.json', json_encode($res, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)); + ConfigCacheService::put('instance.stats.total_local_posts', $res['count']); + Cache::forget('api:nodeinfo'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/database/migrations/2024_07_22_065800_add_expo_token_to_users_table.php b/database/migrations/2024_07_22_065800_add_expo_token_to_users_table.php new file mode 100644 index 000000000..4f7c4688d --- /dev/null +++ b/database/migrations/2024_07_22_065800_add_expo_token_to_users_table.php @@ -0,0 +1,36 @@ +string('expo_token')->nullable(); + $table->boolean('notify_like')->default(true); + $table->boolean('notify_follow')->default(true); + $table->boolean('notify_mention')->default(true); + $table->boolean('notify_comment')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('expo_token'); + $table->dropColumn('notify_like'); + $table->dropColumn('notify_follow'); + $table->dropColumn('notify_mention'); + $table->dropColumn('notify_comment'); + }); + } +}; diff --git a/database/migrations/2024_07_29_081002_add_storage_used_to_users_table.php b/database/migrations/2024_07_29_081002_add_storage_used_to_users_table.php new file mode 100644 index 000000000..d794b945e --- /dev/null +++ b/database/migrations/2024_07_29_081002_add_storage_used_to_users_table.php @@ -0,0 +1,30 @@ +unsignedBigInteger('storage_used')->default(0); + $table->timestamp('storage_used_updated_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('storage_used'); + $table->dropColumn('storage_used_updated_at'); + }); + } +}; diff --git a/database/migrations/2024_09_18_093322_add_notify_shares_to_users_table.php b/database/migrations/2024_09_18_093322_add_notify_shares_to_users_table.php new file mode 100644 index 000000000..54225d46a --- /dev/null +++ b/database/migrations/2024_09_18_093322_add_notify_shares_to_users_table.php @@ -0,0 +1,28 @@ +boolean('notify_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('notify_enabled'); + }); + } +}; diff --git a/database/migrations/2024_10_06_035032_modify_caption_field_in_media_table.php b/database/migrations/2024_10_06_035032_modify_caption_field_in_media_table.php new file mode 100644 index 000000000..9468e5344 --- /dev/null +++ b/database/migrations/2024_10_06_035032_modify_caption_field_in_media_table.php @@ -0,0 +1,28 @@ +text('caption')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('media', function (Blueprint $table) { + $table->text('caption')->nullable(false)->change(); + }); + } +}; diff --git a/database/migrations/2024_10_15_044935_create_moderated_profiles_table.php b/database/migrations/2024_10_15_044935_create_moderated_profiles_table.php new file mode 100644 index 000000000..2aa9580a5 --- /dev/null +++ b/database/migrations/2024_10_15_044935_create_moderated_profiles_table.php @@ -0,0 +1,56 @@ +id(); + $table->string('profile_url')->unique()->nullable()->index(); + $table->unsignedBigInteger('profile_id')->unique()->nullable(); + $table->string('domain')->nullable(); + $table->text('note')->nullable(); + $table->boolean('is_banned')->default(false); + $table->boolean('is_nsfw')->default(false); + $table->boolean('is_unlisted')->default(false); + $table->boolean('is_noautolink')->default(false); + $table->boolean('is_nodms')->default(false); + $table->boolean('is_notrending')->default(false); + $table->timestamps(); + }); + + $logs = ModLog::whereObjectType('App\Profile::class')->whereAction('admin.user.delete')->get(); + + foreach($logs as $log) { + $profile = Profile::withTrashed()->find($log->object_id); + if(!$profile || $profile->private_key) { + continue; + } + ModeratedProfile::updateOrCreate([ + 'profile_url' => $profile->remote_url, + 'profile_id' => $profile->id, + ], [ + 'is_banned' => true, + 'domain' => $profile->domain, + ]); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('moderated_profiles'); + } +}; diff --git a/docker-compose.migrate.yml b/docker-compose.migrate.yml new file mode 100644 index 000000000..d0040b051 --- /dev/null +++ b/docker-compose.migrate.yml @@ -0,0 +1,40 @@ +--- +services: + migrate: + image: "servercontainers/rsync" + entrypoint: "" + working_dir: /migrate + command: 'bash -c "exit 1"' + restart: never + volumes: + ################################ + # Storage volume + ################################ + # OLD + - "app-storage:/migrate/app-storage/old" + # NEW + - "${DOCKER_APP_HOST_STORAGE_PATH}:/migrate/app-storage/new" + + ################################ + # MySQL/DB volume + ################################ + # OLD + - "db-data:/migrate/db-data/old" + # NEW + - "${DOCKER_DB_HOST_DATA_PATH}:/migrate/db-data/new" + + ################################ + # Redis volume + ################################ + # OLD + - "redis-data:/migrate/redis-data/old" + # NEW + - "${DOCKER_REDIS_HOST_DATA_PATH}:/migrate/redis-data/new" + +# Volumes from the old [docker-compose.yml] file +# https://github.com/pixelfed/pixelfed/blob/b1ff44ca2f75c088a11576fb03b5bad2fbed4d5c/docker-compose.yml#L72-L76 +volumes: + db-data: + redis-data: + app-storage: + app-bootstrap: diff --git a/docker-compose.yml b/docker-compose.yml index 6fca2eeb3..2a805eb7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,82 +1,217 @@ --- -version: '3' - -# In order to set configuration, please use a .env file in -# your compose project directory (the same directory as your -# docker-compose.yml), and set database options, application -# name, key, and other settings there. -# A list of available settings is available in .env.example -# -# The services should scale properly across a swarm cluster -# if the volumes are properly shared between cluster members. +############################################################### +# Please see docker/README.md for usage information +############################################################### services: -## App and Worker - app: - # Comment to use dockerhub image - image: pixelfed/pixelfed:latest + # HTTP/HTTPS proxy + # + # Sits in front of the *real* webserver and manages SSL and (optionally) + # load-balancing between multiple web servers + # + # You can disable this service by setting [DOCKER_PROXY_PROFILE="disabled"] + # in your [.env] file - the setting is near the bottom of the file. + # + # This also disables the [proxy-acme] service, if this is not desired, change the + # [DOCKER_PROXY_ACME_PROFILE] setting to an empty string [""] + # + # See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs + proxy: + image: nginxproxy/nginx-proxy:1.4 + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy" restart: unless-stopped - env_file: - - .env.docker + profiles: + - ${DOCKER_PROXY_PROFILE:-} + environment: + DOCKER_SERVICE_NAME: "proxy" volumes: - - app-storage:/var/www/storage - - app-bootstrap:/var/www/bootstrap - - "./.env.docker:/var/www/.env" - networks: - - external - - internal + - "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/tmp/docker.sock:ro" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/vhost.d:/etc/nginx/vhost.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_ALL_HOST_DATA_ROOT_PATH}/proxy/html:/usr/share/nginx/html" ports: - - "8080:80" + - "${DOCKER_PROXY_HOST_PORT_HTTP}:80" + - "${DOCKER_PROXY_HOST_PORT_HTTPS}:443" + healthcheck: + test: "curl --fail https://${APP_DOMAIN}/api/service/health-check" + interval: "${DOCKER_PROXY_HEALTHCHECK_INTERVAL}" + retries: 2 + timeout: 5s + + # Proxy companion for managing letsencrypt SSL certificates + # + # You can disable this service by setting [DOCKER_PROXY_ACME_PROFILE="disabled"] + # in your [.env] file - the setting is near the bottom of the file. + # + # See: https://github.com/nginx-proxy/acme-companion/tree/main/docs + proxy-acme: + image: nginxproxy/acme-companion + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy-acme" + restart: unless-stopped + profiles: + - ${DOCKER_PROXY_ACME_PROFILE:-} + environment: + DEBUG: 0 + DEFAULT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}" + NGINX_PROXY_CONTAINER: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy" + depends_on: + - proxy + volumes: + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy-acme:/etc/acme.sh" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/vhost.d:/etc/nginx/vhost.d" + - "${DOCKER_ALL_HOST_DATA_ROOT_PATH}/proxy/html:/usr/share/nginx/html" + - "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock:ro" + + web: + image: "${DOCKER_APP_IMAGE}:${DOCKER_APP_TAG}" + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-web" + restart: unless-stopped + profiles: + - ${DOCKER_WEB_PROFILE:-} + build: + target: ${DOCKER_APP_RUNTIME}-runtime + cache_from: + - "type=registry,ref=${DOCKER_APP_IMAGE}-cache:${DOCKER_APP_TAG}" + args: + APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" + BUILD_FRONTEND: "${DOCKER_APP_BUILD_FRONTEND:-0}" + PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}" + PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}" + PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}" + PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" + PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}" + environment: + # Used by Pixelfed Docker init script + DOCKER_SERVICE_NAME: "web" + DOCKER_APP_ENTRYPOINT_DEBUG: ${DOCKER_APP_ENTRYPOINT_DEBUG:-0} + ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-} + # Used by [proxy] service + LETSENCRYPT_HOST: "${DOCKER_PROXY_LETSENCRYPT_HOST:?error}" + LETSENCRYPT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}" + LETSENCRYPT_TEST: "${DOCKER_PROXY_LETSENCRYPT_TEST:-}" + VIRTUAL_HOST: "${APP_DOMAIN}" + VIRTUAL_PORT: "80" + volumes: + - "./.env:/var/www/.env" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/shared/proxy/conf.d" + - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" + - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" + - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" + labels: + com.github.nginx-proxy.nginx-proxy.keepalive: 30 + com.github.nginx-proxy.nginx-proxy.http2.enable: true + com.github.nginx-proxy.nginx-proxy.http3.enable: true + ports: + - "${DOCKER_WEB_PORT_EXTERNAL_HTTP}:80" depends_on: - db - redis + healthcheck: + test: 'curl --header "Host: ${APP_DOMAIN}" --fail http://localhost/api/service/health-check' + interval: "${DOCKER_WEB_HEALTHCHECK_INTERVAL}" + retries: 2 + timeout: 5s worker: - image: pixelfed/pixelfed:latest - restart: unless-stopped - env_file: - - .env.docker - volumes: - - app-storage:/var/www/storage - - app-bootstrap:/var/www/bootstrap - networks: - - external - - internal + image: "${DOCKER_APP_IMAGE}:${DOCKER_APP_TAG}" + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-worker" command: gosu www-data php artisan horizon + restart: unless-stopped + stop_signal: SIGTERM + profiles: + - ${DOCKER_WORKER_PROFILE:-} + build: + target: ${DOCKER_APP_RUNTIME}-runtime + cache_from: + - "type=registry,ref=${DOCKER_APP_IMAGE}-cache:${DOCKER_APP_TAG}" + args: + APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" + BUILD_FRONTEND: "${DOCKER_APP_BUILD_FRONTEND:-0}" + PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}" + PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}" + PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}" + PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" + PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}" + environment: + # Used by Pixelfed Docker init script + DOCKER_SERVICE_NAME: "worker" + DOCKER_APP_ENTRYPOINT_DEBUG: ${DOCKER_APP_ENTRYPOINT_DEBUG:-0} + ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-} + volumes: + - "./.env:/var/www/.env" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/shared/proxy/conf.d" + - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" + - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" + - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" depends_on: - db - redis + healthcheck: + test: gosu www-data php artisan horizon:status | grep running + interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL:?error}" + timeout: 5s + retries: 2 -## DB and Cache db: - image: mariadb:jammy + image: ${DOCKER_DB_IMAGE:?error} + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-db" + command: ${DOCKER_DB_COMMAND:-} restart: unless-stopped - networks: - - internal - command: --default-authentication-plugin=mysql_native_password - env_file: - - .env.docker + profiles: + - ${DOCKER_DB_PROFILE:-} + environment: + TZ: "${TZ:?error}" + # MySQL (Oracle) - "Environment Variables" at https://hub.docker.com/_/mysql + MYSQL_ROOT_PASSWORD: "${DOCKER_DB_ROOT_PASSWORD:?error}" + MYSQL_USER: "${DB_USERNAME:?error}" + MYSQL_PASSWORD: "${DB_PASSWORD:?error}" + MYSQL_DATABASE: "${DB_DATABASE:?error}" + # MySQL (MariaDB) - "Start a mariadb server instance with user, password and database" at https://hub.docker.com/_/mariadb + MARIADB_ROOT_PASSWORD: "${DOCKER_DB_ROOT_PASSWORD:?error}" + MARIADB_USER: "${DB_USERNAME:?error}" + MARIADB_PASSWORD: "${DB_PASSWORD:?error}" + MARIADB_DATABASE: "${DB_DATABASE:?error}" + # PostgreSQL - "Environment Variables" at https://hub.docker.com/_/postgres + POSTGRES_USER: "${DB_USERNAME:?error}" + POSTGRES_PASSWORD: "${DB_PASSWORD:?error}" + POSTGRES_DB: "${DB_DATABASE:?error}" volumes: - - "db-data:/var/lib/mysql" + - "${DOCKER_DB_HOST_DATA_PATH:?error}:${DOCKER_DB_CONTAINER_DATA_PATH:?error}" + ports: + - "${DOCKER_DB_HOST_PORT:?error}:${DOCKER_DB_CONTAINER_PORT:?error}" + healthcheck: + test: + [ + "CMD", + "healthcheck.sh", + "--su-mysql", + "--connect", + "--innodb_initialized", + ] + interval: "${DOCKER_DB_HEALTHCHECK_INTERVAL:?error}" + retries: 2 + timeout: 5s redis: - image: redis:5-alpine + image: redis:${DOCKER_REDIS_VERSION} + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-redis" restart: unless-stopped - env_file: - - .env.docker + command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'" + profiles: + - ${DOCKER_REDIS_PROFILE:-} + environment: + TZ: "${TZ:?error}" + REDISCLI_AUTH: ${REDIS_PASSWORD:-} volumes: - - "redis-data:/data" - networks: - - internal - -volumes: - db-data: - redis-data: - app-storage: - app-bootstrap: - -networks: - internal: - internal: true - external: - driver: bridge + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/redis:/etc/redis" + - "${DOCKER_REDIS_HOST_DATA_PATH}:/data" + ports: + - "${DOCKER_REDIS_HOST_PORT}:6379" + healthcheck: + test: ["CMD", "redis-cli", "-p", "6379", "ping"] + interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL:?error}" + retries: 2 + timeout: 5s diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..5230f60fd --- /dev/null +++ b/docker/README.md @@ -0,0 +1,5 @@ +# Pixelfed + Docker + Docker Compose + +Please see the [Pixelfed Docs (Next)](https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/) for current documentation on Docker usage. + +The docs can be [reviewed in the pixelfed/docs-next](https://github.com/pixelfed/docs-next/pull/1) repository. diff --git a/docker/apache/root/etc/apache2/conf-available/remoteip.conf b/docker/apache/root/etc/apache2/conf-available/remoteip.conf new file mode 100644 index 000000000..516da9f5d --- /dev/null +++ b/docker/apache/root/etc/apache2/conf-available/remoteip.conf @@ -0,0 +1,8 @@ +RemoteIPHeader X-Real-IP + +# All private IPs as outlined in rfc1918 +# +# See: https://datatracker.ietf.org/doc/html/rfc1918 +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 diff --git a/docker/artisan b/docker/artisan new file mode 100755 index 000000000..3bbf58aea --- /dev/null +++ b/docker/artisan @@ -0,0 +1,11 @@ +#!/bin/bash + +declare service="${PF_SERVICE:=worker}" +declare user="${PF_USER:=www-data}" + +exec docker compose exec \ + --user "${user}" \ + --env TERM \ + --env COLORTERM \ + "${service}" \ + php artisan "${@}" diff --git a/docker/check-requirements b/docker/check-requirements new file mode 100755 index 000000000..4da1d6427 --- /dev/null +++ b/docker/check-requirements @@ -0,0 +1,126 @@ +#!/bin/bash + +set -e -o errexit -o nounset -o pipefail + +# +# Colors +# + +declare -r RED="\e[31m" +declare -r GREEN="\e[32m" +declare -r BLUE="\e[34m" +declare -r NO_COLOR="\e[0m" + +# +# Helper functions +# + +function highlight() { + local reset="${2:-$NO_COLOR}" + echo "${BLUE}$1${reset}" +} + +function action_start() { + echo -en "⚙️ $1: " +} + +function action_ok() { + echo -e "\n\t✅ ${GREEN}${*}${NO_COLOR}\n" +} + +function action_error() { + echo -e "\n\t❌ ${RED}${*}${NO_COLOR}" >&2 +} + +function action_error_exit() { + action_error "${*}\n\n${RED}Aborting!${NO_COLOR}" + + exit 1 +} + +# +# Configuration +# + +declare -r min_docker_compose_version_arr=(2 17) +min_docker_compose_version=$( + IFS=. + echo "${min_docker_compose_version[*]}" +) + +# +# Help text +# + +DOCKER_HELP=" + +\tWe recommend installing Docker (and Compose) directly from Docker.com instead of your Operation System package registry. +\tPlease see $(highlight "https://docs.docker.com/engine/install/")${RED} for information on how to install Docker on your system. + +\tA convinience script is provided by Docker to automate the installation that should work on all supported platforms: + +\t\t ${GREEN}\$${BLUE} curl -fsSL https://get.docker.com -o get-docker.sh +\t\t ${GREEN}\$${BLUE} sudo sh ./get-docker.sh +${RED} +\tPlease see $(highlight "https://docs.docker.com/engine/install/ubuntu/#install-using-the-convenience-script")${RED} for more information + +\tAlternatively, you can update *JUST* the Compose plugin by following the guide here: +\t$(highlight "https://docs.docker.com/compose/install/linux/#install-the-plugin-manually")${RED} + +\tLearn more about Docker compose release history here: +\t$(highlight "https://docs.docker.com/compose/release-notes/")${RED}${NO_COLOR}" +declare -r DOCKER_HELP + +# +# System checks +# + +echo -e "👋 ${GREEN}Hello!" +echo -e "" +echo -e "This script will check your system for the minimum requirements outlined in the Pixelfed Docker install guide" +echo -e "You can find the guide here ${BLUE}https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/docker/prerequisites.html#software${GREEN}." +echo -e "${NO_COLOR}" + +# git installed? +action_start "Checking if [$(highlight "git")] command is available" +command -v git >/dev/null 2>&1 || { + action_error_exit "Pixelfed require the 'git' command, but it's not installed" +} +action_ok "git is installed" + +# docker installed? +action_start "Checking if [$(highlight "docker")] command is available" +command -v docker >/dev/null 2>&1 || { + action_error_exit "Pixelfed require the 'docker' command, but it's not installed. ${DOCKER_HELP}" +} +action_ok "docker is installed" + +# docker compose installed? +action_start "Checking if [$(highlight "docker compose")] command is available" +docker compose >/dev/null 2>&1 || { + action_error_exit "Pixelfed require the 'docker compose' command, but it's not installed. ${DOCKER_HELP}" +} +action_ok "docker compose is installed" + +# docker compose version is acceptable? +compose_version=$(docker compose version --short) + +declare -a compose_version_arr +IFS="." read -r -a compose_version_arr <<<"$compose_version" + +## major version +action_start "Checking if [$(highlight "docker compose version")] major version (${min_docker_compose_version_arr[0]}) is acceptable" +[[ ${compose_version_arr[0]} -eq ${min_docker_compose_version_arr[0]} ]] || { + action_error_exit "Pixelfed require minimum Docker Compose major version ${min_docker_compose_version_arr[0]}.x.x - found ${compose_version}.${DOCKER_HELP}" +} +action_ok "You're using major version ${compose_version_arr[0]}" + +## minor version +action_start "Checking if [$(highlight "docker compose version")] minor version (${min_docker_compose_version_arr[1]}) is acceptable" +[[ ${compose_version_arr[1]} -ge ${min_docker_compose_version_arr[1]} ]] || { + action_error_exit "Pixelfed require minimum Docker Compose minor version ${min_docker_compose_version_arr[0]}.${min_docker_compose_version_arr[1]} - found ${compose_version}.${DOCKER_HELP}" +} +action_ok "You're using minor version ${compose_version_arr[1]}" + +# Yay, everything is fine +echo -e "🎉 ${GREEN}All checks passed, you should be ready to run Pixelfed on this server!${NO_COLOR}" diff --git a/docker/dottie b/docker/dottie new file mode 100755 index 000000000..8bd304a03 --- /dev/null +++ b/docker/dottie @@ -0,0 +1,45 @@ +#!/bin/bash + +set -e -o errexit -o nounset -o pipefail + +declare project_root="${PWD}" +declare user="${PF_USER:=www-data}" + +if command -v git &>/dev/null; then + project_root=$(git rev-parse --show-toplevel) +fi + +declare -r release="${DOTTIE_VERSION:-latest}" + +declare -r update_check_file="/tmp/.dottie-update-check" # file to check age of since last update +declare -i update_check_max_age=$((8 * 60 * 60)) # 8 hours between checking for dottie version +declare -i update_check_cur_age=$((update_check_max_age + 1)) # by default the "update" event should happen + +# default [docker run] flags +declare -a flags=( + --rm + --interactive + --tty + --user "${user}" + --env TERM + --env COLORTERM + --volume "${project_root}:/var/www" + --workdir /var/www +) + +# if update file exists, find its age since last modification +if [[ -f "${update_check_file}" ]]; then + now=$(date +%s) + changed=$(date -r "${update_check_file}" +%s) + update_check_cur_age=$((now - changed)) +fi + +# if update file is older than max allowed poll for new version of dottie +if [[ $update_check_cur_age -gt $update_check_max_age ]]; then + flags+=(--pull always) + + touch "${update_check_file}" +fi + +# run dottie +exec docker run "${flags[@]}" "ghcr.io/jippi/dottie:${release}" "$@" diff --git a/docker/fpm/root/.gitkeep b/docker/fpm/root/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/docker/nginx/Procfile b/docker/nginx/Procfile new file mode 100644 index 000000000..bd375bf6a --- /dev/null +++ b/docker/nginx/Procfile @@ -0,0 +1,2 @@ +fpm: php-fpm +nginx: nginx -g "daemon off;" diff --git a/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf b/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf new file mode 100644 index 000000000..15bf17beb --- /dev/null +++ b/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf @@ -0,0 +1,49 @@ +server { + listen 80 default_server; + + server_name {{ getenv "APP_DOMAIN" }}; + root /var/www/public; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Content-Type-Options "nosniff"; + + access_log /dev/stdout; + error_log /dev/stderr warn; + + index index.html index.htm index.php; + + charset utf-8; + client_max_body_size {{ getenv "POST_MAX_SIZE" "61M" }}; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { + access_log off; + log_not_found off; + } + + location = /robots.txt { + access_log off; + log_not_found off; + } + + error_page 404 /index.php; + + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + + include fastcgi_params; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} diff --git a/docker/nginx/root/docker/templates/etc/nginx/nginx.conf b/docker/nginx/root/docker/templates/etc/nginx/nginx.conf new file mode 100644 index 000000000..4e87a4565 --- /dev/null +++ b/docker/nginx/root/docker/templates/etc/nginx/nginx.conf @@ -0,0 +1,41 @@ +# This is changed from the original "nginx" in upstream to work properly +# with permissions within pixelfed when serving static files. +user www-data; + +worker_processes auto; + +# Ensure the PID is writable +# Lifted from: https://hub.docker.com/r/nginxinc/nginx-unprivileged +pid /tmp/nginx.pid; + +# Write error log to stderr (/proc/self/fd/2 -> /dev/stderr) +error_log /proc/self/fd/2 notice; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"'; + + # Write error log to stdout (/proc/self/fd/1 -> /dev/stdout) + access_log /proc/self/fd/1 main; + + sendfile on; + tcp_nopush on; + keepalive_timeout 65; + gzip on; + + # Ensure all temp paths are in a writable by "www-data" user. + # Lifted from: https://hub.docker.com/r/nginxinc/nginx-unprivileged + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh new file mode 100755 index 000000000..dc9dc7591 --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -0,0 +1,31 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +# Ensure the Docker volumes and required files are owned by the runtime user as other scripts +# will be writing to these +run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./.env" +run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap/cache" +run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage" +run-as-current-user chown --verbose --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "./storage/docker" + +# Optionally fix ownership of configured paths +: "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS:=""}" + +declare -a ensure_ownership_paths=() +IFS=' ' read -r -a ensure_ownership_paths <<<"${DOCKER_APP_ENSURE_OWNERSHIP_PATHS}" + +if [[ ${#ensure_ownership_paths[@]} == 0 ]]; then + log-info "No paths has been configured for ownership fixes via [\$DOCKER_APP_ENSURE_OWNERSHIP_PATHS]." + + exit 0 +fi + +for path in "${ensure_ownership_paths[@]}"; do + log-info "Ensure ownership of [${path}] is correct" + stream-prefix-command-output run-as-current-user chown --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "${path}" +done diff --git a/docker/shared/root/docker/entrypoint.d/02-check-config.sh b/docker/shared/root/docker/entrypoint.d/02-check-config.sh new file mode 100755 index 000000000..627960352 --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/02-check-config.sh @@ -0,0 +1,21 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +# Validating dot-env files for any issues +for file in "${dot_env_files[@]}"; do + if ! file-exists "${file}"; then + log-warning "Could not source file [${file}]: does not exists" + continue + fi + + # We ignore 'dir' + 'file' rules since they are validate *host* paths + # which do not (and should not) exists inside the container + # + # We disable fixer since its not interactive anyway + run-as-current-user dottie validate --file "${file}" --ignore-rule dir,file --exclude-prefix APP_KEY --no-fix +done diff --git a/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh new file mode 100755 index 000000000..a55a56e6c --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh @@ -0,0 +1,33 @@ +#!/bin/bash + +# NOTE: +# +# This file is *sourced* not run by the entrypoint runner +# so any environment values set here will be accessible to all sub-processes +# and future entrypoint.d scripts +# +# We also don't need to source `helpers.sh` since it's already available + +entrypoint-set-script-name "${BASH_SOURCE[0]}" + +load-config-files + +: "${MAX_PHOTO_SIZE:=15000}" +: "${MAX_ALBUM_LENGTH:=4}" + +# We assign a 1MB buffer to the just-in-time calculated max post size to allow for fields and overhead +: "${POST_MAX_SIZE_BUFFER:=1M}" +log-info "POST_MAX_SIZE_BUFFER is set to [${POST_MAX_SIZE_BUFFER}]" +buffer=$(numfmt --invalid=fail --from=auto --to=none --to-unit=K "${POST_MAX_SIZE_BUFFER}") +log-info "POST_MAX_SIZE_BUFFER converted to KB is [${buffer}]" + +# Automatically calculate the [post_max_size] value for [php.ini] and [nginx] +log-info "POST_MAX_SIZE will be calculated by [({MAX_PHOTO_SIZE} * {MAX_ALBUM_LENGTH}) + {POST_MAX_SIZE_BUFFER}]" +log-info " MAX_PHOTO_SIZE=${MAX_PHOTO_SIZE}" +log-info " MAX_ALBUM_LENGTH=${MAX_ALBUM_LENGTH}" +log-info " POST_MAX_SIZE_BUFFER=${buffer}" +: "${POST_MAX_SIZE:=$(numfmt --invalid=fail --from=auto --from-unit=K --to=si $(((MAX_PHOTO_SIZE * MAX_ALBUM_LENGTH) + buffer)))}" +log-info "POST_MAX_SIZE was calculated to [${POST_MAX_SIZE}]" + +# NOTE: must export the value so it's available in other scripts! +export POST_MAX_SIZE diff --git a/docker/shared/root/docker/entrypoint.d/05-templating.sh b/docker/shared/root/docker/entrypoint.d/05-templating.sh new file mode 100755 index 000000000..e699778cf --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -0,0 +1,56 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +# Show [git diff] of templates being rendered (will help verify output) +: "${ENTRYPOINT_SHOW_TEMPLATE_DIFF:=1}" +# Directory where templates can be found +: "${ENTRYPOINT_TEMPLATE_DIR:=/docker/templates/}" +# Root path to write template template_files to (default is '', meaning it will be written to /) +: "${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX:=}" + +declare template_file relative_template_file_path output_file_dir + +# load all dot-env config files +load-and-export-config-files + + +find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r template_file; do + # Example: template_file=/docker/templates/usr/local/etc/php/php.ini + + # The file path without the template dir prefix ($ENTRYPOINT_TEMPLATE_DIR) + # + # Example: /usr/local/etc/php/php.ini + relative_template_file_path="${template_file#"${ENTRYPOINT_TEMPLATE_DIR}"}" + + # Adds optional prefix to the output file path + # + # Example: /usr/local/etc/php/php.ini + output_file_path="${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX}/${relative_template_file_path}" + + # Remove the file from the path + # + # Example: /usr/local/etc/php + output_file_dir=$(dirname "${output_file_path}") + + # Ensure the output directory is writable + if ! is-writable "${output_file_dir}"; then + log-error-and-exit "${output_file_dir} is not writable" + fi + + # Create the output directory if it doesn't exists + ensure-directory-exists "${output_file_dir}" + + # Render the template + log-info "Running [gomplate] on [${template_file}] --> [${output_file_path}]" + gomplate <"${template_file}" >"${output_file_path}" + + # Show the diff from the envsubst command + if is-true "${ENTRYPOINT_SHOW_TEMPLATE_DIFF}"; then + git --no-pager diff --color=always "${template_file}" "${output_file_path}" || : # ignore diff exit code + fi +done diff --git a/docker/shared/root/docker/entrypoint.d/10-storage.sh b/docker/shared/root/docker/entrypoint.d/10-storage.sh new file mode 100755 index 000000000..54145a365 --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -0,0 +1,13 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +# Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions +run-as-runtime-user cp --force --recursive storage.skel/. ./storage/ + +# Ensure storage linkk are correctly configured +run-as-runtime-user php artisan storage:link diff --git a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh new file mode 100755 index 000000000..fb5c86a39 --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -0,0 +1,38 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +load-config-files + +# Allow automatic applying of outstanding/new migrations on startup +: "${DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS:=1}" + +if is-false "${DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS}"; then + log-warning "Automatic run of the 'One-time setup tasks' is disabled." + log-warning "Please set [DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS=1] in your [.env] file to enable this." + + exit 0 +fi + +await-database-ready + +# Following https://docs.pixelfed.org/running-pixelfed/installation/#one-time-setup-tasks +# +# NOTE: Caches happens in [30-cache.sh] + +only-once "key:generate" run-as-runtime-user php artisan key:generate +only-once "storage:link" run-as-runtime-user php artisan storage:link +only-once "initial:migrate" run-as-runtime-user php artisan migrate --force +only-once "import:cities" run-as-runtime-user php artisan import:cities + +if is-true "${ACTIVITY_PUB:-false}"; then + only-once "instance:actor" run-as-runtime-user php artisan instance:actor +fi + +if is-true "${OAUTH_ENABLED:-false}"; then + only-once "passport:keys" run-as-runtime-user php artisan passport:keys +fi diff --git a/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/docker/shared/root/docker/entrypoint.d/12-migrations.sh new file mode 100755 index 000000000..3b87daf1f --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/12-migrations.sh @@ -0,0 +1,42 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +# Allow automatic applying of outstanding/new migrations on startup +: "${DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY:=0}" + +# Wait for the database to be ready +await-database-ready + +# Run the migrate:status command and capture output +output=$(run-as-runtime-user php artisan migrate:status || :) + +# By default we have no new migrations +declare -i new_migrations=0 + +# Detect if any new migrations are available by checking for "No" in the output +echo "$output" | grep No && new_migrations=1 + +if is-false "${new_migrations}"; then + log-info "No new migrations detected" + + exit 0 +fi + +log-warning "New migrations available" + +# Print the output +echo "$output" + +if is-false "${DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY}"; then + log-info "Automatic applying of new database migrations is disabled" + log-info "Please set [DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY=1] in your [.env] file to enable this." + + exit 0 +fi + +run-as-runtime-user php artisan migrate --force diff --git a/docker/shared/root/docker/entrypoint.d/20-horizon.sh b/docker/shared/root/docker/entrypoint.d/20-horizon.sh new file mode 100755 index 000000000..55efd768d --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/20-horizon.sh @@ -0,0 +1,9 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +run-as-runtime-user php artisan horizon:publish diff --git a/docker/shared/root/docker/entrypoint.d/30-cache.sh b/docker/shared/root/docker/entrypoint.d/30-cache.sh new file mode 100755 index 000000000..c970db60b --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/30-cache.sh @@ -0,0 +1,11 @@ +#!/bin/bash +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +entrypoint-set-script-name "$0" + +run-as-runtime-user php artisan config:cache +run-as-runtime-user php artisan route:cache +run-as-runtime-user php artisan view:cache diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh new file mode 100755 index 000000000..055cf25d7 --- /dev/null +++ b/docker/shared/root/docker/entrypoint.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# short curcuit the entrypoint if $ENTRYPOINT_SKIP isn't set to 0 +if [[ ${ENTRYPOINT_SKIP:=0} != 0 ]]; then + exec "$@" +fi + +: "${ENTRYPOINT_ROOT:="/docker"}" +export ENTRYPOINT_ROOT + +# Directory where entrypoint scripts lives +: "${ENTRYPOINT_D_ROOT:="${ENTRYPOINT_ROOT}/entrypoint.d/"}" +export ENTRYPOINT_D_ROOT + +: "${DOCKER_APP_HOST_OVERRIDES_PATH:="${ENTRYPOINT_ROOT}/overrides"}" +export DOCKER_APP_HOST_OVERRIDES_PATH + +# Space separated list of scripts the entrypoint runner should skip +: "${ENTRYPOINT_SKIP_SCRIPTS:=""}" + +# Load helper scripts +# +# shellcheck source=SCRIPTDIR/helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" + +# Set the entrypoint name for logging +entrypoint-set-script-name "entrypoint.sh" + +# Convert ENTRYPOINT_SKIP_SCRIPTS into a native bash array for easier lookup +declare -a skip_scripts +# shellcheck disable=SC2034 +IFS=' ' read -r -a skip_scripts <<< "$ENTRYPOINT_SKIP_SCRIPTS" + +# Ensure the entrypoint root folder exists +mkdir -p "${ENTRYPOINT_D_ROOT}" + +# If ENTRYPOINT_D_ROOT directory is empty, warn and run the regular command +if directory-is-empty "${ENTRYPOINT_D_ROOT}"; then + log-warning "No files found in ${ENTRYPOINT_D_ROOT}, skipping configuration" + + exec "$@" +fi + +# If the overridess directory exists, then copy all files into the container +if ! directory-is-empty "${DOCKER_APP_HOST_OVERRIDES_PATH}"; then + log-info "Overrides directory is not empty, copying files" + run-as-current-user cp --verbose --recursive "${DOCKER_APP_HOST_OVERRIDES_PATH}/." / +fi + +acquire-lock "entrypoint.sh" + +# Start scanning for entrypoint.d files to source or run +log-info "looking for shell scripts in [${ENTRYPOINT_D_ROOT}]" + +find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r file; do + # Skip the script if it's in the skip-script list + if in-array "$(get-entrypoint-script-name "${file}")" skip_scripts; then + log-warning "Skipping script [${file}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" + + continue + fi + + # Inspect the file extension of the file we're processing + case "${file}" in + *.envsh) + if ! is-executable "${file}"; then + # warn on shell scripts without exec bit + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" + fi + + log-info "${section_message_color}============================================================${color_clear}" + log-info "${section_message_color}Sourcing [${file}]${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" + + # shellcheck disable=SC1090 + source "${file}" + + # the sourced file will (should) than the log prefix, so this restores our own + # "global" log prefix once the file is done being sourced + entrypoint-restore-script-name + ;; + + *.sh) + if ! is-executable "${file}"; then + # warn on shell scripts without exec bit + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" + fi + + log-info "${section_message_color}============================================================${color_clear}" + log-info "${section_message_color}Executing [${file}]${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" + + "${file}" + ;; + + *) + log-warning "Ignoring unrecognized file [${file}]" + ;; + esac +done + +release-lock "entrypoint.sh" + +log-info "Configuration complete; ready for start up" + +exec "$@" diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh new file mode 100644 index 000000000..631b0ef0e --- /dev/null +++ b/docker/shared/root/docker/helpers.sh @@ -0,0 +1,592 @@ +#!/bin/bash +set -e -o errexit -o nounset -o pipefail + +[[ ${DOCKER_APP_ENTRYPOINT_DEBUG:=0} == 1 ]] && set -x + +: "${RUNTIME_UID:="33"}" +: "${RUNTIME_GID:="33"}" + +# Some splash of color for important messages +declare -g error_message_color="\033[1;31m" +declare -g warn_message_color="\033[1;33m" +declare -g notice_message_color="\033[1;34m" +declare -g success_message_color="\033[1;32m" +# shellcheck disable=SC2034 +declare -g section_message_color="\033[1;35m" +declare -g color_clear="\033[1;0m" + +# Current and previous log prefix +declare -g script_name= +declare -g script_name_previous= +declare -g log_prefix= + +declare -Ag lock_fds=() + +# dot-env files to source when reading config +declare -a dot_env_files=( + /var/www/.env +) + +declare -g docker_state_path +docker_state_path="$(readlink -f ./storage/docker)" + +declare -g docker_locks_path="${docker_state_path}/lock" +declare -g docker_once_path="${docker_state_path}/once" + +declare -g runtime_username +runtime_username=$(id -un "${RUNTIME_UID}") + +# We should already be in /var/www, but just to be explicit +cd /var/www || log-error-and-exit "could not change to /var/www" + +# @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] +# @arg $1 string The name (or path) of the entrypoint script being run +function entrypoint-set-script-name() +{ + script_name_previous="${script_name}" + script_name="${1}" + + log_prefix="[entrypoint / $(get-entrypoint-script-name "$1")] - " +} + +# @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] +function entrypoint-restore-script-name() +{ + entrypoint-set-script-name "${script_name_previous}" +} + +# @description Run a command as the [runtime user] +# @arg $@ string The command to run +# @exitcode 0 if the command succeeeds +# @exitcode 1 if the command fails +function run-as-runtime-user() +{ + run-command-as "${runtime_username}" "${@}" +} + +# @description Run a command as the [runtime user] +# @arg $@ string The command to run +# @exitcode 0 if the command succeeeds +# @exitcode 1 if the command fails +function run-as-current-user() +{ + run-command-as "$(id -un)" "${@}" +} + +# @description Run a command as the a named user +# @arg $1 string The user to run the command as +# @arg $@ string The command to run +# @exitcode 0 If the command succeeeds +# @exitcode 1 If the command fails +function run-command-as() +{ + local -i exit_code + local target_user + + target_user=${1} + shift + + log-info-stderr "${notice_message_color}👷 Running [${*}] as [${target_user}]${color_clear}" + + # disable error on exit behavior temporarily while we run the command + set +e + + if [[ ${target_user} != "root" ]]; then + stream-prefix-command-output su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" + else + stream-prefix-command-output "${@}" + fi + + # capture exit code + exit_code=$? + + # re-enable exit code handling + set -e + + if [[ $exit_code != 0 ]]; then + log-error "${error_message_color}❌ Error!${color_clear}" + + return "$exit_code" + fi + + log-info-stderr "${success_message_color}✅ OK!${color_clear}" + + return "$exit_code" +} + +# @description Streams stdout from the command and echo it +# with log prefixing. +# @see stream-prefix-command-output +function stream-stdout-handler() +{ + while read -r line; do + log-info "(stdout) ${line}" + done +} + +# @description Streams stderr from the command and echo it +# with a bit of color and log prefixing. +# @see stream-prefix-command-output +function stream-stderr-handler() +{ + while read -r line; do + log-info-stderr "(${error_message_color}stderr${color_clear}) ${line}" + done +} + +# @description Steam stdout and stderr from a command with log prefix +# and stdout/stderr prefix. If stdout or stderr is being piped/redirected +# it will automatically fall back to non-prefixed output. +# @arg $@ string The command to run +function stream-prefix-command-output() +{ + local stdout=stream-stdout-handler + local stderr=stream-stderr-handler + + # if stdout is being piped, print it like normal with echo + if [ ! -t 1 ]; then + # shellcheck disable=SC1007 + stdout= echo >&1 -ne + fi + + # if stderr is being piped, print it like normal with echo + if [ ! -t 2 ]; then + # shellcheck disable=SC1007 + stderr= echo >&2 -ne + fi + + "$@" > >($stdout) 2> >($stderr) +} + +# @description Print the given error message to stderr +# @arg $message string A error message. +# @stderr The error message provided with log prefix +function log-error() +{ + local msg + + if [[ $# -gt 0 ]]; then + msg="$*" + elif [[ ! -t 0 ]]; then + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" + fi + + echo -e "${error_message_color}${log_prefix}ERROR -${color_clear} ${msg}" >/dev/stderr +} + +# @description Print the given error message to stderr and exit 1 +# @arg $@ string A error message. +# @stderr The error message provided with log prefix +# @exitcode 1 +function log-error-and-exit() +{ + log-error "$@" + + show-call-stack + + exit 1 +} + +# @description Print the given warning message to stderr +# @arg $@ string A warning message. +# @stderr The warning message provided with log prefix +function log-warning() +{ + local msg + + if [[ $# -gt 0 ]]; then + msg="$*" + elif [[ ! -t 0 ]]; then + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" + fi + + echo -e "${warn_message_color}${log_prefix}WARNING -${color_clear} ${msg}" >/dev/stderr +} + +# @description Print the given message to stdout unless [ENTRYPOINT_QUIET_LOGS] is set +# @arg $@ string A info message. +# @stdout The info message provided with log prefix unless $ENTRYPOINT_QUIET_LOGS +function log-info() +{ + local msg + + if [[ $# -gt 0 ]]; then + msg="$*" + elif [[ ! -t 0 ]]; then + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" + fi + + if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo -e "${notice_message_color}${log_prefix}${color_clear}${msg}" + fi +} + +# @description Print the given message to stderr unless [ENTRYPOINT_QUIET_LOGS] is set +# @arg $@ string A info message. +# @stderr The info message provided with log prefix unless $ENTRYPOINT_QUIET_LOGS +function log-info-stderr() +{ + local msg + + if [[ $# -gt 0 ]]; then + msg="$*" + elif [[ ! -t 0 ]]; then + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" + fi + + if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo -e "${notice_message_color}${log_prefix}${color_clear}${msg}" >/dev/stderr + fi +} + +# @description Loads the dot-env files used by Docker +function load-config-files() { + local export_vars=0 + load-config-files-impl "$export_vars" +} + +# @description Loads the dot-env files used by Docker and exports the variables to subshells +function load-and-export-config-files() { + local export_vars=1 + load-config-files-impl "$export_vars" +} + +# @description Implementation of the [load-config-files] and [load-and-export-config-files] functions. Loads th +# @arg $1 int Whether to export the variables or just have them available in the current shell +function load-config-files-impl() +{ + local export_vars=${1:-0} + for file in "${dot_env_files[@]}"; do + if ! file-exists "${file}"; then + log-warning "Could not source file [${file}]: does not exists" + continue + fi + + log-info "Sourcing ${file}" + if ((export_vars)); then set -o allexport; fi + # shellcheck disable=SC1090 + source "${file}" + if ((export_vars)); then set +o allexport; fi + done +} + +# @description Checks if $needle exists in $haystack +# @arg $1 string The needle (value) to search for +# @arg $2 array The haystack (array) to search in +# @exitcode 0 If $needle was found in $haystack +# @exitcode 1 If $needle was *NOT* found in $haystack +function in-array() +{ + local -r needle="\<${1}\>" + local -nr haystack=$2 + + [[ ${haystack[*]} =~ $needle ]] +} + +# @description Checks if $1 has executable bit set or not +# @arg $1 string The path to check +# @exitcode 0 If $1 has executable bit +# @exitcode 1 If $1 does *NOT* have executable bit +function is-executable() +{ + [[ -x "$1" ]] +} + +# @description Checks if $1 is writable or not +# @arg $1 string The path to check +# @exitcode 0 If $1 is writable +# @exitcode 1 If $1 is *NOT* writable +function is-writable() +{ + [[ -w "$1" ]] +} + +# @description Checks if $1 exists (directory or file) +# @arg $1 string The path to check +# @exitcode 0 If $1 exists +# @exitcode 1 If $1 does *NOT* exists +function path-exists() +{ + [[ -e "$1" ]] +} + +# @description Checks if $1 exists (file only) +# @arg $1 string The path to check +# @exitcode 0 If $1 exists +# @exitcode 1 If $1 does *NOT* exists +function file-exists() +{ + [[ -f "$1" ]] +} + +# @description Checks if $1 contains any files or not +# @arg $1 string The path to check +# @exitcode 0 If $1 contains files +# @exitcode 1 If $1 does *NOT* contain files +function directory-is-empty() +{ + ! path-exists "${1}" || [[ -z "$(ls -A "${1}")" ]] +} + +# @description Ensures a directory exists (via mkdir) +# @arg $1 string The path to create +# @exitcode 0 If $1 If the path exists *or* was created +# @exitcode 1 If $1 If the path does *NOT* exists and could *NOT* be created +function ensure-directory-exists() +{ + stream-prefix-command-output mkdir -pv "$@" +} + +# @description Find the relative path for a entrypoint script by removing the ENTRYPOINT_D_ROOT prefix +# @arg $1 string The path to manipulate +# @stdout The relative path to the entrypoint script +function get-entrypoint-script-name() +{ + echo "${1#"$ENTRYPOINT_D_ROOT"}" +} + +# @description Ensure a command is only run once (via a 'lock' file) in the storage directory. +# The 'lock' is only written if the passed in command ($2) successfully ran. +# @arg $1 string The name of the lock file +# @arg $@ string The command to run +function only-once() +{ + local name="${1:-$script_name}" + local file="${docker_once_path}/${name}" + shift + + if [[ -e "${file}" ]]; then + log-info "Command [${*}] has already run once before (remove file [${file}] to run it again)" + + return 0 + fi + + ensure-directory-exists "$(dirname "${file}")" + + if ! "$@"; then + return 1 + fi + + stream-prefix-command-output touch "${file}" + return 0 +} + +# @description Best effort file lock to ensure *something* is not running in multiple containers. +# The script uses "trap" to clean up after itself if the script crashes +# @arg $1 string The lock identifier +function acquire-lock() +{ + local name="${1:-$script_name}" + local file="${docker_locks_path}/${name}" + local lock_fd + + ensure-directory-exists "$(dirname "${file}")" + + exec {lock_fd}>"$file" + + log-info "🔑 Trying to acquire lock: ${file}: " + while ! ([[ -v lock_fds[$name] ]] || flock -n -x "$lock_fd"); do + log-info "🔒 Waiting on lock ${file}" + + staggered-sleep + done + + [[ -v lock_fds[$name] ]] || lock_fds[$name]=$lock_fd + + log-info "🔐 Lock acquired [${file}]" + + on-trap "release-lock ${name}" EXIT INT QUIT TERM +} + +# @description Release a lock aquired by [acquire-lock] +# @arg $1 string The lock identifier +function release-lock() +{ + local name="${1:-$script_name}" + local file="${docker_locks_path}/${name}" + + log-info "🔓 Releasing lock [${file}]" + + [[ -v lock_fds[$name] ]] || return + + # shellcheck disable=SC1083,SC2086 + flock --unlock ${lock_fds[$name]} + unset 'lock_fds[$name]' +} + +# @description Helper function to append multiple actions onto +# the bash [trap] logic +# @arg $1 string The command to run +# @arg $@ string The list of trap signals to register +function on-trap() +{ + local trap_add_cmd=$1 + shift || log-error-and-exit "${FUNCNAME[0]} usage error" + + for trap_add_name in "$@"; do + trap -- "$( + # helper fn to get existing trap command from output + # of trap -p + # + # shellcheck disable=SC2317 + extract_trap_cmd() + { + printf '%s\n' "${3:-}" + } + # print existing trap command with newline + eval "extract_trap_cmd $(trap -p "${trap_add_name}")" + # print the new trap command + printf '%s\n' "${trap_add_cmd}" + )" "${trap_add_name}" \ + || log-error-and-exit "unable to add to trap ${trap_add_name}" + done +} + +# Set the trace attribute for the above function. +# +# This is required to modify DEBUG or RETURN traps because functions don't +# inherit them unless the trace attribute is set +declare -f -t on-trap + +# @description Waits for the database to be healthy and responsive +function await-database-ready() +{ + log-info "❓ Waiting for database to be ready" + + load-config-files + + case "${DB_CONNECTION:-}" in + mysql) + # shellcheck disable=SC2154 + while ! echo "SELECT 1" | mysql --user="${DB_USERNAME}" --password="${DB_PASSWORD}" --host="${DB_HOST}" --port="${DOCKER_DB_HOST_PORT}" "${DB_DATABASE}" --silent >/dev/null; do + staggered-sleep + done + ;; + + pgsql) + # shellcheck disable=SC2154 + while ! echo "SELECT 1" | PGPASSWORD="${DB_PASSWORD}" psql --user="${DB_USERNAME}" --host="${DB_HOST}" --port="${DOCKER_DB_HOST_PORT}" "${DB_DATABASE}" >/dev/null; do + staggered-sleep + done + ;; + + sqlsrv) + log-warning "Don't know how to check if SQLServer is *truely* ready or not - so will just check if we're able to connect to it" + + # shellcheck disable=SC2154 + while ! timeout 1 bash -c "cat < /dev/null > /dev/tcp/${DB_HOST}/${DB_PORT}"; do + staggered-sleep + done + ;; + + sqlite) + log-info "${success_message_color}sqlite is always ready${color_clear}" + ;; + + *) + log-error-and-exit "Unknown database type: [${DB_CONNECTION:-}]" + ;; + esac + + log-info "${success_message_color}✅ Successfully connected to database${color_clear}" +} + +# @description sleeps between 1 and 3 seconds to ensure a bit of randomness +# in multiple scripts/containers doing work almost at the same time. +function staggered-sleep() +{ + sleep "$(get-random-number-between 1 3)" +} + +# @description Helper function to get a random number between $1 and $2 +# @arg $1 int Minimum number in the range (inclusive) +# @arg $2 int Maximum number in the range (inclusive) +function get-random-number-between() +{ + local -i from=${1:-1} + local -i to="${2:-10}" + + shuf -i "${from}-${to}" -n 1 +} + +# @description Helper function to show the bask call stack when something +# goes wrong. Is super useful when needing to debug an issue +function show-call-stack() +{ + local stack_size=${#FUNCNAME[@]} + local func + local lineno + local src + + # to avoid noise we start with 1 to skip the get_stack function + for ((i = 1; i < stack_size; i++)); do + func="${FUNCNAME[$i]}" + [ -z "$func" ] && func="MAIN" + + lineno="${BASH_LINENO[$((i - 1))]}" + src="${BASH_SOURCE[$i]}" + [ -z "$src" ] && src="non_file_source" + + log-error " at: ${func} ${src}:${lineno}" + done +} + +# @description Helper function see if $1 could be considered truthy +# returns [0] if input is truthy, otherwise [1] +# @arg $1 string The string to evaluate +# @see as-boolean +function is-true() +{ + as-boolean "${1:-}" && return 0 + + return 1 +} + +# @description Helper function see if $1 could be considered falsey +# returns [0] if input is falsey, otherwise [1] +# @arg $1 string The string to evaluate +# @see as-boolean +function is-false() +{ + as-boolean "${1:-}" && return 1 + + return 0 +} + +# @description Helper function see if $1 could be truethy or falsey. +# since this is a bash context, returning 0 is true and 1 is false +# so it works with [if is-false $input; then .... fi] +# +# This is a bit confusing, *especially* in a PHP world where [1] would be truthy and +# [0] would be falsely as return values +# @arg $1 string The string to evaluate +function as-boolean() +{ + local input="${1:-}" + local var="${input,,}" # convert input to lower-case + + case "$var" in + 1 | true) + return 0 + ;; + + 0 | false) + return 1 + ;; + + *) + log-warning "[as-boolean] variable [${var}] could not be detected as true or false, returning [1] (false) as default" + + return 1 + ;; + + esac +} diff --git a/docker/shared/root/docker/install/base.sh b/docker/shared/root/docker/install/base.sh new file mode 100755 index 000000000..a1a32a003 --- /dev/null +++ b/docker/shared/root/docker/install/base.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -ex -o errexit -o nounset -o pipefail + +# Ensure we keep apt cache around in a Docker environment +rm -f /etc/apt/apt.conf.d/docker-clean +echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache + +# Don't install recommended packages by default +echo 'APT::Install-Recommends "false";' >> /etc/apt/apt.conf + +# Don't install suggested packages by default +echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf + +declare -a packages=() + +# Standard packages +packages+=( + apt-utils + ca-certificates + curl + git + gnupg1 + gosu + locales + locales-all + moreutils + nano + procps + software-properties-common + unzip + wget + zip +) + +# Image Optimization +packages+=( + gifsicle + jpegoptim + optipng + pngquant +) + +# Video Processing +packages+=( + ffmpeg +) + +# Database +packages+=( + mariadb-client + postgresql-client +) + +readarray -d ' ' -t -O "${#packages[@]}" packages < <(echo -n "${APT_PACKAGES_EXTRA:-}") + +apt-get update +apt-get upgrade -y +apt-get install -y "${packages[@]}" + +locale-gen +update-locale diff --git a/docker/shared/root/docker/install/php-extensions.sh b/docker/shared/root/docker/install/php-extensions.sh new file mode 100755 index 000000000..222f2374d --- /dev/null +++ b/docker/shared/root/docker/install/php-extensions.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -ex -o errexit -o nounset -o pipefail + +declare -a pecl_extensions=() + +readarray -d ' ' -t pecl_extensions < <(echo -n "${PHP_PECL_EXTENSIONS:-}") +readarray -d ' ' -t -O "${#pecl_extensions[@]}" pecl_extensions < <(echo -n "${PHP_PECL_EXTENSIONS_EXTRA:-}") + +declare -a php_extensions=() +readarray -d ' ' -t php_extensions < <(echo -n "${PHP_EXTENSIONS:-}") +readarray -d ' ' -t -O "${#php_extensions[@]}" php_extensions < <(echo -n "${PHP_EXTENSIONS_EXTRA:-}") +readarray -d ' ' -t -O "${#php_extensions[@]}" php_extensions < <(echo -n "${PHP_EXTENSIONS_DATABASE:-}") + +# Optional script folks can copy into their image to do any [docker-php-ext-configure] work before the [docker-php-ext-install] +# this can also overwirte the [gd] configure above by simply running it again +declare -r custom_pre_configure_script="" +if [[ -e "${custom_pre_configure_script}" ]]; then + if [ ! -x "${custom_pre_configure_script}" ]; then + echo >&2 "ERROR: found ${custom_pre_configure_script} but its not executable - please [chmod +x] the file!" + exit 1 + fi + + "${custom_pre_configure_script}" +fi + +# PECL + PHP extensions +IPE_KEEP_SYSPKG_CACHE=1 install-php-extensions "${pecl_extensions[@]}" "${php_extensions[@]}" diff --git a/docker/shared/root/docker/templates/shared/proxy/conf.d/docker-pixelfed.conf b/docker/shared/root/docker/templates/shared/proxy/conf.d/docker-pixelfed.conf new file mode 100644 index 000000000..0b221e604 --- /dev/null +++ b/docker/shared/root/docker/templates/shared/proxy/conf.d/docker-pixelfed.conf @@ -0,0 +1,16 @@ +########################################################### +# DO NOT CHANGE +########################################################### +# This file is generated by the Pixelfed Docker setup, and +# will be rewritten on every container start +# +# You can put any [.conf] file in this directory +# (docker-compose-state/config/proxy/conf.d) and it will +# be loaded by nginx on startup. +# +# Run [docker compose exec proxy bash -c 'nginx -t && nginx -s reload'] +# to test your config and reload the proxy +# +# See: https://github.com/nginx-proxy/nginx-proxy/blob/main/docs/README.md#custom-nginx-configuration + +client_max_body_size {{ getenv "POST_MAX_SIZE" "61M" }}; diff --git a/contrib/docker/php.production.ini b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini similarity index 98% rename from contrib/docker/php.production.ini rename to docker/shared/root/docker/templates/usr/local/etc/php/php.ini index b84839ff5..130166e80 100644 --- a/contrib/docker/php.production.ini +++ b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini @@ -363,7 +363,7 @@ zend.enable_gc = On ; Allows to include or exclude arguments from stack traces generated for exceptions ; Default: Off -; In production, it is recommended to turn this setting on to prohibit the output +; In production, it is recommended to turn this setting on to prohibit the output ; of sensitive information in stack traces zend.exception_ignore_args = On @@ -376,7 +376,7 @@ zend.exception_ignore_args = On ; threat in any way, but it makes it possible to determine whether you use PHP ; on your server or not. ; http://php.net/expose-php -expose_php = On +expose_php = Off ;;;;;;;;;;;;;;;;;;; ; Resource Limits ; @@ -406,7 +406,7 @@ max_input_time = 60 ; Maximum amount of memory a script may consume (128MB) ; http://php.net/memory-limit -memory_limit = 128M +memory_limit = {{ getenv "DOCKER_APP_PHP_MEMORY_LIMIT" "128M" }} ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Error handling and logging ; @@ -462,7 +462,7 @@ memory_limit = 128M ; Development Value: E_ALL ; Production Value: E_ALL & ~E_DEPRECATED & ~E_STRICT ; http://php.net/error-reporting -error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT +error_reporting = {{ getenv "DOCKER_APP_PHP_ERROR_REPORTING" "E_ALL & ~E_DEPRECATED & ~E_STRICT" }} ; This directive controls whether or not and where PHP will output errors, ; notices and warnings too. Error output is very useful during development, but @@ -479,7 +479,7 @@ error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT ; Development Value: On ; Production Value: Off ; http://php.net/display-errors -display_errors = Off +display_errors = {{ getenv "DOCKER_APP_PHP_DISPLAY_ERRORS" "off" }} ; The display of errors which occur during PHP's startup sequence are handled ; separately from display_errors. We strongly recommend you set this to 'off' @@ -488,7 +488,7 @@ display_errors = Off ; Development Value: On ; Production Value: Off ; http://php.net/display-startup-errors -display_startup_errors = Off +display_startup_errors = {{ getenv "DOCKER_APP_PHP_DISPLAY_ERRORS" "off" }} ; Besides displaying errors, PHP can also log errors to locations such as a ; server-specific log, STDERR, or a location specified by the error_log @@ -570,8 +570,9 @@ report_memleaks = On ; Log errors to specified file. PHP's default behavior is to leave this value ; empty. ; http://php.net/error-log -; Example: -;error_log = php_errors.log +; +; NOTE: Write error log to stderr (/proc/self/fd/2 -> /dev/stderr) +error_log = /proc/self/fd/2 ; Log errors to syslog (Event Log on Windows). ;error_log = syslog @@ -679,7 +680,7 @@ auto_globals_jit = On ; Its value may be 0 to disable the limit. It is ignored if POST data reading ; is disabled through enable_post_data_reading. ; http://php.net/post-max-size -post_max_size = 64M +post_max_size = {{ getenv "POST_MAX_SIZE" "61M" }} ; Automatically add files before PHP document. ; http://php.net/auto-prepend-file @@ -831,10 +832,10 @@ file_uploads = On ; Maximum allowed size for uploaded files. ; http://php.net/upload-max-filesize -upload_max_filesize = 64M +upload_max_filesize = {{ getenv "POST_MAX_SIZE" "61M" }} ; Maximum number of files that can be uploaded via a single request -max_file_uploads = 20 +max_file_uploads = {{ getenv "MAX_ALBUM_LENGTH" "4" }} ;;;;;;;;;;;;;;;;;; ; Fopen wrappers ; @@ -947,7 +948,7 @@ cli_server.color = On [Date] ; Defines the default timezone used by the date functions ; http://php.net/date.timezone -;date.timezone = +date.timezone = {{ getenv "TZ" "UTC" }} ; http://php.net/date.default-latitude ;date.default_latitude = 31.7667 @@ -1735,7 +1736,7 @@ ldap.max_links = -1 [opcache] ; Determines if Zend OPCache is enabled -;opcache.enable=1 +opcache.enable={{ getenv "DOCKER_APP_PHP_OPCACHE_ENABLE" "1" }} ; Determines if Zend OPCache is enabled for the CLI version of PHP ;opcache.enable_cli=0 @@ -1761,12 +1762,12 @@ ldap.max_links = -1 ; When disabled, you must reset the OPcache manually or restart the ; webserver for changes to the filesystem to take effect. -;opcache.validate_timestamps=1 +opcache.validate_timestamps={{ getenv "DOCKER_APP_PHP_OPCACHE_VALIDATE_TIMESTAMPS" "0" }} ; How often (in seconds) to check file timestamps for changes to the shared ; memory storage allocation. ("1" means validate once per second, but only ; once per request. "0" means always validate) -;opcache.revalidate_freq=2 +opcache.revalidate_freq={{ getenv "DOCKER_APP_PHP_OPCACHE_REVALIDATE_FREQ" "2" }} ; Enables or disables file search in include_path optimization ;opcache.revalidate_path=0 diff --git a/docker/shared/root/shared/proxy/conf.d/.gitignore b/docker/shared/root/shared/proxy/conf.d/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docker/shell b/docker/shell new file mode 100755 index 000000000..7b725e1b0 --- /dev/null +++ b/docker/shell @@ -0,0 +1,17 @@ +#!/bin/bash + +declare service="${PF_SERVICE:=worker}" +declare user="${PF_USER:=www-data}" + +declare -a command=("bash") + +if [[ $# -ge 1 ]]; then + command=("$@") +fi + +exec docker compose exec \ + --user "${user}" \ + --env TERM \ + --env COLORTERM \ + "${service}" \ + "${command[@]}" diff --git a/funding.json b/funding.json new file mode 100644 index 000000000..507d8441e --- /dev/null +++ b/funding.json @@ -0,0 +1,68 @@ +{ + "version": "v1.0.0", + "entity": { + "type": "individual", + "role": "owner", + "name": "Daniel Supernault", + "email": "danielsupernault@gmail.com", + "phone": "", + "description": "I'm the developer behind Pixelfed, an open-source, federated photo-sharing platform that prioritizes privacy, community, and creativity. With a passion for building ethical alternatives to mainstream social networks, I have championed decentralized technologies and empowered users to share their stories without compromising their digital autonomy. As the driving force behind Pixelfed, I combine innovative development with a thoughtful approach to user experience, fostering an online space that feels personal, authentic, and inclusive.", + "webpageUrl": { + "url": "https://github.com/pixelfed/pixelfed" + } + }, + "projects": [ + { + "guid": "pixelfed", + "name": "Pixelfed", + "description": "Pixelfed is a free, open-source photo-sharing platform designed to put users in control of their content. Built on the principles of decentralization, Pixelfed offers a refreshing alternative to traditional social media, prioritizing privacy, community, and ethical design. Whether you're an artist, photographer, or someone who loves sharing moments, Pixelfed lets you connect and create without intrusive ads, algorithms, or data exploitation. Fully federated and part of the Fediverse, Pixelfed empowers users to join or host their own instances while still connecting with a global network of creatives and communities. It's not just a platform—it's a movement toward a better, more user-centered internet.", + "webpageUrl": { + "url": "https://github.com/pixelfed/pixelfed" + }, + "repositoryUrl": { + "url": "https://github.com/pixelfed/pixelfed" + }, + "licenses": ["spdx:AGPL-3.0"], + "tags": ["activitypub", "fediverse", "laravel", "pixelfed"] + } + ], + "funding": { + "channels": [ + { + "guid": "github-sponsors", + "type": "payment-provider", + "address": "https://github.com/sponsors/dansup", + "description": "Sponsor me through Github." + }, + { + "guid": "paypal-sponsors", + "type": "payment-provider", + "address": "https://www.paypal.com/paypalme/dansup", + "description": "Sponsor me through Paypal." + } + ], + "plans": [ + { + "guid": "developer-time", + "status": "active", + "name": "Developer compensation", + "description": "This will cover the cost of one developer working part-time on the projects.", + "amount": 0, + "currency": "USD", + "frequency": "monthly", + "channels": ["github-sponsors", "paypal-sponsors"] + }, + { + "guid": "support-plan", + "status": "active", + "name": "Support plan", + "description": "Pay anything you wish/can to show your support for the projects.", + "amount": 0, + "currency": "USD", + "frequency": "one-time", + "channels": ["github-sponsors", "paypal-sponsors"] + } + ], + "history": [] + } +} diff --git a/goss.yaml b/goss.yaml new file mode 100644 index 000000000..73f245c64 --- /dev/null +++ b/goss.yaml @@ -0,0 +1,123 @@ +# See: https://github.com/goss-org/goss/blob/master/docs/manual.md#goss-manual + +package: + curl: { installed: true } + ffmpeg: { installed: true } + gifsicle: { installed: true } + gosu: { installed: true } + jpegoptim: { installed: true } + locales-all: { installed: true } + locales: { installed: true } + mariadb-client: { installed: true } + nano: { installed: true } + optipng: { installed: true } + pngquant: { installed: true } + postgresql-client: { installed: true } + unzip: { installed: true } + wget: { installed: true } + zip: { installed: true } + +user: + www-data: + exists: true + uid: 33 + gid: 33 + groups: + - www-data + home: /var/www + shell: /usr/sbin/nologin + +command: + php-version: + exit-status: 0 + exec: 'php -v' + stdout: + - PHP {{ .Env.EXPECTED_PHP_VERSION }} + stderr: [] + + php-extensions: + exit-status: 0 + exec: 'php -m' + stdout: + - bcmath + - Core + - ctype + - curl + - date + - dom + - exif + - fileinfo + - filter + - gd + - hash + - iconv + - imagick + - intl + - json + - libxml + - mbstring + - mysqlnd + - openssl + - pcntl + - pcre + - PDO + - pdo_mysql + - pdo_pgsql + - pdo_sqlite + - Phar + - posix + - readline + - redis + - Reflection + - session + - SimpleXML + - sodium + - SPL + - sqlite3 + - standard + - tokenizer + - xml + - xmlreader + - xmlwriter + - zip + - zlib + stderr: [] + + forego-version: + exit-status: 0 + exec: 'forego version' + stdout: + - dev + stderr: [] + + gomplate-version: + exit-status: 0 + exec: 'gomplate -v' + stdout: + - gomplate version + stderr: [] + + gosu-version: + exit-status: 0 + exec: 'gosu -v' + stdout: + - '1.12' + stderr: [] + +{{ if eq .Env.PHP_BASE_TYPE "nginx" }} + nginx-version: + exit-status: 0 + exec: 'nginx -v' + stdout: [] + stderr: + - 'nginx version: nginx' +{{ end }} + +{{ if eq .Env.PHP_BASE_TYPE "apache" }} + apache-version: + exit-status: 0 + exec: 'apachectl -v' + stdout: + - 'Server version: Apache/' + stderr: [] +{{ end }} diff --git a/package-lock.json b/package-lock.json index 15443b198..2c64259da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "name": "pixelfed", "dependencies": { "@fancyapps/fancybox": "^3.5.7", + "@glidejs/glide": "^3.6.2", "@hcaptcha/vue-hcaptcha": "^1.3.0", "@peertube/p2p-media-loader-core": "^1.0.14", "@peertube/p2p-media-loader-hlsjs": "^1.0.14", @@ -20,7 +21,7 @@ "caniuse-lite": "^1.0.30001418", "chart.js": "^2.7.2", "filesize": "^3.6.1", - "hls.js": "^1.1.5", + "hls.js": "^1.5.13", "howler": "^2.2.0", "infinite-scroll": "^3.0.6", "jquery-scroll-lock": "^3.1.3", @@ -43,11 +44,12 @@ "vue-loading-overlay": "^3.3.3", "vue-timeago": "^5.1.2", "vue-tribute": "^1.0.7", + "webgl-media-editor": "^0.0.1", "zuck.js": "^1.6.0" }, "devDependencies": { "acorn": "^8.7.1", - "axios": "^0.21.1", + "axios": ">=1.6.0", "bootstrap": "^4.5.2", "cross-env": "^5.2.1", "jquery": "^3.6.0", @@ -71,108 +73,53 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", - "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", - "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -196,49 +143,39 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", - "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -255,18 +192,16 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", "semver": "^6.3.1" }, "engines": { @@ -285,12 +220,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -309,9 +244,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", @@ -323,69 +258,38 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", "dependencies": { - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -395,32 +299,32 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -430,13 +334,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -445,162 +349,74 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "dependencies": { + "@babel/types": "^7.26.3" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -608,12 +424,41 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -623,13 +468,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -638,6 +483,21 @@ "@babel/core": "^7.13.0" } }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-object-rest-spread": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", @@ -668,42 +528,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", @@ -715,23 +539,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -741,11 +554,11 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -754,61 +567,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", @@ -820,56 +578,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", @@ -886,11 +594,11 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -900,14 +608,13 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz", - "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", + "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -917,13 +624,13 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", "dependencies": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -933,11 +640,11 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", + "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -947,11 +654,11 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", - "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -961,12 +668,12 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -976,13 +683,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.11", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -992,18 +698,15 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", "globals": "^11.1.0" }, "engines": { @@ -1014,12 +717,12 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1029,11 +732,11 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", - "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1043,12 +746,12 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1058,11 +761,11 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1071,13 +774,27 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1087,12 +804,11 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1102,12 +818,11 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1117,11 +832,12 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1131,13 +847,13 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1147,12 +863,11 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1162,11 +877,11 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1176,12 +891,11 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1191,11 +905,11 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1205,12 +919,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", - "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1220,13 +934,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1236,14 +949,14 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", - "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1253,12 +966,12 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1268,12 +981,12 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1283,11 +996,11 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1297,12 +1010,11 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", + "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1312,12 +1024,11 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1327,15 +1038,13 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1345,12 +1054,12 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1360,12 +1069,11 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1375,13 +1083,12 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", - "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1391,11 +1098,11 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1405,12 +1112,12 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1420,14 +1127,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1437,11 +1143,11 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1451,11 +1157,11 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.25.9", "regenerator-transform": "^0.15.2" }, "engines": { @@ -1465,12 +1171,27 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1480,15 +1201,15 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.2.tgz", - "integrity": "sha512-XOntj6icgzMS58jPVtQpiuF6ZFWxQiJavISGx5KGjRj+3gqZr8+N6Kx+N9BApWzgS+DOjIZfXXj0ZesenOWDyA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz", + "integrity": "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "engines": { @@ -1507,11 +1228,11 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1521,12 +1242,12 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1536,11 +1257,11 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1550,11 +1271,11 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1564,11 +1285,11 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", + "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1578,11 +1299,11 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1592,12 +1313,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1607,12 +1328,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1622,12 +1343,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1637,89 +1358,78 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.2.tgz", - "integrity": "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", + "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", "dependencies": { - "@babel/compat-data": "^7.23.2", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/compat-data": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.23.2", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.23.0", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.23.0", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.23.0", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-modules-systemjs": "^7.23.0", - "@babel/plugin-transform-modules-umd": "^7.22.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.23.0", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.25.9", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.25.9", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.25.9", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.25.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.25.9", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.25.9", + "@babel/plugin-transform-typeof-symbol": "^7.25.9", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.23.0", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", - "core-js-compat": "^3.31.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", "semver": "^6.3.1" }, "engines": { @@ -1750,15 +1460,10 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1767,32 +1472,29 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1800,13 +1502,12 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1829,6 +1530,351 @@ "node": ">=10.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@fancyapps/fancybox": { "version": "3.5.7", "resolved": "https://registry.npmjs.org/@fancyapps/fancybox/-/fancybox-3.5.7.tgz", @@ -1837,6 +1883,11 @@ "jquery": ">=1.9.0" } }, + "node_modules/@glidejs/glide": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@glidejs/glide/-/glide-3.7.1.tgz", + "integrity": "sha512-8he0pZpLSqTesSFYiuWdhBmtdlYSPWxfPUCKtkkiX6ZmT8UdMdmoFPtJaOwLBXv4p2JiGxqbuPdiRGGo2e/htQ==" + }, "node_modules/@hcaptcha/vue-hcaptcha": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@hcaptcha/vue-hcaptcha/-/vue-hcaptcha-1.3.0.tgz", @@ -1846,61 +1897,61 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1951,27 +2002,349 @@ "npm": ">=5.0.0" } }, - "node_modules/@peertube/p2p-media-loader-core": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.14.tgz", - "integrity": "sha512-tjQv1CNziNY+zYzcL1h4q6AA2WuBUZnBIeVyjWR/EsO1EEC1VMdvPsL02cqYLz9yvIxgycjeTsWCm6XDqNgXRw==", + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, "dependencies": { - "bittorrent-tracker": "^9.19.0", - "debug": "^4.3.4", + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@peertube/p2p-media-loader-core": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.20.tgz", + "integrity": "sha512-t6yYFcBTqDZSp3U0HqOI9fJzxFgb2C4PoiRI4FPGd28baUbsilO1PQBRwQzvu6wt8zwjzOE8FBpzYa+1gv1Sqg==", + "dependencies": { + "bittorrent-tracker": "^11.1.0", + "debug": "^4.3.5", + "esbuild": "^0.21.5", "events": "^3.3.0", "sha.js": "^2.4.11", "simple-peer": "^9.11.1" } }, "node_modules/@peertube/p2p-media-loader-hlsjs": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.14.tgz", - "integrity": "sha512-ySUVgUvAFXCE5E94xxjfywQ8xzk3jy9UGVkgi5Oqq+QeY7uG+o7CZ+LsQ/RjXgWBD70tEnyyfADHtL+9FCnwyQ==", + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.20.tgz", + "integrity": "sha512-PZS9h+txV+BX3t5lsh5PZ0ZtOogPJv4GmheQ5etceQZHxRAx2UxcAchMBJsa/sQ5c4CSMsN61Megs9iZ3gWauQ==", "dependencies": { - "@peertube/p2p-media-loader-core": "^1.0.14", - "debug": "^4.3.4", + "@peertube/p2p-media-loader-core": "^1.0.20", + "debug": "^4.3.5", + "esbuild": "^0.21.5", "events": "^3.3.0", - "m3u8-parser": "^4.7.1" + "m3u8-parser": "~4.7.1" + } + }, + "node_modules/@thaunknown/simple-peer": { + "version": "10.0.11", + "resolved": "https://registry.npmjs.org/@thaunknown/simple-peer/-/simple-peer-10.0.11.tgz", + "integrity": "sha512-A5MdmtZ6HUzRa4gwPOS4jG+09HvpTv2rFo4kk7Vwveo2ELm+WmbO124ZrJrQnZc2D7z2Q3AWKSitjl9OKXO88g==", + "dependencies": { + "debug": "^4.3.7", + "err-code": "^3.0.1", + "streamx": "^2.20.1", + "uint8-util": "^2.2.5", + "webrtc-polyfill": "^1.1.10" + } + }, + "node_modules/@thaunknown/simple-websocket": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@thaunknown/simple-websocket/-/simple-websocket-9.1.3.tgz", + "integrity": "sha512-pf/FCJsgWtLJiJmIpiSI7acOZVq3bIQCpnNo222UFc8Ph1lOUOTpe6LoYhhiOSKB9GUaWJEVUtZ+sK1/aBgU5Q==", + "dependencies": { + "debug": "^4.3.5", + "queue-microtask": "^1.2.3", + "streamx": "^2.17.0", + "uint8-util": "^2.2.5", + "ws": "^8.17.1" } }, "node_modules/@trevoreyre/autocomplete-vue": { @@ -1988,9 +2361,9 @@ } }, "node_modules/@types/babel__core": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", - "integrity": "sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -2000,100 +2373,100 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.6.tgz", - "integrity": "sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.3.tgz", - "integrity": "sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.3.tgz", - "integrity": "sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/body-parser": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz", - "integrity": "sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==", + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "node_modules/@types/bonjour": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.12.tgz", - "integrity": "sha512-ky0kWSqXVxSqgqJvPIkgFkcn4C8MnRog308Ou8xBBIVo39OmUFy+jqNe0nPwLCDFxUpmT9EvT91YzOJgkDRcFg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/clean-css": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.9.tgz", - "integrity": "sha512-pjzJ4n5eAXAz/L5Zur4ZymuJUvyo0Uh0iRnRI/1kADFLs76skDky0K0dX1rlv4iXXrJXNk3sxRWVJR7CMDroWA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.11.tgz", + "integrity": "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==", "dependencies": { "@types/node": "*", "source-map": "^0.6.0" } }, "node_modules/@types/connect": { - "version": "3.4.37", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.37.tgz", - "integrity": "sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.2.tgz", - "integrity": "sha512-gX2j9x+NzSh4zOhnRPSdPPmTepS4DfxES0AvIFv3jGv5QyeAJf6u6dY5/BAoAJU9Qq1uTvwOku8SSC2GnCRl6Q==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, "node_modules/@types/eslint": { - "version": "8.44.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz", - "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "node_modules/@types/eslint-scope": { - "version": "3.7.6", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz", - "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "node_modules/@types/estree": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", - "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/express": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.20.tgz", - "integrity": "sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -2112,9 +2485,9 @@ } }, "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.17.39", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.39.tgz", - "integrity": "sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==", + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -2132,46 +2505,46 @@ } }, "node_modules/@types/http-errors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz", - "integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/http-proxy": { - "version": "1.17.13", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.13.tgz", - "integrity": "sha512-GkhdWcMNiR5QSQRYnJ+/oXzu0+7JJEPC8vkWXK351BkhjraZF+1W13CUYARUvX9+NqIU2n6YHA4iwywsc/M6Sw==", + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/imagemin": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.3.tgz", - "integrity": "sha512-se/hpaYxu5DyvPqmUEwbupmbQSx6JNislk0dkoIgWSmArkj+Ow9pGG9pGz8MRmbQDfGNYNzqwPQKHCUy+K+jpQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-9.0.0.tgz", + "integrity": "sha512-4IaT+BdPUAFf/AAy3XlFAbqGk4RawhdidxWO5XTe+PJAYAr4d7m2FHiqyEPXbDpwS+IaLIJq5AIjLE9HcwMGBg==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/imagemin-gifsicle": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/imagemin-gifsicle/-/imagemin-gifsicle-7.0.3.tgz", - "integrity": "sha512-GQBKOk9doOd0Xp7OvO4QDl7U0Vkwk2Ps7J0rxafdAa7wG9lu7idvZTm8TtSZiRtHENdkW88Kz8OjmjMlgeeC5w==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/imagemin-gifsicle/-/imagemin-gifsicle-7.0.4.tgz", + "integrity": "sha512-ZghMBd/Jgqg5utTJNPmvf6DkuHzMhscJ8vgf/7MUGCpO+G+cLrhYltL+5d+h3A1B4W73S2SrmJZ1jS5LACpX+A==", "dependencies": { "@types/imagemin": "*" } }, "node_modules/@types/imagemin-mozjpeg": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@types/imagemin-mozjpeg/-/imagemin-mozjpeg-8.0.3.tgz", - "integrity": "sha512-+U/ibETP2/oRqeuaaXa67dEpKHfzmfK0OBVC09AR4c1CIFAKjQ5xY+dxH+fjoMQRlwdcRQLkn/ALtnxSl3Xsqw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@types/imagemin-mozjpeg/-/imagemin-mozjpeg-8.0.4.tgz", + "integrity": "sha512-ZCAxV8SYJB8ehwHpnbRpHjg5Wc4HcyuAMiDhXbkgC7gujDoOTyHO3dhDkUtZ1oK1DLBRZapqG9etdLVhUml7yQ==", "dependencies": { "@types/imagemin": "*" } }, "node_modules/@types/imagemin-optipng": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/imagemin-optipng/-/imagemin-optipng-5.2.3.tgz", - "integrity": "sha512-Q80ANbJYn+WgKkWVfx9f7/q4LR6qun4NIiuV1eRWCg8KCAmNrU7ZH16a2hGs9kfkFqyJlhBv6oV9SDXe1vL3aQ==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@types/imagemin-optipng/-/imagemin-optipng-5.2.4.tgz", + "integrity": "sha512-mvKnDMC8eCYZetAQudjs1DbgpR84WhsTx1wgvdiXnpuUEti3oJ+MaMYBRWPY0JlQ4+y4TXKOfa7+LOuT8daegQ==", "dependencies": { "@types/imagemin": "*" } @@ -2186,14 +2559,14 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", - "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==" + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/mime": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.3.tgz", - "integrity": "sha512-i8MBln35l856k5iOhKk2XJ4SeAWg75mLIpZB4v6imOagKL6twsukBZGDMNhdOVk7yRFTMPpfILocMos59Q1otQ==" + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/minimatch": { "version": "5.1.2", @@ -2201,27 +2574,35 @@ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" }, "node_modules/@types/node": { - "version": "20.8.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", - "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dependencies": { - "undici-types": "~5.25.1" + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dependencies": { + "@types/node": "*" } }, "node_modules/@types/parse-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz", - "integrity": "sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/qs": { - "version": "6.9.9", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz", - "integrity": "sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==" + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==" }, "node_modules/@types/range-parser": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz", - "integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==" + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/retry": { "version": "0.12.0", @@ -2229,41 +2610,36 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, "node_modules/@types/send": { - "version": "0.17.3", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.3.tgz", - "integrity": "sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==", + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, - "node_modules/@types/send/node_modules/@types/mime": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.4.tgz", - "integrity": "sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==" - }, "node_modules/@types/serve-index": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.3.tgz", - "integrity": "sha512-4KG+yMEuvDPRrYq5fyVm/I2uqAJSAwZK9VSa+Zf+zUq9/oxSSvy3kkIqyL+jjStv6UCVi8/Aho0NHtB1Fwosrg==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.4.tgz", - "integrity": "sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dependencies": { "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" + "@types/node": "*", + "@types/send": "*" } }, "node_modules/@types/sockjs": { - "version": "0.3.35", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.35.tgz", - "integrity": "sha512-tIF57KB+ZvOBpAQwSaACfEu7htponHXaFzP7RfKYgsOS0NoYnn+9+jzp7bbq4fWerizI3dTB4NfAZoyeQKWJLw==", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dependencies": { "@types/node": "*" } @@ -2274,9 +2650,9 @@ "integrity": "sha512-AZU7vQcy/4WFEuwnwsNsJnFwupIpbllH1++LXScN6uxT1Z4zPzdrWG97w4/I7eFKFTvfy/bHFStWjdBAg2Vjug==" }, "node_modules/@types/ws": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz", - "integrity": "sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "dependencies": { "@types/node": "*" } @@ -2296,13 +2672,16 @@ } }, "node_modules/@vue/compiler-sfc": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", - "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", "dependencies": { - "@babel/parser": "^7.18.4", + "@babel/parser": "^7.23.5", "postcss": "^8.4.14", "source-map": "^0.6.1" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" } }, "node_modules/@vue/component-compiler-utils": { @@ -2369,133 +2748,133 @@ "integrity": "sha512-K1undnK70vLLauqdE8bq/l98isTF2FDhcP0UPpXVSjkSWe3xhAn5eRXk5jfA1E5ycNm84Ws/rQFUD7ue11nciw==" }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -2543,15 +2922,26 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, "node_modules/@zip.js/zip.js": { - "version": "2.7.30", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.30.tgz", - "integrity": "sha512-nhMvQCj+TF1ATBqYzFds7v+yxPBhdDYHh8J341KtC1D2UrVBUIYcYK4Jy1/GiTsxOXEiKOXSUxvPG/XR+7jMqw==", + "version": "2.7.54", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.54.tgz", + "integrity": "sha512-qMrJVg2hoEsZJjMJez9yI2+nZlBUxgYzGV3mqcb2B/6T1ihXp0fWBDYlVHlHquuorgNUQP5a8qSmX6HF5rFJNg==", "engines": { "bun": ">=0.7.0", "deno": ">=1.0.0", "node": ">=16.5.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2564,10 +2954,18 @@ "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "bin": { "acorn": "bin/acorn" }, @@ -2575,18 +2973,13 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/addr-to-ip-port": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/addr-to-ip-port/-/addr-to-ip-port-1.5.4.tgz", - "integrity": "sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/addr-to-ip-port/-/addr-to-ip-port-2.0.0.tgz", + "integrity": "sha512-9bYbtjamtdLHZSqVIUXhilOryNPiL+x+Q5J/Unpg4VY3ZIkK3fT52UoErj1NdUeVm3J1t2iBEAur4Ywbl/bahw==", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/adjust-sourcemap-loader": { "version": "4.0.0", @@ -2646,14 +3039,14 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2724,9 +3117,9 @@ } }, "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "node_modules/array-union": { "version": "2.1.0", @@ -2737,20 +3130,19 @@ } }, "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" + "minimalistic-assert": "^1.0.0" } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" }, "node_modules/assert": { "version": "1.5.1", @@ -2774,10 +3166,16 @@ "inherits": "2.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "funding": [ { "type": "opencollective", @@ -2793,11 +3191,11 @@ } ], "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -2811,26 +3209,33 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "dev": true, "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" + }, "node_modules/babel-helper-vue-jsx-merge-props": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz", "integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==" }, "node_modules/babel-loader": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", - "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz", + "integrity": "sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==", "dependencies": { "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", + "loader-utils": "^2.0.4", "make-dir": "^3.1.0", "schema-utils": "^2.6.5" }, @@ -2843,12 +3248,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.6.3", "semver": "^6.3.1" }, "peerDependencies": { @@ -2864,23 +3269,23 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.5.tgz", - "integrity": "sha512-Q6CdATeAvbScWPNLB8lzSO7fgUVBkQt6zLgNlfyeCr/EQaEQR+bWiBYYPYAFyE528BMjRhL+1QBMOI4jc/c5TA==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", - "core-js-compat": "^3.32.2" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -2891,6 +3296,20 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "optional": true + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2916,9 +3335,15 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" }, "node_modules/bencode": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/bencode/-/bencode-2.0.3.tgz", - "integrity": "sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bencode/-/bencode-4.0.0.tgz", + "integrity": "sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==", + "dependencies": { + "uint8-util": "^2.2.2" + }, + "engines": { + "node": ">=12.20.0" + } }, "node_modules/big.js": { "version": "5.2.2", @@ -2929,16 +3354,19 @@ } }, "node_modules/bigpicture": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/bigpicture/-/bigpicture-2.6.2.tgz", - "integrity": "sha512-IZmRDr7ZSJLDtDvOP/dfqvJhMBqV/tGQAl3UgvkaCzePIkHYlYV/uCg9349E4RPF3ng7QM+eqFZ2CY7tE6pLMA==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/bigpicture/-/bigpicture-2.6.3.tgz", + "integrity": "sha512-Zmnca4YpQn3iygBNSxiyILBVk/a0nvdn3BAmNyZ3KLG5aasDYg+f0oEBqsbohL2YPS5UPqrk3NZH2zXSSHDKIg==" }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bittorrent-peerid": { @@ -2961,9 +3389,9 @@ ] }, "node_modules/bittorrent-tracker": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/bittorrent-tracker/-/bittorrent-tracker-9.19.0.tgz", - "integrity": "sha512-09d0aD2b+MC+zWvWajkUAKkYMynYW4tMbTKiRSthKtJZbafzEoNQSUHyND24SoCe3ZOb2fKfa6fu2INAESL9wA==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/bittorrent-tracker/-/bittorrent-tracker-11.1.2.tgz", + "integrity": "sha512-mzINNIJ3FjNBcqQPKPQoxpNzzqEVfbBohwdVxblaOKGQVxagMzipCWwbxiAt+J35WC3xs1wFeCDUXpzQ3TBA+A==", "funding": [ { "type": "github", @@ -2979,39 +3407,83 @@ } ], "dependencies": { - "bencode": "^2.0.1", - "bittorrent-peerid": "^1.3.3", - "bn.js": "^5.2.0", + "@thaunknown/simple-peer": "^10.0.8", + "@thaunknown/simple-websocket": "^9.1.3", + "bencode": "^4.0.0", + "bittorrent-peerid": "^1.3.6", "chrome-dgram": "^3.0.6", - "clone": "^2.0.0", "compact2string": "^1.4.1", - "debug": "^4.1.1", - "ip": "^1.1.5", + "cross-fetch-ponyfill": "^1.0.3", + "debug": "^4.3.4", + "ip": "^2.0.1", "lru": "^3.1.0", - "minimist": "^1.2.5", + "minimist": "^1.2.8", "once": "^1.4.0", "queue-microtask": "^1.2.3", "random-iterate": "^1.0.1", - "randombytes": "^2.1.0", "run-parallel": "^1.2.0", "run-series": "^1.1.9", - "simple-get": "^4.0.0", - "simple-peer": "^9.11.0", - "simple-websocket": "^9.1.0", - "socks": "^2.0.0", - "string2compact": "^1.3.0", + "socks": "^2.8.3", + "string2compact": "^2.0.1", + "uint8-util": "^2.2.5", "unordered-array-remove": "^1.0.2", - "ws": "^7.4.5" + "ws": "^8.17.0" }, "bin": { "bittorrent-tracker": "bin/cmd.js" }, "engines": { - "node": ">=12" + "node": ">=16.0.0" }, "optionalDependencies": { - "bufferutil": "^4.0.3", - "utf-8-validate": "^5.0.5" + "bufferutil": "^4.0.8", + "utf-8-validate": "^6.0.4" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/bluebird": { @@ -3031,20 +3503,20 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", + "qs": "6.13.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -3053,14 +3525,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3075,11 +3539,11 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -3089,12 +3553,10 @@ } }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } @@ -3146,11 +3608,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3196,41 +3658,36 @@ } }, "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.5", + "hash-base": "~3.0", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 6" + "node": ">= 0.12" } }, "node_modules/browserify-zlib": { @@ -3242,9 +3699,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "funding": [ { "type": "opencollective", @@ -3260,10 +3717,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -3311,21 +3768,52 @@ "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==" }, "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "engines": { "node": ">= 0.8" } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3360,9 +3848,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001553", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz", - "integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "funding": [ { "type": "opencollective", @@ -3441,15 +3929,9 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -3462,10 +3944,18 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "node_modules/chrome-dgram": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/chrome-dgram/-/chrome-dgram-3.0.6.tgz", @@ -3490,26 +3980,29 @@ } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "engines": { "node": ">=6.0" } }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dependencies": { "source-map": "~0.6.0" }, @@ -3527,9 +4020,9 @@ } }, "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dependencies": { "string-width": "^4.2.0" }, @@ -3553,14 +4046,6 @@ "node": ">=12" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -3605,6 +4090,18 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -3638,16 +4135,16 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", + "negotiator": "~0.6.4", "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { @@ -3667,11 +4164,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "node_modules/concat": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/concat/-/concat-1.0.3.tgz", @@ -3757,9 +4249,9 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -3770,9 +4262,9 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "node_modules/core-js": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.1.tgz", - "integrity": "sha512-qVSq3s+d4+GsqN0teRCJtM6tdEEXyWxjzbhVrCHmBS5ZTM0FS2MOS0D13dUXAWDUN6a+lHI/N1hF9Ytz6iLl9Q==", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -3780,11 +4272,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.1.tgz", - "integrity": "sha512-6pYKNOgD/j/bkC5xS5IIg6bncid3rfrI42oBH1SQJbsmYPKF7rhzcFzYCcxYMmNQQ0rCEB8WqpW7QHndOggaeQ==", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", + "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.24.2" }, "funding": { "type": "opencollective", @@ -3821,9 +4313,9 @@ } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" }, "node_modules/create-hash": { "version": "1.2.0", @@ -3851,9 +4343,9 @@ } }, "node_modules/cropperjs": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.1.tgz", - "integrity": "sha512-F4wsi+XkDHCOMrHMYjrTEE4QBOrsHHN5/2VsVAaRq8P7E5z7xQpT75S+f/9WikmBEailas3+yo+6zPIomW+NOA==" + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz", + "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==" }, "node_modules/cross-env": { "version": "5.2.1", @@ -3871,10 +4363,36 @@ "node": ">=4.0" } }, + "node_modules/cross-fetch-ponyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cross-fetch-ponyfill/-/cross-fetch-ponyfill-1.0.3.tgz", + "integrity": "sha512-uOBkDhUAGAbx/FEzNKkOfx3w57H8xReBBXoZvUnOKTI0FW0Xvrj3GrYv2iZXUqlffC1LMGfQzhmBM/ke+6eTDA==", + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "^3.3.0" + } + }, + "node_modules/cross-fetch-ponyfill/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "dependencies": { "nice-try": "^1.0.4", @@ -3896,24 +4414,28 @@ } }, "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" }, "engines": { - "node": "*" + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/css-declaration-sorter": { @@ -3928,54 +4450,47 @@ } }, "node_modules/css-loader": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", - "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", "dev": true, "peer": true, "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.21", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.3", - "postcss-modules-scope": "^3.0.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/css-loader/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" }, - "engines": { - "node": ">=10" + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/css-loader/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "peer": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -3983,13 +4498,6 @@ "node": ">=10" } }, - "node_modules/css-loader/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "peer": true - }, "node_modules/css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", @@ -4138,15 +4646,23 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/custom-event-polyfill": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz", "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", @@ -4159,11 +4675,11 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -4188,6 +4704,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -4200,16 +4724,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -4255,6 +4782,15 @@ "node": ">=8" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4286,6 +4822,19 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -4302,9 +4851,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" }, "node_modules/dir-glob": { "version": "3.0.1", @@ -4317,11 +4866,6 @@ "node": ">=8" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -4448,20 +4992,33 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.563", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.563.tgz", - "integrity": "sha512-dg5gj5qOgfZNkPNeyKBZQAQitIQ/xwfIDmEQJHCbXaD9ebTZxwJXUsDYcBlAvZGZLi+/354l35J1wkmP6CqYaw==" + "version": "1.5.75", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", + "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==" }, "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -4473,9 +5030,9 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -4491,17 +5048,25 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -4519,9 +5084,9 @@ } }, "node_modules/envinfo": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", - "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "bin": { "envinfo": "dist/cli.js" }, @@ -4542,20 +5107,84 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", - "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==" + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/es6-object-assign": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "engines": { "node": ">=6" } @@ -4565,14 +5194,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -4633,6 +5254,14 @@ "resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz", "integrity": "sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q==" }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -4678,9 +5307,9 @@ } }, "node_modules/execa/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4731,37 +5360,45 @@ "node": ">= 8" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -4770,13 +5407,12 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -4791,11 +5427,11 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -4809,10 +5445,15 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -4829,6 +5470,11 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -4838,9 +5484,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dependencies": { "reusify": "^1.0.4" } @@ -4856,6 +5502,28 @@ "node": ">=0.8.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", @@ -4909,9 +5577,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4920,12 +5588,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -4994,9 +5662,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", @@ -5012,6 +5680,31 @@ } } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5040,6 +5733,11 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -5054,9 +5752,9 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", - "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==" }, "node_modules/fs.realpath": { "version": "1.0.0", @@ -5106,14 +5804,23 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5130,10 +5837,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5201,11 +5920,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5235,31 +5954,20 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "engines": { - "node": ">= 0.4" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -5268,29 +5976,15 @@ } }, "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", "dependencies": { "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "safe-buffer": "^5.2.1" }, "engines": { - "node": ">=4" - } - }, - "node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "node": ">= 0.10" } }, "node_modules/hash-sum": { @@ -5308,9 +6002,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -5327,9 +6021,9 @@ } }, "node_modules/hls.js": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.4.12.tgz", - "integrity": "sha512-1RBpx2VihibzE3WE9kGoVCtrhhDWTzydzElk/kyRbEOLnb1WIE+3ZabM/L8BqKFTCL3pUy4QzhXgD1Q6Igr1JA==" + "version": "1.5.18", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.18.tgz", + "integrity": "sha512-znxR+2jecWluu/0KOBqUcvVyAB5tLff10vjMGrpAlz1eFY+ZhF1bY3r82V+Bk7WJdk03iTjtja9KFFz5BrqjSA==" }, "node_modules/hmac-drbg": { "version": "1.0.1", @@ -5358,9 +6052,9 @@ } }, "node_modules/html-entities": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", - "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", "funding": [ { "type": "github", @@ -5520,9 +6214,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -5597,9 +6291,9 @@ ] }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "engines": { "node": ">= 4" } @@ -5660,9 +6354,9 @@ } }, "node_modules/immutable": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", - "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", "dev": true }, "node_modules/import-fresh": { @@ -5681,9 +6375,9 @@ } }, "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -5720,6 +6414,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -5730,6 +6425,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -5739,14 +6439,26 @@ } }, "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } }, "node_modules/ipaddr.js": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", - "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "engines": { "node": ">= 10" } @@ -5773,11 +6485,14 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", + "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5964,15 +6679,20 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-parse-even-better-errors": { @@ -6032,9 +6752,9 @@ } }, "node_modules/laravel-echo": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.15.3.tgz", - "integrity": "sha512-SRXzccaat6w4qKgZ4/rjFKr3nJfVxB+ly4V0MEJNIF1/TpERNXepo3uk7NnOjBGsiV/np1fl2XitAzW4Sa1s/w==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.17.1.tgz", + "integrity": "sha512-ORWc4vDfnBj/Oe5ThZ5kYyGItRjLDqAQUyhD/7UhehUOqc+s5x9HEBjtMVludNMP6VuXw6t7Uxt8bp63kaTofg==", "dev": true, "engines": { "node": ">=10" @@ -6151,17 +6871,6 @@ "webpack": "^4.27.0 || ^5.0.0" } }, - "node_modules/laravel-mix/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/laravel-mix/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -6180,12 +6889,9 @@ } }, "node_modules/laravel-mix/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -6193,15 +6899,10 @@ "node": ">=10" } }, - "node_modules/laravel-mix/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/launch-editor": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", - "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dependencies": { "picocolors": "^1.0.0", "shell-quote": "^1.8.1" @@ -6242,9 +6943,9 @@ } }, "node_modules/loadjs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loadjs/-/loadjs-4.2.0.tgz", - "integrity": "sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loadjs/-/loadjs-4.3.0.tgz", + "integrity": "sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==" }, "node_modules/locate-path": { "version": "5.0.0", @@ -6305,9 +7006,9 @@ } }, "node_modules/m3u8-parser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.8.0.tgz", - "integrity": "sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.1.tgz", + "integrity": "sha512-pbrQwiMiq+MmI9bl7UjtPT3AK603PV9bogNlr83uC+X9IoxqL5E4k7kU7fMQ0dpRgxgeSMygqUa0IMLQNXLBNA==", "dependencies": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "^3.0.5", @@ -6336,6 +7037,14 @@ "semver": "bin/semver.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -6381,9 +7090,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-source-map": { "version": "1.1.0", @@ -6416,11 +7128,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -6440,9 +7152,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" }, "node_modules/mime": { "version": "1.6.0", @@ -6567,18 +7279,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "engines": { "node": "*" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -6593,9 +7310,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -6609,10 +7326,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "engines": { "node": ">= 0.6" } @@ -6637,6 +7359,84 @@ "tslib": "^2.0.3" } }, + "node_modules/node-abi": { + "version": "3.71.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", + "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true + }, + "node_modules/node-datachannel": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.12.0.tgz", + "integrity": "sha512-pZ9FsVZpHdUKqyWynuCc9IBLkZPJMpDzpNk4YNPCizbIXHYifpYeWqSF35REHGIWi9JMCf11QzapsyQGo/Y4Ig==", + "hasInstallScript": true, + "dependencies": { + "node-domexception": "^2.0.1", + "prebuild-install": "^7.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/node-datachannel/node_modules/node-domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-2.0.1.tgz", + "integrity": "sha512-M85rnSC7WQ7wnfQTARPT4LrK7nwCHLdDFOCcItZMhTQjyCebJH8GciKqYJNgaOFZs9nFmTmd/VMyi3OW5jA47w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -6665,9 +7465,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", - "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "optional": true, "bin": { "node-gyp-build": "bin.js", @@ -6718,24 +7518,10 @@ "which": "^2.0.2" } }, - "node_modules/node-notifier/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-notifier/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -6757,15 +7543,10 @@ "node": ">= 8" } }, - "node_modules/node-notifier/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -6825,9 +7606,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6841,13 +7625,15 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -7018,15 +7804,19 @@ } }, "node_modules/parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/parse-json": { @@ -7099,9 +7889,9 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/path-type": { "version": "4.0.0", @@ -7127,9 +7917,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -7184,9 +7974,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -7202,9 +7992,9 @@ } ], "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -7347,24 +8137,10 @@ "webpack": "^5.0.0" } }, - "node_modules/postcss-loader/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/postcss-loader/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -7372,11 +8148,6 @@ "node": ">=10" } }, - "node_modules/postcss-loader/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/postcss-merge-longhand": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", @@ -7470,9 +8241,9 @@ } }, "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -7481,12 +8252,12 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -7496,12 +8267,24 @@ "postcss": "^8.1.0" } }, - "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dependencies": { + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -7510,6 +8293,18 @@ "postcss": "^8.1.0" } }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-modules-values": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", @@ -7694,9 +8489,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -7739,11 +8534,43 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, "optional": true, "bin": { "prettier": "bin-prettier.js" @@ -7801,6 +8628,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -7821,9 +8654,18 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, "node_modules/punycode": { "version": "1.4.1", @@ -7848,11 +8690,11 @@ "dev": true }, "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -7888,6 +8730,11 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, "node_modules/random-iterate": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/random-iterate/-/random-iterate-1.0.1.tgz", @@ -7924,9 +8771,9 @@ "integrity": "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==" }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -7937,12 +8784,18 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" } }, "node_modules/readable-stream": { @@ -8008,9 +8861,9 @@ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dependencies": { "regenerate": "^1.4.2" }, @@ -8019,9 +8872,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -8032,20 +8885,20 @@ } }, "node_modules/regex-parser": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", "dev": true }, "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dependencies": { - "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -8053,23 +8906,31 @@ "node": ">=4" } }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==" + }, "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", "dependencies": { - "jsesc": "~0.5.0" + "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "bin": { "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/relateurl": { @@ -8110,17 +8971,20 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8195,6 +9059,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dependencies": { "glob": "^7.1.3" }, @@ -8280,13 +9145,13 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.69.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.4.tgz", - "integrity": "sha512-+qEreVhqAy8o++aQfCJwp0sklr2xyEzkm9Pp/Igu9wNPoe7EZEQ8X/MBvvXggI2ql607cxKg/RKOwDj6pp2XDA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.0.tgz", + "integrity": "sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==", "dev": true, "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", + "chokidar": "^4.0.0", + "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -8294,6 +9159,9 @@ }, "engines": { "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, "node_modules/sass-loader": { @@ -8334,6 +9202,34 @@ } } }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -8357,10 +9253,11 @@ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" }, "node_modules/selfsigned": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -8377,9 +9274,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -8412,15 +9309,18 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dependencies": { "randombytes": "^2.1.0" } @@ -8496,28 +9396,30 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8578,9 +9480,12 @@ } }, "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8591,13 +9496,68 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8715,45 +9675,6 @@ "node": ">= 6" } }, - "node_modules/simple-websocket": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/simple-websocket/-/simple-websocket-9.1.0.tgz", - "integrity": "sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "debug": "^4.3.1", - "queue-microtask": "^1.2.2", - "randombytes": "^2.1.0", - "readable-stream": "^3.6.0", - "ws": "^7.4.2" - } - }, - "node_modules/simple-websocket/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -8782,23 +9703,18 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" - }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -8813,9 +9729,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -8870,6 +9786,11 @@ "node": ">= 6" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -8885,9 +9806,9 @@ } }, "node_modules/std-env": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", - "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==" + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==" }, "node_modules/stream-browserify": { "version": "2.0.2", @@ -8910,6 +9831,19 @@ "xtend": "^4.0.0" } }, + "node_modules/streamx": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", + "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8932,12 +9866,15 @@ } }, "node_modules/string2compact": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/string2compact/-/string2compact-1.3.2.tgz", - "integrity": "sha512-3XUxUgwhj7Eqh2djae35QHZZT4mN3fsO7kagZhSGmhhlrQagVvWSFuuFIWnpxFS0CdTB2PlQcaL16RDi14I8uw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string2compact/-/string2compact-2.0.1.tgz", + "integrity": "sha512-Bm/T8lHMTRXw+u83LE+OW7fXmC/wM+Mbccfdo533ajSBNxddDHlRrvxE49NdciGHgXkUQM5WYskJ7uTkbBUI0A==", "dependencies": { - "addr-to-ip-port": "^1.0.1", + "addr-to-ip-port": "^2.0.0", "ipaddr.js": "^2.0.0" + }, + "engines": { + "node": ">=12.20.0" } }, "node_modules/strip-ansi": { @@ -8959,6 +9896,14 @@ "node": ">=6" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/style-loader": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", @@ -9074,10 +10019,49 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/terser": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.22.0.tgz", - "integrity": "sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -9092,15 +10076,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -9124,14 +10108,46 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 10.13.0" @@ -9146,6 +10162,23 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -9167,14 +10200,6 @@ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9205,21 +10230,38 @@ "integrity": "sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==" }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "dev": true }, + "node_modules/twgl.js": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/twgl.js/-/twgl.js-5.5.4.tgz", + "integrity": "sha512-6kFOmijOpmblTN9CCwOTCxK4lPg7rCyQjLuub6EMOlEp89Ex6yUcsMjsmH7andNPL2NE3XmHdqHeP5gVKKPhxw==", + "license": "MIT" + }, "node_modules/twitter-text": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/twitter-text/-/twitter-text-2.0.5.tgz", @@ -9240,15 +10282,23 @@ "node": ">= 0.6" } }, + "node_modules/uint8-util": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/uint8-util/-/uint8-util-2.2.5.tgz", + "integrity": "sha512-/QxVQD7CttWpVUKVPz9znO+3Dd4BdTSnFQ7pv/4drVhC9m4BaL2LFHTkJn6EsYoxT79VDq/2Gg8L0H22PrzyMw==", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/undici-types": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", - "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" }, "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "engines": { "node": ">=4" } @@ -9266,9 +10316,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "engines": { "node": ">=4" } @@ -9282,9 +10332,9 @@ } }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "engines": { "node": ">= 10.0.0" } @@ -9303,9 +10353,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "funding": [ { "type": "opencollective", @@ -9321,8 +10371,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -9340,20 +10390,23 @@ } }, "node_modules/uri-js/node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "engines": { "node": ">=6" } }, "node_modules/url": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", - "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", "dependencies": { "punycode": "^1.4.1", - "qs": "^6.11.2" + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/url-polyfill": { @@ -9367,9 +10420,9 @@ "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" }, "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.5.tgz", + "integrity": "sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==", "hasInstallScript": true, "optional": true, "dependencies": { @@ -9427,11 +10480,12 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, "node_modules/vue": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz", - "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", "dependencies": { - "@vue/compiler-sfc": "2.7.14", + "@vue/compiler-sfc": "2.7.16", "csstype": "^3.1.0" } }, @@ -9617,9 +10671,9 @@ } }, "node_modules/vue-template-compiler": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", - "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", "dev": true, "dependencies": { "de-indent": "^1.0.2", @@ -9668,9 +10722,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -9687,39 +10741,58 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webgl-media-editor": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/webgl-media-editor/-/webgl-media-editor-0.0.1.tgz", + "integrity": "sha512-TxnuRl3rpWa1Cia/pn+vh+0iz3yDNwzsrnRGJ61YkdZAYuimu2afBivSHv0RK73hKza6Y/YoRCkuEcsFmtxPNw==", + "license": "AGPL-3.0-only", + "dependencies": { + "cropperjs": "^1.6.2", + "gl-matrix": "^3.4.3", + "throttle-debounce": "^5.0.2", + "twgl.js": "^5.5.4" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -9785,9 +10858,9 @@ } }, "node_modules/webpack-cli/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -9839,9 +10912,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dependencies": { "colorette": "^2.0.10", "memfs": "^3.4.3", @@ -9861,14 +10934,14 @@ } }, "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -9892,9 +10965,9 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9902,7 +10975,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -9910,9 +10983,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", - "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -9942,7 +11015,7 @@ "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", + "webpack-dev-middleware": "^5.3.4", "ws": "^8.13.0" }, "bin": { @@ -9968,14 +11041,14 @@ } }, "node_modules/webpack-dev-server/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -9999,9 +11072,9 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -10009,33 +11082,13 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" } }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-merge": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", @@ -10117,6 +11170,18 @@ "webpack": "3 || 4 || 5" } }, + "node_modules/webrtc-polyfill": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/webrtc-polyfill/-/webrtc-polyfill-1.1.10.tgz", + "integrity": "sha512-sOn0bj3/noUdzQX7rvk0jFbBurqWDGGo2ipl+WfgoOe/x3cxbGLk/ZUY+WHCISSlLaIeBumi1X3wxQZnUESExQ==", + "dependencies": { + "node-datachannel": "^v0.12.0", + "node-domexception": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -10186,15 +11251,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { diff --git a/package.json b/package.json index 6598175d9..691972ced 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "devDependencies": { "acorn": "^8.7.1", - "axios": "^0.21.1", + "axios": ">=1.6.0", "bootstrap": "^4.5.2", "cross-env": "^5.2.1", "jquery": "^3.6.0", @@ -34,6 +34,7 @@ }, "dependencies": { "@fancyapps/fancybox": "^3.5.7", + "@glidejs/glide": "^3.6.2", "@hcaptcha/vue-hcaptcha": "^1.3.0", "@peertube/p2p-media-loader-core": "^1.0.14", "@peertube/p2p-media-loader-hlsjs": "^1.0.14", @@ -47,7 +48,7 @@ "caniuse-lite": "^1.0.30001418", "chart.js": "^2.7.2", "filesize": "^3.6.1", - "hls.js": "^1.1.5", + "hls.js": "^1.5.13", "howler": "^2.2.0", "infinite-scroll": "^3.0.6", "jquery-scroll-lock": "^3.1.3", @@ -70,6 +71,7 @@ "vue-loading-overlay": "^3.3.3", "vue-timeago": "^5.1.2", "vue-tribute": "^1.0.7", + "webgl-media-editor": "^0.0.1", "zuck.js": "^1.6.0" }, "collective": { diff --git a/public/css/admin.css b/public/css/admin.css index 658de1dbc..54df3aa46 100644 Binary files a/public/css/admin.css and b/public/css/admin.css differ diff --git a/public/css/app.css b/public/css/app.css index 0c96787bd..8b93403a2 100644 Binary files a/public/css/app.css and b/public/css/app.css differ diff --git a/public/css/appdark.css b/public/css/appdark.css index 414d666f8..9cd24c41c 100644 Binary files a/public/css/appdark.css and b/public/css/appdark.css differ diff --git a/public/css/landing.css b/public/css/landing.css index 152a6a78f..bfec69d9f 100644 Binary files a/public/css/landing.css and b/public/css/landing.css differ diff --git a/public/css/profile.css b/public/css/profile.css new file mode 100644 index 000000000..4f5b6ef16 Binary files /dev/null and b/public/css/profile.css differ diff --git a/public/css/spa.css b/public/css/spa.css index fd4124d27..9847ed198 100644 Binary files a/public/css/spa.css and b/public/css/spa.css differ diff --git a/public/embed.js b/public/embed.js index 5acc20efe..ab7f9a539 100644 Binary files a/public/embed.js and b/public/embed.js differ diff --git a/public/js/account-import.js b/public/js/account-import.js index 20ca89318..bf2e40b37 100644 Binary files a/public/js/account-import.js and b/public/js/account-import.js differ diff --git a/public/js/activity.js b/public/js/activity.js index 6d643b050..d32596a9a 100644 Binary files a/public/js/activity.js and b/public/js/activity.js differ diff --git a/public/js/admin.js b/public/js/admin.js index d71cbfa94..1a7357d38 100644 Binary files a/public/js/admin.js and b/public/js/admin.js differ diff --git a/public/js/admin_invite.js b/public/js/admin_invite.js index 071d24aca..2f232337f 100644 Binary files a/public/js/admin_invite.js and b/public/js/admin_invite.js differ diff --git a/public/js/app.js b/public/js/app.js index 86bae4e30..c01f7a131 100644 Binary files a/public/js/app.js and b/public/js/app.js differ diff --git a/public/js/changelog.bundle.742a06ba0a547120.js b/public/js/changelog.bundle.742a06ba0a547120.js deleted file mode 100644 index 3e0be8d49..000000000 Binary files a/public/js/changelog.bundle.742a06ba0a547120.js and /dev/null differ diff --git a/public/js/changelog.bundle.7fc2ee6c4475458c.js b/public/js/changelog.bundle.7fc2ee6c4475458c.js new file mode 100644 index 000000000..822d2599b Binary files /dev/null and b/public/js/changelog.bundle.7fc2ee6c4475458c.js differ diff --git a/public/js/collectioncompose.js b/public/js/collectioncompose.js index e295e7965..ae3e62289 100644 Binary files a/public/js/collectioncompose.js and b/public/js/collectioncompose.js differ diff --git a/public/js/collections.js b/public/js/collections.js index 73ebdde48..45efad196 100644 Binary files a/public/js/collections.js and b/public/js/collections.js differ diff --git a/public/js/components.js b/public/js/components.js index 4604aedf5..b5eec688d 100644 Binary files a/public/js/components.js and b/public/js/components.js differ diff --git a/public/js/compose-classic.js b/public/js/compose-classic.js index 19a9c3f2e..dc2236620 100644 Binary files a/public/js/compose-classic.js and b/public/js/compose-classic.js differ diff --git a/public/js/compose.chunk.965eab35620423e5.js b/public/js/compose.chunk.965eab35620423e5.js deleted file mode 100644 index 787f7dd6b..000000000 Binary files a/public/js/compose.chunk.965eab35620423e5.js and /dev/null differ diff --git a/public/js/compose.chunk.e1f297b242137d23.js b/public/js/compose.chunk.e1f297b242137d23.js new file mode 100644 index 000000000..91ed42f8b Binary files /dev/null and b/public/js/compose.chunk.e1f297b242137d23.js differ diff --git a/public/js/home.chunk.351f55e9d09b6482.js.LICENSE.txt b/public/js/compose.chunk.e1f297b242137d23.js.LICENSE.txt similarity index 100% rename from public/js/home.chunk.351f55e9d09b6482.js.LICENSE.txt rename to public/js/compose.chunk.e1f297b242137d23.js.LICENSE.txt diff --git a/public/js/compose.js b/public/js/compose.js index 242f07420..358d724f3 100644 Binary files a/public/js/compose.js and b/public/js/compose.js differ diff --git a/public/js/post.chunk.74f8b1d1954f5d01.js.LICENSE.txt b/public/js/compose.js.LICENSE.txt similarity index 100% rename from public/js/post.chunk.74f8b1d1954f5d01.js.LICENSE.txt rename to public/js/compose.js.LICENSE.txt diff --git a/public/js/daci.chunk.3ed914c15dec4ff4.js b/public/js/daci.chunk.3ed914c15dec4ff4.js new file mode 100644 index 000000000..afa171eaf Binary files /dev/null and b/public/js/daci.chunk.3ed914c15dec4ff4.js differ diff --git a/public/js/daci.chunk.b17a0b11877389d7.js b/public/js/daci.chunk.b17a0b11877389d7.js deleted file mode 100644 index ccbc67f41..000000000 Binary files a/public/js/daci.chunk.b17a0b11877389d7.js and /dev/null differ diff --git a/public/js/developers.js b/public/js/developers.js index b76d00957..d4f9859b0 100644 Binary files a/public/js/developers.js and b/public/js/developers.js differ diff --git a/public/js/direct.js b/public/js/direct.js index 704d14c08..a9fd7e0ac 100644 Binary files a/public/js/direct.js and b/public/js/direct.js differ diff --git a/public/js/discover.chunk.2986d7e977f5188a.js b/public/js/discover.chunk.2986d7e977f5188a.js new file mode 100644 index 000000000..998af561b Binary files /dev/null and b/public/js/discover.chunk.2986d7e977f5188a.js differ diff --git a/public/js/discover.chunk.9606885dad3c8a99.js b/public/js/discover.chunk.9606885dad3c8a99.js deleted file mode 100644 index 7892b83f2..000000000 Binary files a/public/js/discover.chunk.9606885dad3c8a99.js and /dev/null differ diff --git a/public/js/discover.js b/public/js/discover.js index 4102b71f7..35f72887a 100644 Binary files a/public/js/discover.js and b/public/js/discover.js differ diff --git a/public/js/discover~findfriends.chunk.02be60ab26503531.js b/public/js/discover~findfriends.chunk.02be60ab26503531.js deleted file mode 100644 index 60d93a0a6..000000000 Binary files a/public/js/discover~findfriends.chunk.02be60ab26503531.js and /dev/null differ diff --git a/public/js/discover~findfriends.chunk.84758c764668a02c.js b/public/js/discover~findfriends.chunk.84758c764668a02c.js new file mode 100644 index 000000000..412cd73a4 Binary files /dev/null and b/public/js/discover~findfriends.chunk.84758c764668a02c.js differ diff --git a/public/js/discover~hashtag.bundle.9cfffc517f35044e.js b/public/js/discover~hashtag.bundle.9cfffc517f35044e.js deleted file mode 100644 index afb2b3de0..000000000 Binary files a/public/js/discover~hashtag.bundle.9cfffc517f35044e.js and /dev/null differ diff --git a/public/js/discover~hashtag.bundle.db1d86f9e9dcb79a.js b/public/js/discover~hashtag.bundle.db1d86f9e9dcb79a.js new file mode 100644 index 000000000..b6c51b20b Binary files /dev/null and b/public/js/discover~hashtag.bundle.db1d86f9e9dcb79a.js differ diff --git a/public/js/discover~memories.chunk.3b45432a80b08e9b.js b/public/js/discover~memories.chunk.3b45432a80b08e9b.js new file mode 100644 index 000000000..1d8d83814 Binary files /dev/null and b/public/js/discover~memories.chunk.3b45432a80b08e9b.js differ diff --git a/public/js/discover~memories.chunk.ce9cc6446020e9b3.js b/public/js/discover~memories.chunk.ce9cc6446020e9b3.js deleted file mode 100644 index ec86c87f7..000000000 Binary files a/public/js/discover~memories.chunk.ce9cc6446020e9b3.js and /dev/null differ diff --git a/public/js/discover~myhashtags.chunk.67fd16950ee21ad8.js b/public/js/discover~myhashtags.chunk.67fd16950ee21ad8.js new file mode 100644 index 000000000..e91438ebd Binary files /dev/null and b/public/js/discover~myhashtags.chunk.67fd16950ee21ad8.js differ diff --git a/public/js/discover~myhashtags.chunk.6eab2414b2b16e19.js b/public/js/discover~myhashtags.chunk.6eab2414b2b16e19.js deleted file mode 100644 index 31039621f..000000000 Binary files a/public/js/discover~myhashtags.chunk.6eab2414b2b16e19.js and /dev/null differ diff --git a/public/js/discover~serverfeed.chunk.0f2dcc473fdce17e.js b/public/js/discover~serverfeed.chunk.0f2dcc473fdce17e.js deleted file mode 100644 index ed30efc2f..000000000 Binary files a/public/js/discover~serverfeed.chunk.0f2dcc473fdce17e.js and /dev/null differ diff --git a/public/js/discover~serverfeed.chunk.93bc564867eaa7c3.js b/public/js/discover~serverfeed.chunk.93bc564867eaa7c3.js new file mode 100644 index 000000000..59f1826ce Binary files /dev/null and b/public/js/discover~serverfeed.chunk.93bc564867eaa7c3.js differ diff --git a/public/js/discover~settings.chunk.732c1f76a00d9204.js b/public/js/discover~settings.chunk.732c1f76a00d9204.js deleted file mode 100644 index 959ab4d3f..000000000 Binary files a/public/js/discover~settings.chunk.732c1f76a00d9204.js and /dev/null differ diff --git a/public/js/discover~settings.chunk.950c11c918a541b0.js b/public/js/discover~settings.chunk.950c11c918a541b0.js new file mode 100644 index 000000000..620fcb701 Binary files /dev/null and b/public/js/discover~settings.chunk.950c11c918a541b0.js differ diff --git a/public/js/dms.chunk.53a951c5de2d95ac.js b/public/js/dms.chunk.53a951c5de2d95ac.js deleted file mode 100644 index 0ee8967ee..000000000 Binary files a/public/js/dms.chunk.53a951c5de2d95ac.js and /dev/null differ diff --git a/public/js/dms.chunk.b7e970fb49da0199.js b/public/js/dms.chunk.b7e970fb49da0199.js new file mode 100644 index 000000000..4845baf6d Binary files /dev/null and b/public/js/dms.chunk.b7e970fb49da0199.js differ diff --git a/public/js/dms~message.chunk.011f31232754f650.js b/public/js/dms~message.chunk.011f31232754f650.js new file mode 100644 index 000000000..86f49696e Binary files /dev/null and b/public/js/dms~message.chunk.011f31232754f650.js differ diff --git a/public/js/dms~message.chunk.15157ff4a6c17cc7.js b/public/js/dms~message.chunk.15157ff4a6c17cc7.js deleted file mode 100644 index e74ff4325..000000000 Binary files a/public/js/dms~message.chunk.15157ff4a6c17cc7.js and /dev/null differ diff --git a/public/js/error404.bundle.3bbc118159460db6.js b/public/js/error404.bundle.3bbc118159460db6.js deleted file mode 100644 index 93b6b5acd..000000000 Binary files a/public/js/error404.bundle.3bbc118159460db6.js and /dev/null differ diff --git a/public/js/error404.bundle.ad885ef6f9b2c101.js b/public/js/error404.bundle.ad885ef6f9b2c101.js new file mode 100644 index 000000000..904132756 Binary files /dev/null and b/public/js/error404.bundle.ad885ef6f9b2c101.js differ diff --git a/public/js/group-status.js b/public/js/group-status.js new file mode 100644 index 000000000..106c271ed Binary files /dev/null and b/public/js/group-status.js differ diff --git a/public/js/group-topic-feed.js b/public/js/group-topic-feed.js new file mode 100644 index 000000000..e7ca72a39 Binary files /dev/null and b/public/js/group-topic-feed.js differ diff --git a/public/js/group.create.0d645a1de271e28d.js b/public/js/group.create.0d645a1de271e28d.js new file mode 100644 index 000000000..0c2274c61 Binary files /dev/null and b/public/js/group.create.0d645a1de271e28d.js differ diff --git a/public/js/groups-page-about.06576420562628e3.js b/public/js/groups-page-about.06576420562628e3.js new file mode 100644 index 000000000..551db02c7 Binary files /dev/null and b/public/js/groups-page-about.06576420562628e3.js differ diff --git a/public/js/groups-page-media.f611a51e684c48ef.js b/public/js/groups-page-media.f611a51e684c48ef.js new file mode 100644 index 000000000..81cc2098d Binary files /dev/null and b/public/js/groups-page-media.f611a51e684c48ef.js differ diff --git a/public/js/groups-page-members.bfdefdd66058e838.js b/public/js/groups-page-members.bfdefdd66058e838.js new file mode 100644 index 000000000..f862c4c0e Binary files /dev/null and b/public/js/groups-page-members.bfdefdd66058e838.js differ diff --git a/public/js/groups-page-topics.431ebaf843ca9b16.js b/public/js/groups-page-topics.431ebaf843ca9b16.js new file mode 100644 index 000000000..40f026580 Binary files /dev/null and b/public/js/groups-page-topics.431ebaf843ca9b16.js differ diff --git a/public/js/groups-page.53eccead9512c61f.js b/public/js/groups-page.53eccead9512c61f.js new file mode 100644 index 000000000..5ed4c8dbd Binary files /dev/null and b/public/js/groups-page.53eccead9512c61f.js differ diff --git a/public/js/groups-post.639cb121bdc6f4a7.js b/public/js/groups-post.639cb121bdc6f4a7.js new file mode 100644 index 000000000..6db341dc2 Binary files /dev/null and b/public/js/groups-post.639cb121bdc6f4a7.js differ diff --git a/public/js/groups-profile.3b11ffa46ae76520.js b/public/js/groups-profile.3b11ffa46ae76520.js new file mode 100644 index 000000000..4d2dda3fc Binary files /dev/null and b/public/js/groups-profile.3b11ffa46ae76520.js differ diff --git a/public/js/groups.js b/public/js/groups.js new file mode 100644 index 000000000..0c589194e Binary files /dev/null and b/public/js/groups.js differ diff --git a/public/js/hashtag.js b/public/js/hashtag.js index 5ef001f9f..e543b0162 100644 Binary files a/public/js/hashtag.js and b/public/js/hashtag.js differ diff --git a/public/js/home.chunk.351f55e9d09b6482.js b/public/js/home.chunk.351f55e9d09b6482.js deleted file mode 100644 index c6f3e4d4f..000000000 Binary files a/public/js/home.chunk.351f55e9d09b6482.js and /dev/null differ diff --git a/public/js/home.chunk.c362371940daf318.js b/public/js/home.chunk.c362371940daf318.js new file mode 100644 index 000000000..4ebd0feb2 Binary files /dev/null and b/public/js/home.chunk.c362371940daf318.js differ diff --git a/public/js/home.chunk.c362371940daf318.js.LICENSE.txt b/public/js/home.chunk.c362371940daf318.js.LICENSE.txt new file mode 100644 index 000000000..ae386fb79 --- /dev/null +++ b/public/js/home.chunk.c362371940daf318.js.LICENSE.txt @@ -0,0 +1 @@ +/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ diff --git a/public/js/i18n.bundle.47cbf9f04d955267.js b/public/js/i18n.bundle.47cbf9f04d955267.js deleted file mode 100644 index bf83a1075..000000000 Binary files a/public/js/i18n.bundle.47cbf9f04d955267.js and /dev/null differ diff --git a/public/js/i18n.bundle.882da44b752e4e1a.js b/public/js/i18n.bundle.882da44b752e4e1a.js new file mode 100644 index 000000000..4f85b6937 Binary files /dev/null and b/public/js/i18n.bundle.882da44b752e4e1a.js differ diff --git a/public/js/landing.js b/public/js/landing.js index 8a3b2322c..3069baa87 100644 Binary files a/public/js/landing.js and b/public/js/landing.js differ diff --git a/public/js/manifest.js b/public/js/manifest.js index 7ba9d795b..38f8d015d 100644 Binary files a/public/js/manifest.js and b/public/js/manifest.js differ diff --git a/public/js/notifications.chunk.3b92cf46da469de1.js b/public/js/notifications.chunk.3b92cf46da469de1.js deleted file mode 100644 index 94f7da536..000000000 Binary files a/public/js/notifications.chunk.3b92cf46da469de1.js and /dev/null differ diff --git a/public/js/notifications.chunk.8c41265737b2568a.js b/public/js/notifications.chunk.8c41265737b2568a.js new file mode 100644 index 000000000..dc1c1ffb4 Binary files /dev/null and b/public/js/notifications.chunk.8c41265737b2568a.js differ diff --git a/public/js/portfolio.js b/public/js/portfolio.js index ce1e7097f..d4b0f18f2 100644 Binary files a/public/js/portfolio.js and b/public/js/portfolio.js differ diff --git a/public/js/post.chunk.5f457aeaa4ae598c.js b/public/js/post.chunk.5f457aeaa4ae598c.js new file mode 100644 index 000000000..8b86545da Binary files /dev/null and b/public/js/post.chunk.5f457aeaa4ae598c.js differ diff --git a/public/js/post.chunk.5f457aeaa4ae598c.js.LICENSE.txt b/public/js/post.chunk.5f457aeaa4ae598c.js.LICENSE.txt new file mode 100644 index 000000000..ae386fb79 --- /dev/null +++ b/public/js/post.chunk.5f457aeaa4ae598c.js.LICENSE.txt @@ -0,0 +1 @@ +/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ diff --git a/public/js/post.chunk.74f8b1d1954f5d01.js b/public/js/post.chunk.74f8b1d1954f5d01.js deleted file mode 100644 index 55d35a437..000000000 Binary files a/public/js/post.chunk.74f8b1d1954f5d01.js and /dev/null differ diff --git a/public/js/profile-directory.js b/public/js/profile-directory.js index 11324401e..59acb414d 100644 Binary files a/public/js/profile-directory.js and b/public/js/profile-directory.js differ diff --git a/public/js/profile.chunk.0e5bd852054d6355.js b/public/js/profile.chunk.0e5bd852054d6355.js deleted file mode 100644 index acf4175dc..000000000 Binary files a/public/js/profile.chunk.0e5bd852054d6355.js and /dev/null differ diff --git a/public/js/profile.chunk.9e77e21e157a47c5.js b/public/js/profile.chunk.9e77e21e157a47c5.js new file mode 100644 index 000000000..3ec558805 Binary files /dev/null and b/public/js/profile.chunk.9e77e21e157a47c5.js differ diff --git a/public/js/profile.js b/public/js/profile.js index a9fcd40df..7a210d81d 100644 Binary files a/public/js/profile.js and b/public/js/profile.js differ diff --git a/public/js/profile.js.LICENSE.txt b/public/js/profile.js.LICENSE.txt new file mode 100644 index 000000000..ae386fb79 --- /dev/null +++ b/public/js/profile.js.LICENSE.txt @@ -0,0 +1 @@ +/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ diff --git a/public/js/profile~followers.bundle.731f680cfb96563d.js b/public/js/profile~followers.bundle.731f680cfb96563d.js deleted file mode 100644 index 51805c382..000000000 Binary files a/public/js/profile~followers.bundle.731f680cfb96563d.js and /dev/null differ diff --git a/public/js/profile~followers.bundle.f26ee6ed6ced9aa7.js b/public/js/profile~followers.bundle.f26ee6ed6ced9aa7.js new file mode 100644 index 000000000..141b5ea05 Binary files /dev/null and b/public/js/profile~followers.bundle.f26ee6ed6ced9aa7.js differ diff --git a/public/js/profile~following.bundle.3d95796c9f1678dd.js b/public/js/profile~following.bundle.3d95796c9f1678dd.js deleted file mode 100644 index 24db7dafa..000000000 Binary files a/public/js/profile~following.bundle.3d95796c9f1678dd.js and /dev/null differ diff --git a/public/js/profile~following.bundle.4ac5466dca6ca1c4.js b/public/js/profile~following.bundle.4ac5466dca6ca1c4.js new file mode 100644 index 000000000..b435bf157 Binary files /dev/null and b/public/js/profile~following.bundle.4ac5466dca6ca1c4.js differ diff --git a/public/js/remote_auth.js b/public/js/remote_auth.js index bbc0e463f..390b4c9a5 100644 Binary files a/public/js/remote_auth.js and b/public/js/remote_auth.js differ diff --git a/public/js/search.js b/public/js/search.js index 12c3c6751..448799422 100644 Binary files a/public/js/search.js and b/public/js/search.js differ diff --git a/public/js/spa.js b/public/js/spa.js index b496ba432..5c1950c77 100644 Binary files a/public/js/spa.js and b/public/js/spa.js differ diff --git a/public/js/status.js b/public/js/status.js index fb8eb6b7e..e90c8a402 100644 Binary files a/public/js/status.js and b/public/js/status.js differ diff --git a/public/js/stories.js b/public/js/stories.js index e38874d8e..8992e412c 100644 Binary files a/public/js/stories.js and b/public/js/stories.js differ diff --git a/public/js/story-compose.js b/public/js/story-compose.js index 85b348200..6f208b266 100644 Binary files a/public/js/story-compose.js and b/public/js/story-compose.js differ diff --git a/public/js/timeline.js b/public/js/timeline.js index a0ec87563..73ff3311e 100644 Binary files a/public/js/timeline.js and b/public/js/timeline.js differ diff --git a/public/js/vendor.js b/public/js/vendor.js index 530259cb1..1753e176e 100644 Binary files a/public/js/vendor.js and b/public/js/vendor.js differ diff --git a/public/js/vendor.js.LICENSE.txt b/public/js/vendor.js.LICENSE.txt index bd8662c18..83e24868a 100644 --- a/public/js/vendor.js.LICENSE.txt +++ b/public/js/vendor.js.LICENSE.txt @@ -31,13 +31,19 @@ */ /*! - * Cropper.js v1.6.1 + * Cropper.js v1.6.2 * https://fengyuanchen.github.io/cropperjs * * Copyright 2015-present Chen Fengyuan * Released under the MIT license * - * Date: 2023-09-17T03:44:19.860Z + * Date: 2024-04-21T07:43:05.335Z + */ + +/*! + * Glide.js v3.6.2 + * (c) 2013-2024 Jędrzej Chałubek (https://github.com/jedrzejchalubek/) + * Released under the MIT License. */ /*! @@ -64,15 +70,8 @@ */ /*! - * The buffer module from node.js, for the browser. - * - * @author Feross Aboukhadijeh - * @license MIT - */ - -/*! - * Vue.js v2.7.14 - * (c) 2014-2022 Evan You + * Vue.js v2.7.16 + * (c) 2014-2023 Evan You * Released under the MIT License. */ @@ -159,16 +158,6 @@ and limitations under the License. /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ -/*! queue-microtask. MIT License. Feross Aboukhadijeh */ - -/*! run-parallel. MIT License. Feross Aboukhadijeh */ - -/*! safe-buffer. MIT License. Feross Aboukhadijeh */ - -/*! simple-peer. MIT License. Feross Aboukhadijeh */ - -/*! simple-websocket. MIT License. Feross Aboukhadijeh */ - /** * vue-class-component v7.2.3 * (c) 2015-present Evan You @@ -184,23 +173,6 @@ and limitations under the License. * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors */ -/** - * @license Apache-2.0 - * Copyright 2018 Novage LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /** * filesize * diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 787d3c4bb..fa05d8a82 100644 Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ diff --git a/resources/assets/components/AccountImport.vue b/resources/assets/components/AccountImport.vue index b669345db..4cc6408f1 100644 --- a/resources/assets/components/AccountImport.vue +++ b/resources/assets/components/AccountImport.vue @@ -197,8 +197,17 @@

Media #{{idx + 1}}

- +
+

Caption

{{ media.title ? media.title : modalData.title }}

@@ -348,8 +357,22 @@ }, 500); }, - filterPostMeta(media) { - let json = JSON.parse(media); + async fixFacebookEncoding(string) { + // Facebook and Instagram are encoding UTF8 characters in a weird way in their json + // here is a good explanation what's going wrong https://sorashi.github.io/fix-facebook-json-archive-encoding + // See https://github.com/pixelfed/pixelfed/pull/4726 for more info + const replaced = string.replace(/\\u00([a-f0-9]{2})/g, (x) => String.fromCharCode(parseInt(x.slice(2), 16))); + const buffer = Array.from(replaced, (c) => c.charCodeAt(0)); + return new TextDecoder().decode(new Uint8Array(buffer)); + }, + + async filterPostMeta(media) { + let fbfix = await this.fixFacebookEncoding(media); + let json = JSON.parse(fbfix); + /* Sometimes the JSON isn't an array, when there's only one post */ + if (!Array.isArray(json)) { + json = new Array(json); + } let res = json.filter(j => { let ids = j.media.map(m => m.uri).filter(m => { if(this.config.allow_video_posts == true) { @@ -371,7 +394,7 @@ let file = this.$refs.zipInput.files[0]; let entries = await this.model(file); if (entries && entries.length) { - let files = await entries.filter(e => e.filename === 'content/posts_1.json'); + let files = await entries.filter(e => e.filename === 'content/posts_1.json' || e.filename === 'your_instagram_activity/content/posts_1.json'); if(!files || !files.length) { this.contactModal( @@ -392,16 +415,18 @@ let entries = await this.model(file); if (entries && entries.length) { this.zipFiles = entries; - let media = await entries.filter(e => e.filename === 'content/posts_1.json')[0].getData(new zip.TextWriter()); + let media = await entries.filter(e => e.filename === 'content/posts_1.json' || e.filename === 'your_instagram_activity/content/posts_1.json')[0].getData(new zip.TextWriter()); this.filterPostMeta(media); let imgs = await Promise.all(entries.filter(entry => { - return entry.filename.startsWith('media/posts/') && (entry.filename.endsWith('.png') || entry.filename.endsWith('.jpg') || entry.filename.endsWith('.mp4')); + return (entry.filename.startsWith('media/posts/') || entry.filename.startsWith('media/other/')) && (entry.filename.endsWith('.png') || entry.filename.endsWith('.jpg') || entry.filename.endsWith('.mp4')); }) .map(async entry => { if( - entry.filename.startsWith('media/posts/') && ( + entry.filename.startsWith('media/posts/') || + entry.filename.startsWith('media/other/') + ) && ( entry.filename.endsWith('.png') || entry.filename.endsWith('.jpg') || entry.filename.endsWith('.mp4') diff --git a/resources/assets/components/Direct.vue b/resources/assets/components/Direct.vue index bd8cf2078..3de61c3c0 100644 --- a/resources/assets/components/Direct.vue +++ b/resources/assets/components/Direct.vue @@ -112,14 +112,17 @@ import Sidebar from './partials/sidebar.vue'; import Placeholder from './partials/placeholders/DirectMessagePlaceholder.vue'; import Intersect from 'vue-intersect' + import Autocomplete from '@trevoreyre/autocomplete-vue' + import '@trevoreyre/autocomplete-vue/dist/style.css'; export default { components: { "drawer": Drawer, - "sidebar": Sidebar, - "intersect": Intersect, - "dm-placeholder": Placeholder - }, + "sidebar": Sidebar, + "intersect": Intersect, + "dm-placeholder": Placeholder, + "autocomplete": Autocomplete + }, data() { return { diff --git a/resources/assets/components/DirectMessage.vue b/resources/assets/components/DirectMessage.vue index e91ecdfaf..4766e5df1 100644 --- a/resources/assets/components/DirectMessage.vue +++ b/resources/assets/components/DirectMessage.vue @@ -456,7 +456,9 @@ // objDiv.scrollTop = objDiv.scrollHeight; // }, 300); }).catch(err => { - if(err.response.status == 403) { + if(err.response.status == 400) { + swal('Error', err.response.data.error, 'error'); + } else if(err.response.status == 403) { self.blocked = true; swal('Profile Unavailable', 'You cannot message this profile at this time.', 'error'); } diff --git a/resources/assets/components/Discover.vue b/resources/assets/components/Discover.vue index 44f8f9cef..366d38196 100644 --- a/resources/assets/components/Discover.vue +++ b/resources/assets/components/Discover.vue @@ -1,196 +1,14 @@ + + diff --git a/resources/assets/components/admin/AdminSettings.vue b/resources/assets/components/admin/AdminSettings.vue new file mode 100644 index 000000000..88b6192d4 --- /dev/null +++ b/resources/assets/components/admin/AdminSettings.vue @@ -0,0 +1,1568 @@ + + + + + diff --git a/resources/assets/components/admin/partial/AdminModalPost.vue b/resources/assets/components/admin/partial/AdminModalPost.vue new file mode 100644 index 000000000..ac5ad0df6 --- /dev/null +++ b/resources/assets/components/admin/partial/AdminModalPost.vue @@ -0,0 +1,139 @@ + + + diff --git a/resources/assets/components/admin/partial/AdminReadMore.vue b/resources/assets/components/admin/partial/AdminReadMore.vue new file mode 100644 index 000000000..b5c1e47f2 --- /dev/null +++ b/resources/assets/components/admin/partial/AdminReadMore.vue @@ -0,0 +1,106 @@ + + + diff --git a/resources/assets/components/admin/partial/AdminRemoteReportModal.vue b/resources/assets/components/admin/partial/AdminRemoteReportModal.vue new file mode 100644 index 000000000..63beff06d --- /dev/null +++ b/resources/assets/components/admin/partial/AdminRemoteReportModal.vue @@ -0,0 +1,304 @@ + + + diff --git a/resources/assets/components/admin/partial/AdminSettingsCheckbox.vue b/resources/assets/components/admin/partial/AdminSettingsCheckbox.vue new file mode 100644 index 000000000..bb8cfec96 --- /dev/null +++ b/resources/assets/components/admin/partial/AdminSettingsCheckbox.vue @@ -0,0 +1,42 @@ + + + diff --git a/resources/assets/components/admin/partial/AdminSettingsInput.vue b/resources/assets/components/admin/partial/AdminSettingsInput.vue new file mode 100644 index 000000000..25309134d --- /dev/null +++ b/resources/assets/components/admin/partial/AdminSettingsInput.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/resources/assets/components/admin/partial/AdminSettingsTabHeader.vue b/resources/assets/components/admin/partial/AdminSettingsTabHeader.vue new file mode 100644 index 000000000..ac75d3f37 --- /dev/null +++ b/resources/assets/components/admin/partial/AdminSettingsTabHeader.vue @@ -0,0 +1,63 @@ + + + diff --git a/resources/assets/components/groups/CreateGroup.vue b/resources/assets/components/groups/CreateGroup.vue new file mode 100644 index 000000000..55a857ab8 --- /dev/null +++ b/resources/assets/components/groups/CreateGroup.vue @@ -0,0 +1,359 @@ + + + + + diff --git a/resources/assets/components/groups/GroupFeed.vue b/resources/assets/components/groups/GroupFeed.vue new file mode 100644 index 000000000..9a357d4ee --- /dev/null +++ b/resources/assets/components/groups/GroupFeed.vue @@ -0,0 +1,989 @@ + + + + + diff --git a/resources/assets/components/groups/GroupInvite.vue b/resources/assets/components/groups/GroupInvite.vue new file mode 100644 index 000000000..ec11185a5 --- /dev/null +++ b/resources/assets/components/groups/GroupInvite.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/resources/assets/components/groups/GroupProfile.vue b/resources/assets/components/groups/GroupProfile.vue new file mode 100644 index 000000000..67077b84e --- /dev/null +++ b/resources/assets/components/groups/GroupProfile.vue @@ -0,0 +1,379 @@ + + + + + diff --git a/resources/assets/components/groups/GroupSettings.vue b/resources/assets/components/groups/GroupSettings.vue new file mode 100644 index 000000000..099d598f2 --- /dev/null +++ b/resources/assets/components/groups/GroupSettings.vue @@ -0,0 +1,1079 @@ + + + + + diff --git a/resources/assets/components/groups/GroupTopicFeed.vue b/resources/assets/components/groups/GroupTopicFeed.vue new file mode 100644 index 000000000..ee7f57433 --- /dev/null +++ b/resources/assets/components/groups/GroupTopicFeed.vue @@ -0,0 +1,170 @@ + + + diff --git a/resources/assets/components/groups/GroupsHome.vue b/resources/assets/components/groups/GroupsHome.vue new file mode 100644 index 000000000..3a3d6dde8 --- /dev/null +++ b/resources/assets/components/groups/GroupsHome.vue @@ -0,0 +1,473 @@ + + + + + diff --git a/resources/assets/components/groups/Page/GroupAbout.vue b/resources/assets/components/groups/Page/GroupAbout.vue new file mode 100644 index 000000000..8285a3db2 --- /dev/null +++ b/resources/assets/components/groups/Page/GroupAbout.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/resources/assets/components/groups/Page/GroupMedia.vue b/resources/assets/components/groups/Page/GroupMedia.vue new file mode 100644 index 000000000..b2d098ac8 --- /dev/null +++ b/resources/assets/components/groups/Page/GroupMedia.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/resources/assets/components/groups/Page/GroupMembers.vue b/resources/assets/components/groups/Page/GroupMembers.vue new file mode 100644 index 000000000..5b866fc17 --- /dev/null +++ b/resources/assets/components/groups/Page/GroupMembers.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/resources/assets/components/groups/Page/GroupTopics.vue b/resources/assets/components/groups/Page/GroupTopics.vue new file mode 100644 index 000000000..60f0fa496 --- /dev/null +++ b/resources/assets/components/groups/Page/GroupTopics.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/resources/assets/components/groups/partials/CommentDrawer.vue b/resources/assets/components/groups/partials/CommentDrawer.vue new file mode 100644 index 000000000..e7df4a6eb --- /dev/null +++ b/resources/assets/components/groups/partials/CommentDrawer.vue @@ -0,0 +1,845 @@ + + + + + diff --git a/resources/assets/components/groups/partials/CommentPost.vue b/resources/assets/components/groups/partials/CommentPost.vue new file mode 100644 index 000000000..4b448f913 --- /dev/null +++ b/resources/assets/components/groups/partials/CommentPost.vue @@ -0,0 +1,405 @@ + + + + + diff --git a/resources/assets/components/groups/partials/ContextMenu.vue b/resources/assets/components/groups/partials/ContextMenu.vue new file mode 100644 index 000000000..52fad0e74 --- /dev/null +++ b/resources/assets/components/groups/partials/ContextMenu.vue @@ -0,0 +1,692 @@ + + + diff --git a/resources/assets/components/groups/partials/CreateForm/CheckboxInput.vue b/resources/assets/components/groups/partials/CreateForm/CheckboxInput.vue new file mode 100644 index 000000000..03fa8727a --- /dev/null +++ b/resources/assets/components/groups/partials/CreateForm/CheckboxInput.vue @@ -0,0 +1,59 @@ + + + diff --git a/resources/assets/components/groups/partials/CreateForm/SelectInput.vue b/resources/assets/components/groups/partials/CreateForm/SelectInput.vue new file mode 100644 index 000000000..304ce0c7d --- /dev/null +++ b/resources/assets/components/groups/partials/CreateForm/SelectInput.vue @@ -0,0 +1,70 @@ + + + diff --git a/resources/assets/components/groups/partials/CreateForm/TextAreaInput.vue b/resources/assets/components/groups/partials/CreateForm/TextAreaInput.vue new file mode 100644 index 000000000..e8977db3f --- /dev/null +++ b/resources/assets/components/groups/partials/CreateForm/TextAreaInput.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupEvents.vue b/resources/assets/components/groups/partials/GroupEvents.vue new file mode 100644 index 000000000..e69de29bb diff --git a/resources/assets/components/groups/partials/GroupInfoCard.vue b/resources/assets/components/groups/partials/GroupInfoCard.vue new file mode 100644 index 000000000..455954b8f --- /dev/null +++ b/resources/assets/components/groups/partials/GroupInfoCard.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupInsights.vue b/resources/assets/components/groups/partials/GroupInsights.vue new file mode 100644 index 000000000..9909508cb --- /dev/null +++ b/resources/assets/components/groups/partials/GroupInsights.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupInviteModal.vue b/resources/assets/components/groups/partials/GroupInviteModal.vue new file mode 100644 index 000000000..75e5f9f68 --- /dev/null +++ b/resources/assets/components/groups/partials/GroupInviteModal.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupListCard.vue b/resources/assets/components/groups/partials/GroupListCard.vue new file mode 100644 index 000000000..64300160e --- /dev/null +++ b/resources/assets/components/groups/partials/GroupListCard.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupMedia.vue b/resources/assets/components/groups/partials/GroupMedia.vue new file mode 100644 index 000000000..65a96001d --- /dev/null +++ b/resources/assets/components/groups/partials/GroupMedia.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupMembers.vue b/resources/assets/components/groups/partials/GroupMembers.vue new file mode 100644 index 000000000..3913aa93d --- /dev/null +++ b/resources/assets/components/groups/partials/GroupMembers.vue @@ -0,0 +1,684 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupModeration.vue b/resources/assets/components/groups/partials/GroupModeration.vue new file mode 100644 index 000000000..54d114391 --- /dev/null +++ b/resources/assets/components/groups/partials/GroupModeration.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupPolls.vue b/resources/assets/components/groups/partials/GroupPolls.vue new file mode 100644 index 000000000..e69de29bb diff --git a/resources/assets/components/groups/partials/GroupPostModal.vue b/resources/assets/components/groups/partials/GroupPostModal.vue new file mode 100644 index 000000000..094d98a26 --- /dev/null +++ b/resources/assets/components/groups/partials/GroupPostModal.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupSearchModal.vue b/resources/assets/components/groups/partials/GroupSearchModal.vue new file mode 100644 index 000000000..8cc70039d --- /dev/null +++ b/resources/assets/components/groups/partials/GroupSearchModal.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupStatus.vue b/resources/assets/components/groups/partials/GroupStatus.vue new file mode 100644 index 000000000..fe61c892e --- /dev/null +++ b/resources/assets/components/groups/partials/GroupStatus.vue @@ -0,0 +1,874 @@ + + + + + diff --git a/resources/assets/components/groups/partials/GroupTopics.vue b/resources/assets/components/groups/partials/GroupTopics.vue new file mode 100644 index 000000000..ed4885b1e --- /dev/null +++ b/resources/assets/components/groups/partials/GroupTopics.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/resources/assets/components/groups/partials/LeaveGroup.vue b/resources/assets/components/groups/partials/LeaveGroup.vue new file mode 100644 index 000000000..417c29347 --- /dev/null +++ b/resources/assets/components/groups/partials/LeaveGroup.vue @@ -0,0 +1,9 @@ + + + diff --git a/resources/assets/components/groups/partials/MemberLimitInteractionsModal.vue b/resources/assets/components/groups/partials/MemberLimitInteractionsModal.vue new file mode 100644 index 000000000..143b47575 --- /dev/null +++ b/resources/assets/components/groups/partials/MemberLimitInteractionsModal.vue @@ -0,0 +1,172 @@ + + + diff --git a/resources/assets/components/groups/partials/Membership/MemberOnlyWarning.vue b/resources/assets/components/groups/partials/Membership/MemberOnlyWarning.vue new file mode 100644 index 000000000..cea224a5f --- /dev/null +++ b/resources/assets/components/groups/partials/Membership/MemberOnlyWarning.vue @@ -0,0 +1,38 @@ + + + diff --git a/resources/assets/components/groups/partials/Page/GroupBanner.vue b/resources/assets/components/groups/partials/Page/GroupBanner.vue new file mode 100644 index 000000000..8038cdce5 --- /dev/null +++ b/resources/assets/components/groups/partials/Page/GroupBanner.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/resources/assets/components/groups/partials/Page/GroupHeaderDetails.vue b/resources/assets/components/groups/partials/Page/GroupHeaderDetails.vue new file mode 100644 index 000000000..baeb2dfd5 --- /dev/null +++ b/resources/assets/components/groups/partials/Page/GroupHeaderDetails.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/resources/assets/components/groups/partials/Page/GroupNavTabs.vue b/resources/assets/components/groups/partials/Page/GroupNavTabs.vue new file mode 100644 index 000000000..c0a8827ea --- /dev/null +++ b/resources/assets/components/groups/partials/Page/GroupNavTabs.vue @@ -0,0 +1,167 @@ + + + + diff --git a/resources/assets/components/groups/partials/ReadMore.vue b/resources/assets/components/groups/partials/ReadMore.vue new file mode 100644 index 000000000..9dabf199d --- /dev/null +++ b/resources/assets/components/groups/partials/ReadMore.vue @@ -0,0 +1,51 @@ + + + diff --git a/resources/assets/components/groups/partials/SelfDiscover.vue b/resources/assets/components/groups/partials/SelfDiscover.vue new file mode 100644 index 000000000..2fb15a39f --- /dev/null +++ b/resources/assets/components/groups/partials/SelfDiscover.vue @@ -0,0 +1,465 @@ + + + + + diff --git a/resources/assets/components/groups/partials/SelfFeed.vue b/resources/assets/components/groups/partials/SelfFeed.vue new file mode 100644 index 000000000..2fff5298d --- /dev/null +++ b/resources/assets/components/groups/partials/SelfFeed.vue @@ -0,0 +1,133 @@ + + + diff --git a/resources/assets/components/groups/partials/SelfGroups.vue b/resources/assets/components/groups/partials/SelfGroups.vue new file mode 100644 index 000000000..411ec67e4 --- /dev/null +++ b/resources/assets/components/groups/partials/SelfGroups.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/resources/assets/components/groups/partials/SelfInvitations.vue b/resources/assets/components/groups/partials/SelfInvitations.vue new file mode 100644 index 000000000..f9d4f1e64 --- /dev/null +++ b/resources/assets/components/groups/partials/SelfInvitations.vue @@ -0,0 +1,41 @@ + + + diff --git a/resources/assets/components/groups/partials/SelfNotifications.vue b/resources/assets/components/groups/partials/SelfNotifications.vue new file mode 100644 index 000000000..f591cfbd7 --- /dev/null +++ b/resources/assets/components/groups/partials/SelfNotifications.vue @@ -0,0 +1,309 @@ + + + + + diff --git a/resources/assets/components/groups/partials/SelfRemoteSearch.vue b/resources/assets/components/groups/partials/SelfRemoteSearch.vue new file mode 100644 index 000000000..9c3443960 --- /dev/null +++ b/resources/assets/components/groups/partials/SelfRemoteSearch.vue @@ -0,0 +1,47 @@ + + + diff --git a/resources/assets/components/groups/partials/ShareMenu.vue b/resources/assets/components/groups/partials/ShareMenu.vue new file mode 100644 index 000000000..3f4141486 --- /dev/null +++ b/resources/assets/components/groups/partials/ShareMenu.vue @@ -0,0 +1,11 @@ + + + diff --git a/resources/assets/components/groups/partials/Status/GroupHeader.vue b/resources/assets/components/groups/partials/Status/GroupHeader.vue new file mode 100644 index 000000000..966b16b92 --- /dev/null +++ b/resources/assets/components/groups/partials/Status/GroupHeader.vue @@ -0,0 +1,326 @@ + + + + + diff --git a/resources/assets/components/groups/partials/Status/ParentUnavailable.vue b/resources/assets/components/groups/partials/Status/ParentUnavailable.vue new file mode 100644 index 000000000..edb0f5062 --- /dev/null +++ b/resources/assets/components/groups/partials/Status/ParentUnavailable.vue @@ -0,0 +1,58 @@ + + + diff --git a/resources/assets/components/groups/sections/Loader.vue b/resources/assets/components/groups/sections/Loader.vue new file mode 100644 index 000000000..e0dc053d0 --- /dev/null +++ b/resources/assets/components/groups/sections/Loader.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/assets/components/groups/sections/Sidebar.vue b/resources/assets/components/groups/sections/Sidebar.vue new file mode 100644 index 000000000..7b6326b52 --- /dev/null +++ b/resources/assets/components/groups/sections/Sidebar.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/resources/assets/components/landing/sections/nav.vue b/resources/assets/components/landing/sections/nav.vue index 38fde6ef2..16dbfef1e 100644 --- a/resources/assets/components/landing/sections/nav.vue +++ b/resources/assets/components/landing/sections/nav.vue @@ -1,33 +1,46 @@ diff --git a/resources/assets/components/partials/navbar.vue b/resources/assets/components/partials/navbar.vue index c3c21243e..21f59e0ec 100644 --- a/resources/assets/components/partials/navbar.vue +++ b/resources/assets/components/partials/navbar.vue @@ -1,7 +1,7 @@