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/.env.docker b/.env.docker index 0d5836745..0e373aedc 100644 --- a/.env.docker +++ b/.env.docker @@ -60,6 +60,15 @@ ADMIN_DOMAIN="${APP_DOMAIN}" # @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" @@ -1025,7 +1034,7 @@ DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/ca # Automatically run "One-time setup tasks" commands. # -# If you are migrating to this docker-compose setup or have manually run the "One time seutp" +# 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. # @@ -1122,6 +1131,13 @@ DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/ca # @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 ################################################################################ @@ -1202,6 +1218,14 @@ DOCKER_DB_HOST_PORT="${DB_PORT:?error}" # @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}" diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..79ce65337 --- /dev/null +++ b/.env.example @@ -0,0 +1,79 @@ +APP_NAME="Pixelfed" +APP_ENV="production" +APP_KEY= +APP_DEBUG="false" + +# Instance Configuration +OPEN_REGISTRATION="false" +ENFORCE_EMAIL_VERIFICATION="false" +PF_MAX_USERS="1000" +OAUTH_ENABLED="true" +ENABLE_CONFIG_CACHE=true + +# Media Configuration +PF_OPTIMIZE_IMAGES="true" +IMAGE_QUALITY="80" +MAX_PHOTO_SIZE="15000" +MAX_CAPTION_LENGTH="500" +MAX_ALBUM_LENGTH="4" + +# Instance URL Configuration +APP_URL="http://localhost" +APP_DOMAIN="localhost" +ADMIN_DOMAIN="localhost" +SESSION_DOMAIN="localhost" +TRUST_PROXIES="*" + +# Database Configuration +DB_CONNECTION="mysql" +DB_HOST="127.0.0.1" +DB_PORT="3306" +DB_DATABASE="pixelfed" +DB_USERNAME="pixelfed" +DB_PASSWORD="pixelfed" + +# Redis Configuration +REDIS_CLIENT="predis" +REDIS_SCHEME="tcp" +REDIS_HOST="127.0.0.1" +REDIS_PASSWORD="null" +REDIS_PORT="6379" + +# Laravel Configuration +SESSION_DRIVER="database" +CACHE_DRIVER="redis" +QUEUE_DRIVER="redis" +BROADCAST_DRIVER="log" +LOG_CHANNEL="stack" +HORIZON_PREFIX="horizon-" + +# ActivityPub Configuration +ACTIVITY_PUB="false" +AP_REMOTE_FOLLOW="false" +AP_INBOX="false" +AP_OUTBOX="false" +AP_SHAREDINBOX="false" + +# Experimental Configuration +EXP_EMC="true" + +## Mail Configuration (Post-Installer) +MAIL_DRIVER=log +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="pixelfed@example.com" +MAIL_FROM_NAME="Pixelfed" + +## S3 Configuration (Post-Installer) +PF_ENABLE_CLOUD=false +FILESYSTEM_CLOUD=s3 +#AWS_ACCESS_KEY_ID= +#AWS_SECRET_ACCESS_KEY= +#AWS_DEFAULT_REGION= +#AWS_BUCKET= +#AWS_URL= +#AWS_ENDPOINT= +#AWS_USE_PATH_STYLE_ENDPOINT=false diff --git a/.hadolint.yaml b/.hadolint.yaml index cbb62ca47..27fa2ff27 100644 --- a/.hadolint.yaml +++ b/.hadolint.yaml @@ -1,5 +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/CHANGELOG.md b/CHANGELOG.md index 2b7670c75..2272b30a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,66 @@ # Release Notes -## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.13...dev) +## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev) + +### 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)) +- ([](https://github.com/pixelfed/pixelfed/commit/)) +- ([](https://github.com/pixelfed/pixelfed/commit/)) + +## [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 @@ -19,7 +79,58 @@ - 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)) -- ([](https://github.com/pixelfed/pixelfed/commit/)) +- 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) diff --git a/Dockerfile b/Dockerfile index a6ad884b8..ff8c9e24b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -132,6 +132,10 @@ 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 @@ -176,6 +180,55 @@ RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${T 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 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 \ +<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/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/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 index 54329f7c7..367c22d1f 100644 --- a/app/Console/Commands/MediaCloudUrlRewrite.php +++ b/app/Console/Commands/MediaCloudUrlRewrite.php @@ -47,7 +47,7 @@ class MediaCloudUrlRewrite extends Command implements PromptsForMissingInput protected function preflightCheck() { - if(config_cache('pixelfed.cloud_storage') != true) { + if(!(bool) config_cache('pixelfed.cloud_storage')) { $this->info('Error: Cloud storage is not enabled!'); $this->error('Aborting...'); exit; 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/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 dcee73ee1..d5f6962f4 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -19,7 +19,6 @@ class Kernel extends ConsoleKernel /** * Define the application's command schedule. * - * @param \Illuminate\Console\Scheduling\Schedule $schedule * * @return void */ @@ -32,8 +31,9 @@ class Kernel extends ConsoleKernel $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); } @@ -51,6 +51,7 @@ class Kernel extends ConsoleKernel $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(); } /** @@ -60,7 +61,7 @@ class Kernel extends ConsoleKernel */ protected function commands() { - $this->load(__DIR__ . '/Commands'); + $this->load(__DIR__.'/Commands'); require base_path('routes/console.php'); } 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 8d3e4b7fc..ce53ea560 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,37 +31,37 @@ 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) : []; @@ -84,22 +74,22 @@ trait AdminDirectoryController $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; @@ -108,8 +98,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.'); } }, @@ -120,7 +110,7 @@ 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(); @@ -146,11 +136,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)]); } } @@ -159,25 +149,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; } @@ -194,11 +185,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) : []; @@ -208,26 +199,27 @@ 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); - }); + ->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))); @@ -240,9 +232,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; } @@ -253,7 +246,7 @@ trait AdminDirectoryController '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'), @@ -273,8 +266,8 @@ trait AdminDirectoryController '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.'); } }, @@ -285,10 +278,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); } @@ -297,6 +290,7 @@ trait AdminDirectoryController $data = (new PixelfedDirectoryController())->buildListing(); $res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data); + return 200; } @@ -304,7 +298,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); @@ -312,12 +306,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']); } @@ -328,12 +322,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') @@ -343,21 +338,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'); @@ -380,11 +375,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; } @@ -392,13 +388,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([]); @@ -409,7 +405,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()); @@ -417,8 +413,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; } @@ -426,7 +423,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'); @@ -434,18 +431,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/AdminSettingsController.php b/app/Http/Controllers/Admin/AdminSettingsController.php index 525f6114c..98e16bbc0 100644 --- a/app/Http/Controllers/Admin/AdminSettingsController.php +++ b/app/Http/Controllers/Admin/AdminSettingsController.php @@ -7,7 +7,9 @@ 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 Artisan; @@ -71,6 +73,7 @@ trait AdminSettingsController 'admin_account_id' => 'nullable', 'regs' => 'required|in:open,filtered,closed', 'account_migration' => 'nullable', + 'rule_delete' => 'sometimes', ]); $orb = false; @@ -310,4 +313,573 @@ trait AdminSettingsController 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', + '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.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..3e292037c 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -2,562 +2,589 @@ 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\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 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) + { + $messages = Contact::orderByDesc('id')->paginate(10); - 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; + return view('admin.messages.home', compact('messages')); + } - 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; + public function messagesShow(Request $request, $id) + { + $message = Contact::findOrFail($id); - } - } + return view('admin.messages.show', compact('message')); + } - if($changed) { - $news->save(); - } - $redirect = $news->published_at ? $news->permalink() : $news->editUrl(); - return redirect($redirect); - } + 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(); - public function diagnosticsHome(Request $request) - { - return view('admin.diagnostics.home'); - } + } - public function diagnosticsDecrypt(Request $request) - { - $this->validate($request, [ - 'payload' => 'required' - ]); + public function newsroomHome(Request $request) + { + $newsroom = Newsroom::latest()->paginate(10); - $key = 'exception_report:'; - $decrypted = decrypt($request->input('payload')); + return view('admin.newsroom.home', compact('newsroom')); + } - if(!starts_with($decrypted, $key)) { - abort(403, 'Can only decrypt error diagnostics'); - } + public function newsroomCreate(Request $request) + { + return view('admin.newsroom.create'); + } - $res = [ - 'decrypted' => substr($decrypted, strlen($key)) - ]; + public function newsroomEdit(Request $request, $id) + { + $news = Newsroom::findOrFail($id); - return response()->json($res); - } + return view('admin.newsroom.edit', compact('news')); + } - 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 newsroomDelete(Request $request, $id) + { + $news = Newsroom::findOrFail($id); + $news->delete(); - 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' - ]); + return redirect('/i/admin/newsroom'); + } - if($request->has('cc')) { - Cache::forget('pf:admin:custom_emoji:stats'); - Cache::forget('pf:custom_emoji'); - return redirect(route('admin.custom-emoji')); - } + 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; - $sort = $request->input('sort') ?? 'all'; + 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($sort == 'search' && empty($request->input('q'))) { - return redirect(route('admin.custom-emoji')); - } + } + } - $pg = config('database.default') == 'pgsql'; + if ($changed) { + $news->save(); + } + $redirect = $news->published_at ? $news->permalink() : $news->editUrl(); - $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 redirect($redirect); + } - $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 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; - if($pg) { - $res['duplicate'] = CustomEmoji::select('shortcode')->groupBy('shortcode')->havingRaw('count(*) > 1')->count(); - } else { - $res['duplicate'] = CustomEmoji::groupBy('shortcode')->havingRaw('count(*) > 1')->count(); - } + 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 $res; - }); + } + } - return view('admin.custom-emoji.home', compact('emojis', 'sort', 'stats')); - } + if ($changed) { + $news->save(); + } + $redirect = $news->published_at ? $news->permalink() : $news->editUrl(); - 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($redirect); + } - public function customEmojiAdd(Request $request) - { - abort_unless(config('federation.custom_emoji.enabled'), 404); - return view('admin.custom-emoji.add'); - } + public function diagnosticsHome(Request $request) + { + return view('admin.diagnostics.home'); + } - 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) - ]); + public function diagnosticsDecrypt(Request $request) + { + $this->validate($request, [ + 'payload' => 'required', + ]); - $emoji = new CustomEmoji; - $emoji->shortcode = $request->input('shortcode'); - $emoji->domain = config('pixelfed.domain.app'); - $emoji->save(); + $key = 'exception_report:'; + $decrypted = decrypt($request->input('payload')); - $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 (! starts_with($decrypted, $key)) { + abort(403, 'Can only decrypt error diagnostics'); + } - 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')); - } + $res = [ + 'decrypted' => substr($decrypted, strlen($key)), + ]; - 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')); - } + 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 index 7b25ac369..afdcfba1a 100644 --- a/app/Http/Controllers/AdminCuratedRegisterController.php +++ b/app/Http/Controllers/AdminCuratedRegisterController.php @@ -174,7 +174,7 @@ class AdminCuratedRegisterController extends Controller public function apiMessageSendStore(Request $request, $id) { $this->validate($request, [ - 'message' => 'required|string|min:5|max:1000', + 'message' => 'required|string|min:5|max:3000', ]); $record = CuratedRegister::findOrFail($id); abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); @@ -240,6 +240,11 @@ class AdminCuratedRegisterController extends Controller $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, diff --git a/app/Http/Controllers/Api/AdminApiController.php b/app/Http/Controllers/Api/AdminApiController.php index 69ba54cee..21a832a76 100644 --- a/app/Http/Controllers/Api/AdminApiController.php +++ b/app/Http/Controllers/Api/AdminApiController.php @@ -2,45 +2,40 @@ 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() || !$request->user()->token(), 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); @@ -50,7 +45,7 @@ class AdminApiController extends Controller public function getStats(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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); @@ -59,12 +54,13 @@ class AdminApiController extends Controller $res['autospam_count'] = AccountInterstitial::whereType('post.autospam') ->whereNull('appeal_handled_at') ->count(); + return $res; } public function autospam(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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); @@ -73,26 +69,27 @@ class AdminApiController extends Controller ->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; }); @@ -101,14 +98,14 @@ class AdminApiController extends Controller public function autospamHandle(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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'); @@ -122,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(); @@ -148,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; @@ -167,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'; @@ -198,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'; @@ -231,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; } @@ -247,44 +250,48 @@ class AdminApiController extends Controller public function modReports(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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() @@ -295,14 +302,14 @@ class AdminApiController extends Controller public function modReportHandle(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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'); @@ -311,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); } @@ -355,7 +362,7 @@ class AdminApiController extends Controller public function getConfiguration(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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); @@ -366,42 +373,43 @@ class AdminApiController extends Controller [ '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() || !$request->user()->token(), 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); @@ -410,7 +418,7 @@ class AdminApiController extends Controller $this->validate($request, [ 'key' => 'required', - 'value' => 'required' + 'value' => 'required', ]); $allowedKeys = [ @@ -423,50 +431,51 @@ 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() || !$request->user()->token(), 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); @@ -477,27 +486,29 @@ class AdminApiController extends Controller $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() || !$request->user()->token(), 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); @@ -510,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; @@ -520,7 +531,7 @@ class AdminApiController extends Controller public function userAdminAction(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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); @@ -528,7 +539,7 @@ class AdminApiController extends Controller $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'); @@ -538,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); } @@ -567,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; @@ -586,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) @@ -600,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(); @@ -612,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) @@ -625,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) @@ -640,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) @@ -655,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); @@ -673,7 +685,7 @@ class AdminApiController extends Controller ->action('admin.user.moderate') ->metadata([ 'action' => $action, - 'message' => 'Success!' + 'message' => 'Success!', ]) ->accessLevel('admin') ->save(); @@ -687,14 +699,14 @@ 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() || !$request->user()->token(), 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); @@ -711,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) @@ -734,7 +746,7 @@ class AdminApiController extends Controller public function getInstance(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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); @@ -747,7 +759,7 @@ class AdminApiController extends Controller public function moderateInstance(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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); @@ -755,7 +767,7 @@ class AdminApiController extends Controller $this->validate($request, [ 'id' => 'required', 'key' => 'required|in:unlisted,auto_cw,banned', - 'value' => 'required' + 'value' => 'required', ]); $id = $request->input('id'); @@ -773,7 +785,7 @@ class AdminApiController extends Controller public function refreshInstanceStats(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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); @@ -793,51 +805,51 @@ class AdminApiController extends Controller public function getAllStats(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 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); - if($request->has('refresh')) { + 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/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 2da53762a..80c955fb9 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -60,6 +60,7 @@ 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; @@ -131,7 +132,7 @@ class ApiV1Controller extends Controller */ public function apps(Request $request) { - abort_if(! config_cache('pixelfed.oauth_enabled'), 404); + abort_if(! (bool) config_cache('pixelfed.oauth_enabled'), 404); $this->validate($request, [ 'client_name' => 'required', @@ -193,6 +194,10 @@ class ApiV1Controller extends Controller 'fields' => [], ]; + if ($request->has(self::PF_API_ENTITY_KEY)) { + $res['settings'] = AccountService::getAccountSettings($user->profile_id); + } + return $this->json($res); } @@ -207,6 +212,7 @@ class ApiV1Controller extends Controller abort_if(! $request->user() || ! $request->user()->token(), 403); abort_unless($request->user()->tokenCan('read'), 403); + $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); @@ -326,7 +332,7 @@ class ApiV1Controller extends Controller } if ($request->has('locked')) { - $locked = $request->input('locked') == 'true'; + $locked = $request->boolean('locked'); if ($profile->is_private != $locked) { $profile->is_private = $locked; $changes = true; @@ -334,7 +340,7 @@ class ApiV1Controller extends Controller } if ($request->has('reduce_motion')) { - $reduced = $request->input('reduce_motion'); + $reduced = $request->boolean('reduce_motion'); if ($settings->reduce_motion != $reduced) { $settings->reduce_motion = $reduced; $changes = true; @@ -342,7 +348,7 @@ class ApiV1Controller extends Controller } if ($request->has('high_contrast_mode')) { - $contrast = $request->input('high_contrast_mode'); + $contrast = $request->boolean('high_contrast_mode'); if ($settings->high_contrast_mode != $contrast) { $settings->high_contrast_mode = $contrast; $changes = true; @@ -350,7 +356,7 @@ class ApiV1Controller extends Controller } if ($request->has('video_autoplay')) { - $autoplay = $request->input('video_autoplay'); + $autoplay = $request->boolean('video_autoplay'); if ($settings->video_autoplay != $autoplay) { $settings->video_autoplay = $autoplay; $changes = true; @@ -370,7 +376,7 @@ class ApiV1Controller extends Controller } if ($request->has('media_descriptions')) { - $md = $request->input('media_descriptions') == true; + $md = $request->boolean('media_descriptions'); if ($composeSettings['media_descriptions'] != $md) { $composeSettings['media_descriptions'] = $md; $changes = true; @@ -378,7 +384,7 @@ class ApiV1Controller extends Controller } if ($request->has('crawlable')) { - $crawlable = $request->input('crawlable'); + $crawlable = $request->boolean('crawlable'); if ($settings->crawlable != $crawlable) { $settings->crawlable = $crawlable; $changes = true; @@ -386,7 +392,7 @@ class ApiV1Controller extends Controller } if ($request->has('show_profile_follower_count')) { - $show_profile_follower_count = $request->input('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; @@ -395,7 +401,7 @@ class ApiV1Controller extends Controller } if ($request->has('show_profile_following_count')) { - $show_profile_following_count = $request->input('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; @@ -404,7 +410,7 @@ class ApiV1Controller extends Controller } if ($request->has('public_dm')) { - $public_dm = $request->input('public_dm'); + $public_dm = $request->boolean('public_dm'); if ($settings->public_dm != $public_dm) { $settings->public_dm = $public_dm; $changes = true; @@ -422,7 +428,7 @@ class ApiV1Controller extends Controller } if ($request->has('disable_embeds')) { - $disabledEmbeds = $request->input('disable_embeds'); + $disabledEmbeds = $request->boolean('disable_embeds'); if ($other['disable_embeds'] != $disabledEmbeds) { $other['disable_embeds'] = $disabledEmbeds; $changes = true; @@ -441,8 +447,16 @@ class ApiV1Controller extends Controller 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); + 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) { @@ -740,7 +754,15 @@ class ApiV1Controller extends Controller $dir = $min_id ? '>' : '<'; $id = $min_id ?? $max_id; - $res = Status::whereProfileId($profile['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) @@ -960,10 +982,22 @@ class ApiV1Controller extends Controller $napi = $request->has(self::PF_API_ENTITY_KEY); $pid = $request->user()->profile_id ?? $request->user()->profile->id; $res = collect($ids) - ->filter(function ($id) use ($pid) { - return intval($id) !== intval($pid); - }) ->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); @@ -1103,7 +1137,7 @@ class ApiV1Controller extends Controller } $count = UserFilterService::blockCount($pid); - $maxLimit = intval(config('instance.user_filters.max_user_blocks')); + $maxLimit = (int) config_cache('instance.user_filters.max_user_blocks'); if ($count == 0) { $filterCount = UserFilter::whereUserId($pid) ->whereFilterType('block') @@ -1200,8 +1234,8 @@ class ApiV1Controller extends Controller if ($filter) { $filter->delete(); UserFilterService::unblock($pid, $profile->id); - RelationshipService::refresh($pid, $id); } + RelationshipService::refresh($pid, $id); $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); $res = $this->fractal->createData($resource)->toArray(); @@ -1301,12 +1335,17 @@ class ApiV1Controller extends Controller if ($res->count()) { $ids = $res->map(function ($status) { return $status['like_id']; - }); - $max = $ids->max(); - $min = $ids->min(); + })->filter(); + + $max = $ids->min() - 1; + $min = $ids->max(); $baseUrl = config('app.url').'/api/v1/favourites?limit='.$limit.'&'; - $link = '<'.$baseUrl.'max_id='.$max.'>; rel="next",<'.$baseUrl.'min_id='.$min.'>; rel="prev"'; + 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 { @@ -1328,7 +1367,8 @@ class ApiV1Controller extends Controller $user = $request->user(); abort_if($user->has_roles && ! UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); - $status = StatusService::getMastodon($id, false); + $napi = $request->has(self::PF_API_ENTITY_KEY); + $status = $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false); abort_unless($status, 404); @@ -1396,34 +1436,47 @@ class ApiV1Controller extends Controller $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); + + 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); - $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); + 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->scope, ['public', 'unlisted']), 403); + abort_if(! in_array($status['visibility'], ['public', 'unlisted']), 403); } } $like = Like::whereProfileId($user->profile_id) - ->whereStatusId($status->id) + ->whereStatusId($status['id']) ->first(); if ($like) { $like->forceDelete(); - $status->likes_count = $status->likes_count > 1 ? $status->likes_count - 1 : 0; - $status->save(); + $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); + StatusService::del($status['id']); - $res = StatusService::getMastodon($status->id, false); - $res['favourited'] = false; + $status['favourited'] = false; + $status['favourites_count'] = isset($ogStatus) ? $ogStatus->likes_count : $status['favourites_count'] - 1; - return $this->json($res); + return $this->json($status); } /** @@ -1611,7 +1664,7 @@ class ApiV1Controller extends Controller $stats = Cache::remember('api:v1:instance-data:stats', 43200, function () { return [ 'user_count' => User::count(), - 'status_count' => Status::whereNull('uri')->count(), + 'status_count' => StatusService::totalLocalStatuses(), 'domain_count' => Instance::count(), ]; }); @@ -1632,7 +1685,7 @@ class ApiV1Controller extends Controller return [ 'uri' => config('pixelfed.domain.app'), - 'title' => config('app.name'), + 'title' => config_cache('app.name'), 'short_description' => config_cache('app.short_description'), 'description' => config_cache('app.description'), 'email' => config('instance.email'), @@ -1651,7 +1704,7 @@ class ApiV1Controller extends Controller 'media_attachments' => [ 'image_matrix_limit' => 16777216, 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, - 'supported_mime_types' => explode(',', config('pixelfed.media_types')), + '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, @@ -1665,7 +1718,7 @@ class ApiV1Controller extends Controller 'statuses' => [ 'characters_reserved_per_url' => 23, 'max_characters' => (int) config_cache('pixelfed.max_caption_length'), - 'max_media_attachments' => (int) config('pixelfed.max_album_length'), + 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), ], ], ]; @@ -1754,12 +1807,16 @@ class ApiV1Controller extends Controller $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; - }); + $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 ($size >= $limit) { + if ($updatedAccountSize >= $limit) { abort(403, 'Account size limit reached.'); } } @@ -1767,8 +1824,6 @@ class ApiV1Controller extends Controller $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.'); @@ -1831,6 +1886,10 @@ class ApiV1Controller extends Controller 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(); @@ -1971,12 +2030,16 @@ class ApiV1Controller extends Controller $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; - }); + $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 ($size >= $limit) { + if ($updatedAccountSize >= $limit) { abort(403, 'Account size limit reached.'); } } @@ -1984,8 +2047,6 @@ class ApiV1Controller extends Controller $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.'); @@ -2053,6 +2114,10 @@ class ApiV1Controller extends Controller 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(); @@ -2145,7 +2210,7 @@ class ApiV1Controller extends Controller } $count = UserFilterService::muteCount($pid); - $maxLimit = intval(config('instance.user_filters.max_user_mutes')); + $maxLimit = (int) config_cache('instance.user_filters.max_user_mutes'); if ($count == 0) { $filterCount = UserFilter::whereUserId($pid) ->whereFilterType('mute') @@ -2207,9 +2272,10 @@ class ApiV1Controller extends Controller if ($filter) { $filter->delete(); UserFilterService::unmute($pid, $profile->id); - RelationshipService::refresh($pid, $id); } + RelationshipService::refresh($pid, $id); + $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); $res = $this->fractal->createData($resource)->toArray(); @@ -2233,14 +2299,17 @@ class ApiV1Controller extends Controller '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', ]); $pid = $request->user()->profile_id; $limit = $request->input('limit', 20); + $ogLimit = $request->input('limit', 20); if ($limit > 40) { $limit = 40; + $ogLimit = 40; } $since = $request->input('since_id'); @@ -2258,6 +2327,10 @@ class ApiV1Controller extends Controller $types = $request->input('types'); + if ($request->has('types')) { + $limit = 150; + } + $maxId = null; $minId = null; AccountService::setLastActive($request->user()->id); @@ -2280,7 +2353,12 @@ class ApiV1Controller extends Controller } } - $baseUrl = config('app.url').'/api/v1/notifications?limit='.$limit.'&'; + 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.'&'; + } if ($minId == $maxId) { $minId = null; @@ -2322,7 +2400,16 @@ class ApiV1Controller extends Controller } return true; - })->values(); + }) + ->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"'; @@ -3019,9 +3106,9 @@ class ApiV1Controller extends Controller abort_unless($request->user()->tokenCan('read'), 403); $user = $request->user(); - AccountService::setLastActive($user->id); $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); @@ -3048,7 +3135,9 @@ class ApiV1Controller extends Controller $descendants = []; if ($status['in_reply_to_id']) { - $ancestors[] = StatusService::getMastodon($status['in_reply_to_id'], false); + $ancestors[] = $pe ? + StatusService::get($status['in_reply_to_id'], false) : + StatusService::getMastodon($status['in_reply_to_id'], false); } if ($status['replies_count']) { @@ -3058,8 +3147,10 @@ class ApiV1Controller extends Controller ->where('in_reply_to_id', $id) ->limit(20) ->pluck('id') - ->map(function ($sid) { - return StatusService::getMastodon($sid, false); + ->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); @@ -3308,9 +3399,9 @@ class ApiV1Controller extends Controller abort_unless($request->user()->tokenCan('write'), 403); $this->validate($request, [ - 'status' => 'nullable|string|max:' . config_cache('pixelfed.max_caption_length'), + 'status' => 'nullable|string|max:'.(int) config_cache('pixelfed.max_caption_length'), 'in_reply_to_id' => 'nullable', - 'media_ids' => 'sometimes|array|max:'.config_cache('pixelfed.max_album_length'), + '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', @@ -3360,10 +3451,9 @@ class ApiV1Controller extends Controller $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) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->where('created_at', '>', now()->subDays(1)) + ->where('id', '>', $minId) ->count(); return $dailyLimit >= 1000; @@ -3436,7 +3526,7 @@ class ApiV1Controller extends Controller $mimes = []; foreach ($ids as $k => $v) { - if ($k + 1 > config_cache('pixelfed.max_album_length')) { + if ($k + 1 > (int) config_cache('pixelfed.max_album_length')) { continue; } $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v); @@ -3712,7 +3802,6 @@ class ApiV1Controller extends Controller } $res = StatusHashtag::whereHashtagId($tag->id) - ->whereIn('status_visibility', ['public', 'private', 'unlisted']) ->where('status_id', $dir, $id) ->orderBy('status_id', 'desc') ->limit(100) @@ -3729,11 +3818,11 @@ class ApiV1Controller extends Controller return false; } } - if ($i['visibility'] === 'private') { - if ((int) $i['account']['id'] !== $pid) { - return FollowerService::follows($pid, $i['account']['id'], true); - } - } + // 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; @@ -4199,4 +4288,26 @@ class ApiV1Controller extends Controller return $this->json([]); } + + /** + * GET /api/v1/instance/peers + * + * + * @return array + */ + public function instancePeers(Request $request) + { + if ((bool) config('instance.show_peers') == false) { + return $this->json([]); + } + + 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 6d051866b..efd04c60d 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php @@ -2,914 +2,927 @@ 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\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\ProfileStatusService; +use App\Services\PublicTimelineService; +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\FollowerService; -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\Autolink; 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 NotificationChannels\Expo\ExpoPushToken; 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) - { - 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' - ]); - - $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'); - - $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() || !$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'); - } 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() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + 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', + ]); + + $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 []; + } + + 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 + 'user_id' => $uid, ]); - if(!$settings->other) { + if (! $settings->other) { return []; } - return $settings->other; - } + + return $settings->other; + } public function setWebSettings(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('write'), 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; @@ -923,18 +936,320 @@ class ApiV1Dot1Controller extends Controller public function getMutualAccounts(Request $request, $id) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('follows'), 403); + 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 []; } + if (! $account || ! isset($account['id'])) { + return []; + } $res = collect(FollowerService::mutualAccounts($request->user()->profile_id, $id)) - ->map(function($accountId) { + ->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 getExpoPushNotifications(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('push'), 403); + abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.'); + $user = $request->user(); + $res = [ + 'expo_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 disableExpoPushNotifications(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('push'), 403); + abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.'); + $request->user()->update([ + 'expo_token' => null, + ]); + + return $this->json(['expo_token' => null]); + } + + public function updateExpoPushNotifications(Request $request) + { + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('push'), 403); + abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.'); + $this->validate($request, [ + 'expo_token' => ['required', ExpoPushToken::rule()], + 'notify_like' => 'sometimes', + 'notify_follow' => 'sometimes', + 'notify_mention' => 'sometimes', + 'notify_comment' => 'sometimes', + ]); + + $user = $request->user()->update([ + 'expo_token' => $request->input('expo_token'), + 'notify_like' => $request->has('notify_like') && $request->boolean('notify_like'), + 'notify_follow' => $request->has('notify_follow') && $request->boolean('notify_follow'), + 'notify_mention' => $request->has('notify_mention') && $request->boolean('notify_mention'), + 'notify_comment' => $request->has('notify_comment') && $request->boolean('notify_comment'), + ]); + + $res = [ + 'expo_token' => (bool) $request->user()->expo_token, + 'notify_like' => (bool) $request->user()->notify_like, + 'notify_follow' => (bool) $request->user()->notify_follow, + 'notify_mention' => (bool) $request->user()->notify_mention, + 'notify_comment' => (bool) $request->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 []; + } + + $content = strip_tags($request->input('status')); + $rendered = Autolink::create()->autolink($content); + $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 = $rendered; + $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); } } diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index ebdc851b8..ee193e8af 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -2,44 +2,32 @@ 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; -use App\Services\UserRoleService; class ApiV2Controller extends Controller { - const PF_API_ENTITY_KEY = "_pe"; + const PF_API_ENTITY_KEY = '_pe'; public function json($res, $code = 200, $headers = []) { @@ -49,10 +37,11 @@ class ApiV2Controller extends Controller public function instance(Request $request) { $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () { - if(config_cache('instance.admin.pid')) { + 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; @@ -61,41 +50,42 @@ class ApiV2Controller extends Controller $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() : []; + ->map(function ($rule, $key) { + $id = $key + 1; + + return [ + 'id' => "{$id}", + 'text' => $rule, + ]; + }) + ->toArray() : []; }); - $res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use($contact, $rules) { + $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') .')', + '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() - ] + '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')) - ] + '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), + ], ], 'languages' => [config('app.locale')], 'configuration' => [ 'urls' => [ 'streaming' => null, - 'status' => null + 'status' => null, ], 'vapid' => [ 'public_key' => config('webpush.vapid.public_key'), @@ -106,7 +96,7 @@ class ApiV2Controller extends Controller '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 + 'characters_reserved_per_url' => 23, ], 'media_attachments' => [ 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')), @@ -114,7 +104,7 @@ class ApiV2Controller extends Controller 'image_matrix_limit' => 3686400, 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, 'video_frame_rate_limit' => 240, - 'video_matrix_limit' => 3686400 + 'video_matrix_limit' => 3686400, ], 'polls' => [ 'max_options' => 0, @@ -134,14 +124,15 @@ class ApiV2Controller extends Controller ], 'contact' => [ 'email' => config('instance.email'), - 'account' => $contact + 'account' => $contact, ], - 'rules' => $rules + '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); } @@ -153,7 +144,7 @@ class ApiV2Controller extends Controller */ public function search(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); + abort_if(! $request->user() || ! $request->user()->token(), 403); abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ @@ -166,18 +157,19 @@ class ApiV2Controller extends Controller 'resolve' => 'nullable', 'limit' => 'nullable|integer|max:40', 'offset' => 'nullable|integer', - 'following' => 'nullable' + 'following' => 'nullable', ]); - if($request->user()->has_roles && !UserRoleService::can('can-view-discover', $request->user()->id)) { + if ($request->user()->has_roles && ! UserRoleService::can('can-view-discover', $request->user()->id)) { return [ 'accounts' => [], 'hashtags' => [], - 'statuses' => [] + 'statuses' => [], ]; } - $mastodonMode = !$request->has('_pe'); + $mastodonMode = ! $request->has('_pe'); + return $this->json(SearchApiV2Service::query($request, $mastodonMode)); } @@ -193,7 +185,7 @@ class ApiV2Controller extends Controller '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') + 'cluster' => config('broadcasting.connections.pusher.options.cluster'), ] : []; } @@ -205,39 +197,39 @@ class ApiV2Controller extends Controller */ public function mediaUploadV2(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); + 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'), + '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'), + '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' + '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) { + if ($user->last_active_at == null) { return []; } - if(empty($request->file('file'))) { + if (empty($request->file('file'))) { return response('', 422); } - $limitKey = 'compose:rate-limit:media-upload:' . $user->id; + $limitKey = 'compose:rate-limit:media-upload:'.$user->id; $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); return $dailyLimit >= 1250; @@ -246,23 +238,25 @@ class ApiV2Controller extends Controller $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; - }); + $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 ($size >= $limit) { - abort(403, 'Account size limit reached.'); + 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; - $photo = $request->file('file'); - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), $mimes) == false) { + if (in_array($photo->getMimeType(), $mimes) == false) { abort(403, 'Invalid or unsupported mime type.'); } @@ -274,24 +268,24 @@ class ApiV2Controller extends Controller $settings = UserSetting::whereUserId($user->id)->first(); - if($settings && !empty($settings->compose_settings)) { + if ($settings && ! empty($settings->compose_settings)) { $compose = $settings->compose_settings; - if(isset($compose['default_license']) && $compose['default_license'] != 1) { + 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')) { + 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) { + if ($removeMedia) { MediaDeletePipeline::dispatch($removeMedia) ->onQueue('mmo') ->delay(now()->addMinutes(15)); @@ -309,7 +303,7 @@ class ApiV2Controller extends Controller $media->caption = $request->input('description'); $media->filter_class = $filterClass; $media->filter_name = $filterName; - if($license) { + if ($license) { $media->license = $license; } $media->save(); @@ -327,13 +321,18 @@ class ApiV2Controller extends Controller 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['preview_url'] = $media->url().'?v='.time(); $res['url'] = null; + return $this->json($res, 202); } } 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/DomainBlockController.php b/app/Http/Controllers/Api/V1/DomainBlockController.php index 5a2698361..3b6730789 100644 --- a/app/Http/Controllers/Api/V1/DomainBlockController.php +++ b/app/Http/Controllers/Api/V1/DomainBlockController.php @@ -72,7 +72,7 @@ class DomainBlockController extends Controller abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server'); $existingCount = UserDomainBlock::whereProfileId($pid)->count(); - $maxLimit = config('instance.user_filters.max_domain_blocks'); + $maxLimit = (int) config_cache('instance.user_filters.max_domain_blocks'); $errorMsg = __('profile.block.domain.max', ['max' => $maxLimit]); abort_if($existingCount >= $maxLimit, 400, $errorMsg); 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 627a879cc..86ee52c84 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -74,10 +74,10 @@ class LoginController extends Controller $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') ) diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 8bdd57bf8..230daea85 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -3,234 +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 - */ - public 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 - */ - public 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((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); - 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); - } - } - } + 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/CollectionController.php b/app/Http/Controllers/CollectionController.php index 85f3f30cf..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,20 +131,20 @@ 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'); } @@ -160,10 +155,10 @@ class CollectionController extends Controller $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::deleteCollection($collection->id); @@ -177,112 +172,112 @@ class CollectionController extends Controller 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) { + ->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(); + }) + ->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'); @@ -290,7 +285,7 @@ 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!'); } @@ -308,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(); }); @@ -319,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/ComposeController.php b/app/Http/Controllers/ComposeController.php index 341d56ea8..cfd44969b 100644 --- a/app/Http/Controllers/ComposeController.php +++ b/app/Http/Controllers/ComposeController.php @@ -21,6 +21,7 @@ use App\Services\MediaStorageService; use App\Services\MediaTagService; use App\Services\SnowflakeService; use App\Services\UserRoleService; +use App\Services\UserStorageService; use App\Status; use App\Transformer\Api\MediaTransformer; use App\UserFilter; @@ -70,7 +71,7 @@ class ComposeController extends Controller 'filter_class' => 'nullable|alpha_dash|max:24', ]); - $user = Auth::user(); + $user = $request->user(); $profile = $user->profile; abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); @@ -84,21 +85,22 @@ class ComposeController extends Controller 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; - }); + $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; + + if ((bool) config_cache('pixelfed.enforce_account_limit') == true) { $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { + 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; - - $photo = $request->file('file'); - $mimes = explode(',', config_cache('pixelfed.media_types')); abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format'); @@ -143,6 +145,10 @@ class ComposeController extends Controller 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(); @@ -198,6 +204,7 @@ class ComposeController extends Controller ]; ImageOptimize::dispatch($media)->onQueue('mmo'); Cache::forget($limitKey); + UserStorageService::recalculateUpdateStorageUsed($request->user()->id); return $res; } @@ -218,6 +225,8 @@ class ComposeController extends Controller MediaStorageService::delete($media, true); + UserStorageService::recalculateUpdateStorageUsed($request->user()->id); + return response()->json([ 'msg' => 'Successfully deleted', 'code' => 200, @@ -494,17 +503,17 @@ class ComposeController extends Controller $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(); + // $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; - }); + // return $dailyLimit >= 1000; + // }); - abort_if($limitReached == true, 429); + // abort_if($limitReached == true, 429); $license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null; @@ -626,7 +635,6 @@ class ComposeController extends Controller 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); @@ -741,7 +749,7 @@ class ComposeController extends Controller case 'image/jpeg': case 'image/png': case 'video/mp4': - $finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at; + $finished = (bool) config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at; break; default: diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php index 0d91d4f17..1a30032cd 100644 --- a/app/Http/Controllers/DirectMessageController.php +++ b/app/Http/Controllers/DirectMessageController.php @@ -2,31 +2,28 @@ 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 App\Jobs\StatusPipeline\StatusDelete; -use Illuminate\Support\Str; -use App\Util\ActivityPub\Helpers; -use App\Services\AccountService; -use App\Services\StatusService; -use App\Services\WebfingerService; -use App\Models\Conversation; +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 Illuminate\Http\Request; +use Illuminate\Support\Str; class DirectMessageController extends Controller { @@ -39,260 +36,260 @@ class DirectMessageController extends Controller { $this->validate($request, [ 'a' => 'nullable|string|in:inbox,sent,filtered', - 'page' => 'nullable|integer|min:1|max:99' + 'page' => 'nullable|integer|min:1|max:99', ]); $user = $request->user(); - if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id)) { + 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') { + 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(); + ->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') { + 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' => [] - ]; - }); + ->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') { + 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' => [] - ]; - }); + ->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') { + } 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' => [] - ]; - }); + ->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') { + 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' => [] - ]; - }); + ->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') { + 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' => [] - ]; - }); + ->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' => [], + ]; + }); } } @@ -304,19 +301,20 @@ class DirectMessageController extends Controller $this->validate($request, [ 'to_id' => 'required', 'message' => 'required|string|min:1|max:500', - 'type' => 'required|in:text,emoji' + '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'); + abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + 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) { + if ((! $recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) { + if ($recipient->follows($profile) == true) { $hidden = false; } else { $hidden = true; @@ -345,35 +343,35 @@ class DirectMessageController extends Controller Conversation::updateOrInsert( [ 'to_id' => $recipient->id, - 'from_id' => $profile->id + 'from_id' => $profile->id, ], [ 'type' => $dm->type, 'status_id' => $status->id, 'dm_id' => $dm->id, - 'is_hidden' => $hidden + 'is_hidden' => $hidden, ] ); - if(filter_var($msg, FILTER_VALIDATE_URL)) { - if(Helpers::validateUrl($msg)) { + 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) + 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(); + ->whereFilterableId($profile->id) + ->whereFilterableType('App\Profile') + ->whereFilterType('dm.mute') + ->exists(); - if($recipient->domain == null && $hidden == false && !$nf) { + if ($recipient->domain == null && $hidden == false && ! $nf) { $notification = new Notification(); $notification->profile_id = $recipient->id; $notification->actor_id = $profile->id; @@ -383,7 +381,7 @@ class DirectMessageController extends Controller $notification->save(); } - if($recipient->domain) { + if ($recipient->domain) { $this->remoteDeliver($dm); } @@ -392,12 +390,12 @@ class DirectMessageController extends Controller 'isAuthor' => $profile->id == $dm->from_id, 'reportId' => (string) $dm->status_id, 'hidden' => (bool) $dm->is_hidden, - 'type' => $dm->type, + 'type' => $dm->type, 'text' => $dm->status->caption, 'media' => null, - 'timeAgo' => $dm->created_at->diffForHumans(null,null,true), + 'timeAgo' => $dm->created_at->diffForHumans(null, null, true), 'seen' => $dm->read_at != null, - 'meta' => $dm->meta + 'meta' => $dm->meta, ]; return response()->json($res); @@ -406,10 +404,10 @@ class DirectMessageController extends Controller public function thread(Request $request) { $this->validate($request, [ - 'pid' => 'required' + 'pid' => 'required', ]); $user = $request->user(); - abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + 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'); @@ -418,54 +416,58 @@ class DirectMessageController extends Controller $r = Profile::findOrFail($pid); - if($min_id) { + 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) { + ->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(); + } elseif ($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(); + ->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]]); + $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(); + ->latest() + ->take(8) + ->get(); } - $res = $res->filter(function($s) { + $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(); + ->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, @@ -473,25 +475,23 @@ class DirectMessageController extends Controller '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, + '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 + 'messages' => $res, ]; - return response()->json($w, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + return response()->json($w, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } public function delete(Request $request) { $this->validate($request, [ - 'id' => 'required' + 'id' => 'required', ]); $sid = $request->input('id'); @@ -506,30 +506,30 @@ class DirectMessageController extends Controller $recipient = AccountService::get($dm->to_id); - if(!$recipient) { + if (! $recipient) { return response('', 422); } - if($recipient['local'] == false) { + if ($recipient['local'] == false) { $dmc = $dm; $this->remoteDelete($dmc); } else { StatusDelete::dispatch($status)->onQueue('high'); } - if(Conversation::whereStatusId($sid)->count()) { + 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) { + 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 + 'is_hidden' => false, ]); Conversation::where(['to_id' => $dm->to_id, 'from_id' => $dm->from_id]) @@ -537,19 +537,19 @@ class DirectMessageController extends Controller 'updated_at' => $latest->updated_at, 'status_id' => $latest->status_id, 'type' => $latest->type, - 'is_hidden' => false + 'is_hidden' => false, ]); } else { Conversation::where([ 'status_id' => $sid, 'to_id' => $dm->from_id, - 'from_id' => $dm->to_id + 'from_id' => $dm->to_id, ])->delete(); Conversation::where([ 'status_id' => $sid, 'from_id' => $dm->from_id, - 'to_id' => $dm->to_id + 'to_id' => $dm->to_id, ])->delete(); } } @@ -557,41 +557,43 @@ class DirectMessageController extends Controller 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'); + 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); + + return response()->json($dm, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } public function mediaUpload(Request $request) { $this->validate($request, [ - 'file' => function() { + 'file' => function () { return [ 'required', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), + 'mimetypes:'.config_cache('pixelfed.media_types'), + 'max:'.config_cache('pixelfed.max_photo_size'), ]; }, - 'to_id' => 'required' + 'to_id' => 'required', ]); $user = $request->user(); - abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + 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) { + if ((! $recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) { + if ($recipient->follows($profile) == true) { $hidden = false; } else { $hidden = true; @@ -600,23 +602,26 @@ class DirectMessageController extends Controller $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; - }); + $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 ($size >= $limit) { + if ($updatedAccountSize >= $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) { + if (in_array($photo->getMimeType(), $mimes) == false) { abort(403, 'Invalid or unsupported mime type.'); } - $storagePath = MediaPathService::get($user, 2) . Str::random(8); + $storagePath = MediaPathService::get($user, 2).Str::random(8); $path = $photo->storePublicly($storagePath); $hash = \hash_file('sha256', $photo); @@ -655,17 +660,21 @@ class DirectMessageController extends Controller Conversation::updateOrInsert( [ 'to_id' => $recipient->id, - 'from_id' => $profile->id + 'from_id' => $profile->id, ], [ 'type' => $dm->type, 'status_id' => $status->id, 'dm_id' => $dm->id, - 'is_hidden' => $hidden + 'is_hidden' => $hidden, ] ); - if($recipient->domain) { + $user->storage_used = (int) $updatedAccountSize; + $user->storage_used_updated_at = now(); + $user->save(); + + if ($recipient->domain) { $this->remoteDeliver($dm); } @@ -673,7 +682,7 @@ class DirectMessageController extends Controller 'id' => $dm->id, 'reportId' => (string) $dm->status_id, 'type' => $dm->type, - 'url' => $media->url() + 'url' => $media->url(), ]; } @@ -685,55 +694,56 @@ class DirectMessageController extends Controller ]); $user = $request->user(); - if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id)) { + 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('.')) { + if ($r && ! Str::of($q)->contains('.')) { return []; } - if($r && Helpers::validateUrl($q)) { + if ($r && Helpers::validateUrl($q)) { Helpers::profileFetch($q); } - if(Str::of($q)->startsWith('@')) { - if(strlen($q) < 3) { + if (Str::of($q)->startsWith('@')) { + if (strlen($q) < 3) { return []; } - if(substr_count($q, '@') == 2) { + 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'); + ->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 - ]; - }); + $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; } @@ -742,21 +752,21 @@ class DirectMessageController extends Controller { $this->validate($request, [ 'pid' => 'required', - 'sid' => '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'); + 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(); + ->whereFromId($pid) + ->where('status_id', '>=', $sid) + ->get(); $now = now(); - foreach($dms as $dm) { + foreach ($dms as $dm) { $dm->read_at = $now; $dm->save(); } @@ -767,11 +777,11 @@ class DirectMessageController extends Controller public function mute(Request $request) { $this->validate($request, [ - 'id' => 'required' + 'id' => 'required', ]); $user = $request->user(); - abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + 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; @@ -780,7 +790,7 @@ class DirectMessageController extends Controller 'user_id' => $pid, 'filterable_id' => $fid, 'filterable_type' => 'App\Profile', - 'filter_type' => 'dm.mute' + 'filter_type' => 'dm.mute', ] ); @@ -790,20 +800,20 @@ class DirectMessageController extends Controller public function unmute(Request $request) { $this->validate($request, [ - 'id' => 'required' + 'id' => 'required', ]); $user = $request->user(); - abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + 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(); + ->whereFilterableId($fid) + ->whereFilterableType('App\Profile') + ->whereFilterType('dm.mute') + ->firstOrFail(); $f->delete(); @@ -820,7 +830,7 @@ class DirectMessageController extends Controller 'type' => 'Mention', 'href' => $dm->recipient->permalink(), 'name' => $dm->recipient->emailUrl(), - ] + ], ]; $body = [ @@ -828,34 +838,34 @@ class DirectMessageController extends Controller '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' => [], + '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) { + '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(), + 'type' => $media->activityVerb(), 'mediaType' => $media->mime, - 'url' => $media->url(), - 'name' => $media->caption, + 'url' => $media->url(), + 'name' => $media->caption, ]; })->toArray(), - 'tag' => $tags, - ] + 'tag' => $tags, + ], ]; DirectDeliverPipeline::dispatch($profile, $url, $body)->onQueue('high'); @@ -872,14 +882,14 @@ class DirectMessageController extends Controller ], 'id' => $dm->status->permalink('#delete'), 'to' => [ - 'https://www.w3.org/ns/activitystreams#Public' + 'https://www.w3.org/ns/activitystreams#Public', ], 'type' => 'Delete', 'actor' => $dm->status->profile->permalink(), 'object' => [ 'id' => $dm->status->url(), - 'type' => 'Tombstone' - ] + 'type' => 'Tombstone', + ], ]; DirectDeletePipeline::dispatch($profile, $url, $body)->onQueue('high'); } diff --git a/app/Http/Controllers/DiscoverController.php b/app/Http/Controllers/DiscoverController.php index c9e93eecf..b3047ff79 100644 --- a/app/Http/Controllers/DiscoverController.php +++ b/app/Http/Controllers/DiscoverController.php @@ -11,6 +11,7 @@ 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\SnowflakeService; @@ -420,4 +421,11 @@ class DiscoverController extends Controller 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 55c7b4393..5738292f1 100644 --- a/app/Http/Controllers/FederationController.php +++ b/app/Http/Controllers/FederationController.php @@ -2,57 +2,42 @@ 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\Services\InstanceService; +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); + abort_if(! config('federation.nodeinfo.enabled'), 404); + return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES) - ->header('Access-Control-Allow-Origin','*'); + ->header('Access-Control-Allow-Origin', '*'); } public function nodeinfo() { - abort_if(!config('federation.nodeinfo.enabled'), 404); + abort_if(! config('federation.nodeinfo.enabled'), 404); + return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES) - ->header('Access-Control-Allow-Origin','*'); + ->header('Access-Control-Allow-Origin', '*'); } public function webfinger(Request $request) { - if (!config('federation.webfinger.enabled') || - !$request->has('resource') || - !$request->filled('resource') + if (! config('federation.webfinger.enabled') || + ! $request->has('resource') || + ! $request->filled('resource') ) { return response('', 400); } @@ -60,55 +45,87 @@ class FederationController extends Controller $resource = $request->input('resource'); $domain = config('pixelfed.domain.app'); - if(config('federation.activitypub.sharedInbox') && - $resource == 'acct:' . $domain . '@' . $domain) { + // Instance Actor + if ( + config('federation.activitypub.sharedInbox') && + $resource == 'acct:'.$domain.'@'.$domain + ) { $res = [ - 'subject' => 'acct:' . $domain . '@' . $domain, + 'subject' => 'acct:'.$domain.'@'.$domain, 'aliases' => [ - 'https://' . $domain . '/i/actor' + 'https://'.$domain.'/i/actor', ], 'links' => [ [ 'rel' => 'http://webfinger.net/rel/profile-page', 'type' => 'text/html', - 'href' => 'https://' . $domain . '/site/kb/instance-actor' + 'href' => 'https://'.$domain.'/site/kb/instance-actor', ], [ 'rel' => 'self', 'type' => 'application/activity+json', - 'href' => 'https://' . $domain . '/i/actor' - ] - ] + 'href' => 'https://'.$domain.'/i/actor', + ], + ], ]; + return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); } + + 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); + + 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)) { + $key = 'federation:webfinger:sha256:'.$hash; + if ($cached = Cache::get($key)) { return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES); } - if(strpos($resource, $domain) == false) { + if (strpos($resource, $domain) == false) { return response('', 400); } $parsed = Nickname::normalizeProfileUrl($resource); - if(empty($parsed) || $parsed['domain'] !== $domain) { + if (empty($parsed) || $parsed['domain'] !== $domain) { return response('', 400); } $username = $parsed['username']; - $profile = Profile::whereNull('domain')->whereUsername($username)->first(); - if(!$profile || $profile->status !== null) { + $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); return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES) - ->header('Access-Control-Allow-Origin','*'); + ->header('Access-Control-Allow-Origin', '*'); } public function hostMeta(Request $request) { - abort_if(!config('federation.webfinger.enabled'), 404); + abort_if(! config('federation.webfinger.enabled'), 404); $path = route('well-known.webfinger'); $xml = ''; @@ -118,19 +135,19 @@ class FederationController extends Controller public function userOutbox(Request $request, $username) { - abort_if(!config_cache('federation.activitypub.enabled'), 404); + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); - if(!$request->wantsJson()) { - return redirect('/' . $username); + if (! $request->wantsJson()) { + return redirect('/'.$username); } $id = AccountService::usernameToId($username); - abort_if(!$id, 404); + abort_if(! $id, 404); $account = AccountService::get($id); - abort_if(!$account || !isset($account['statuses_count']), 404); + 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', + 'id' => 'https://'.config('pixelfed.domain.app').'/users/'.$username.'/outbox', 'type' => 'OrderedCollection', 'totalItems' => $account['statuses_count'] ?? 0, ]; @@ -140,135 +157,145 @@ class FederationController extends Controller public function userInbox(Request $request, $username) { - abort_if(!config_cache('federation.activitypub.enabled'), 404); - abort_if(!config('federation.activitypub.inbox'), 404); + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); + abort_if(! config('federation.activitypub.inbox'), 404); $headers = $request->headers->all(); $payload = $request->getContent(); - if(!$payload || empty($payload)) { + if (! $payload || empty($payload)) { return; } $obj = json_decode($payload, true, 8); - if(!isset($obj['id'])) { + if (! isset($obj['id'])) { return; } $domain = parse_url($obj['id'], PHP_URL_HOST); - if(in_array($domain, InstanceService::getBannedDomains())) { + 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()) { + 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()) { + 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') { + 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'])) { + } 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'); } - return; + } public function sharedInbox(Request $request) { - abort_if(!config_cache('federation.activitypub.enabled'), 404); - abort_if(!config('federation.activitypub.sharedInbox'), 404); + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); + abort_if(! config('federation.activitypub.sharedInbox'), 404); $headers = $request->headers->all(); $payload = $request->getContent(); - if(!$payload || empty($payload)) { + if (! $payload || empty($payload)) { return; } $obj = json_decode($payload, true, 8); - if(!isset($obj['id'])) { + if (! isset($obj['id'])) { return; } $domain = parse_url($obj['id'], PHP_URL_HOST); - if(in_array($domain, InstanceService::getBannedDomains())) { + 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()) { + 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()) { + 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') { + 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'])) { + } 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'); } - return; + } public function userFollowing(Request $request, $username) { - abort_if(!config_cache('federation.activitypub.enabled'), 404); + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); $id = AccountService::usernameToId($username); - abort_if(!$id, 404); + abort_if(! $id, 404); $account = AccountService::get($id); - abort_if(!$account || !isset($account['following_count']), 404); + abort_if(! $account || ! isset($account['following_count']), 404); $obj = [ '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $request->getUri(), - 'type' => 'OrderedCollection', + '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(!config_cache('federation.activitypub.enabled'), 404); + abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404); $id = AccountService::usernameToId($username); - abort_if(!$id, 404); + abort_if(! $id, 404); $account = AccountService::get($id); - abort_if(!$account || !isset($account['followers_count']), 404); + abort_if(! $account || ! isset($account['followers_count']), 404); $obj = [ '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $request->getUri(), - 'type' => 'OrderedCollection', + '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..7f45f74a4 --- /dev/null +++ b/app/Http/Controllers/GroupFederationController.php @@ -0,0 +1,103 @@ +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 + + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $gp->url(), + + 'type' => 'Note', + + 'summary' => null, + 'content' => $status->rendered ?? $status->caption, + '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/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/PixelfedDirectoryController.php b/app/Http/Controllers/PixelfedDirectoryController.php index cfe3f690a..0477c5170 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,40 +45,40 @@ 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); } @@ -85,27 +89,27 @@ class PixelfedDirectoryController extends Controller $res['curated_onboarding'] = $curatedOnboarding; $oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first(); - if($oauthEnabled) { + if ($oauthEnabled) { $keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-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') && @@ -115,29 +119,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'); @@ -150,19 +159,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/ProfileController.php b/app/Http/Controllers/ProfileController.php index 6471ed760..fba1e40b3 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -172,6 +172,8 @@ class ProfileController extends Controller $user = $this->getCachedUser($username); + abort_if(! $user, 404); + return redirect($user->url()); } @@ -252,7 +254,7 @@ class ProfileController extends Controller abort_if(! $profile || $profile['locked'] || ! $profile['local'], 404); - $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 86400, function () use ($profile) { + $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 3600, function () use ($profile) { $uid = User::whereProfileId($profile['id'])->first(); if (! $uid) { return true; @@ -267,7 +269,7 @@ class ProfileController extends Controller abort_if($aiCheck, 404); - $enabled = Cache::remember('profile:atom:enabled:'.$profile['id'], 84600, function () use ($profile) { + $enabled = Cache::remember('profile:atom:enabled:'.$profile['id'], 86400, function () use ($profile) { $uid = User::whereProfileId($profile['id'])->first(); if (! $uid) { return false; @@ -346,7 +348,7 @@ class ProfileController extends Controller return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); } - $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 86400, function () use ($profile) { + $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; @@ -359,7 +361,7 @@ class ProfileController extends Controller return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); } - if (AccountService::canEmbed($profile->user_id) == false) { + if (AccountService::canEmbed($profile->id) == false) { return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); } @@ -371,7 +373,7 @@ class ProfileController extends Controller public function stories(Request $request, $username) { - abort_if(! config_cache('instance.stories.enabled') || ! $request->user(), 404); + 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; diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index 78008eda4..b0e2efc40 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -2,47 +2,28 @@ namespace App\Http\Controllers; -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 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 League\Fractal; +use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use League\Fractal\Serializer\ArraySerializer; class PublicApiController extends Controller { @@ -56,7 +37,7 @@ class PublicApiController extends Controller protected function getUserData($user) { - if(!$user) { + if (! $user) { return []; } else { return AccountService::get($user->profile_id); @@ -65,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) @@ -88,9 +69,9 @@ 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()); @@ -107,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' => [], @@ -118,6 +99,7 @@ class PublicApiController extends Controller 'bookmarked' => false, ], ]; + return response()->json($res); } $res = [ @@ -130,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; @@ -146,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', 'rendered', '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', 'rendered', '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', 'rendered', '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->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); } @@ -199,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); } } @@ -225,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); } @@ -244,123 +228,126 @@ 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', + '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(); $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); @@ -368,17 +355,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'; @@ -390,7 +377,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); @@ -398,8 +385,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(); }); @@ -409,123 +397,126 @@ 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', + '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(); } 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', + '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(); } } 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'); @@ -538,99 +529,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); @@ -638,23 +632,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); } @@ -662,10 +656,11 @@ class PublicApiController extends Controller public function account(Request $request, $id) { $res = AccountService::get($id); - if($res && isset($res['local'], $res['url']) && !$res['local']) { + 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); } @@ -675,17 +670,17 @@ 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']) { + 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); } @@ -696,28 +691,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']; } @@ -725,38 +722,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..9388d3abd 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -2,368 +2,367 @@ 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 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(); + $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(), + ]]; + } + + $remote = Helpers::fetchFromUrl($tag); + + 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(); + } } diff --git a/app/Http/Controllers/Settings/HomeSettings.php b/app/Http/Controllers/Settings/HomeSettings.php index 99326c097..ce411e4fd 100644 --- a/app/Http/Controllers/Settings/HomeSettings.php +++ b/app/Http/Controllers/Settings/HomeSettings.php @@ -4,21 +4,17 @@ 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 { @@ -40,11 +36,11 @@ trait HomeSettings 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'), + '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' + 'pronouns' => 'nullable|array|max:4', ]); $changes = false; @@ -57,14 +53,14 @@ trait HomeSettings $pronouns = $request->input('pronouns'); $existingPronouns = PronounService::get($profile->id); $layout = $request->input('profile_layout'); - if($layout) { - $layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout; + if ($layout) { + $layout = ! in_array($layout, ['metro', 'moment']) ? 'metro' : $layout; } $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 (! $enforceEmailVerification || ! $changes && $user->email_verified_at) { if ($profile->name != $name) { $changes = true; $user->name = $name; @@ -81,7 +77,7 @@ trait HomeSettings $profile->bio = Autolink::create()->autolink($bio); } - if($user->language != $language && + if ($user->language != $language && in_array($language, \App\Util\Localization\Localization::languages()) ) { $changes = true; @@ -89,8 +85,8 @@ trait HomeSettings session()->put('locale', $language); } - if($existingPronouns != $pronouns) { - if($pronouns && in_array('Select Pronoun(s)', $pronouns)) { + if ($existingPronouns != $pronouns) { + if ($pronouns && in_array('Select Pronoun(s)', $pronouns)) { PronounService::clear($profile->id); } else { PronounService::put($profile->id, $pronouns); @@ -102,7 +98,9 @@ trait HomeSettings $user->save(); $profile->save(); Cache::forget('user:account:id:'.$user->id); + AccountService::forgetAccountSettings($profile->id); AccountService::del($profile->id); + return redirect('/settings/home')->with('status', 'Profile successfully updated!'); } @@ -117,10 +115,10 @@ trait HomeSettings public function passwordUpdate(Request $request) { $this->validate($request, [ - 'current' => 'required|string', - 'password' => 'required|string', - 'password_confirmation' => 'required|string', - ]); + 'current' => 'required|string', + 'password' => 'required|string', + 'password_confirmation' => 'required|string', + ]); $current = $request->input('current'); $new = $request->input('password'); @@ -144,6 +142,7 @@ trait HomeSettings $log->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.'); @@ -159,7 +158,7 @@ trait HomeSettings public function emailUpdate(Request $request) { $this->validate($request, [ - 'email' => 'required|email|unique:users,email', + 'email' => 'required|email|unique:users,email', ]); $changes = false; $email = $request->input('email'); diff --git a/app/Http/Controllers/Settings/PrivacySettings.php b/app/Http/Controllers/Settings/PrivacySettings.php index 6509071e0..c9caa168d 100644 --- a/app/Http/Controllers/Settings/PrivacySettings.php +++ b/app/Http/Controllers/Settings/PrivacySettings.php @@ -2,30 +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; -use App\Models\UserDomainBlock; 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')); } @@ -34,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') { @@ -67,7 +79,7 @@ trait PrivacySettings } else { $settings->{$field} = true; } - } elseif ($field == 'public_dm') { + } elseif ($field == 'public_dm') { if ($form == 'on') { $settings->{$field} = true; } else { @@ -85,33 +97,36 @@ trait PrivacySettings $settings->save(); } $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)); + 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; @@ -123,6 +138,8 @@ trait PrivacySettings ->firstOrFail(); $filter->delete(); }); + RelationshipService::refresh($pid, $fid); + return redirect()->back(); } @@ -131,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; @@ -150,6 +167,8 @@ trait PrivacySettings ->firstOrFail(); $filter->delete(); }); + RelationshipService::refresh($pid, $fid); + return redirect()->back(); } @@ -194,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'); @@ -209,9 +228,9 @@ trait PrivacySettings case 'remove-all': Follower::whereFollowingId($profile->id)->delete(); break; - + default: - # code... + // code... break; } } @@ -221,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 5e205d64d..8c13e0b59 100644 --- a/app/Http/Controllers/SiteController.php +++ b/app/Http/Controllers/SiteController.php @@ -5,7 +5,7 @@ namespace App\Http\Controllers; use App\Page; use App\Profile; use App\Services\FollowerService; -use App\Status; +use App\Services\StatusService; use App\User; use App\Util\ActivityPub\Helpers; use App\Util\Localization\Localization; @@ -60,7 +60,7 @@ class SiteController extends Controller { return Cache::remember('site.about_v2', now()->addMinutes(15), function () { $user_count = number_format(User::count()); - $post_count = number_format(Status::count()); + $post_count = number_format(StatusService::totalLocalStatuses()); $rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null; return view('site.about', compact('rules', 'user_count', 'post_count'))->render(); diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index 4a3b3552d..ba02cd015 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -8,6 +8,7 @@ use App\Jobs\SharePipeline\UndoSharePipeline; 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; @@ -34,8 +35,21 @@ class StatusController extends Controller } } - $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); + $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); } @@ -70,18 +84,6 @@ class StatusController extends Controller } } - 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')); @@ -113,19 +115,41 @@ class StatusController extends Controller return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); } - $profile = Profile::whereNull(['domain', 'status']) - ->whereIsPrivate(false) - ->whereUsername($username) - ->first(); + $status = StatusService::get($id); - if (! $profile) { + if ( + ! $status || + ! isset($status['account'], $status['account']['id'], $status['local']) || + ! $status['local'] || + strtolower($status['account']['username']) !== strtolower($username) + ) { + $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'); } - $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(); + $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; } @@ -138,17 +162,22 @@ class StatusController extends Controller 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) { + + $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'; @@ -319,12 +348,17 @@ class StatusController extends Controller 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(); + $key = 'pf:status:ap:v1:sid:'.$status['id']; - return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + 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) diff --git a/app/Http/Controllers/Stories/StoryApiV1Controller.php b/app/Http/Controllers/Stories/StoryApiV1Controller.php index ca6a24791..5d0a15160 100644 --- a/app/Http/Controllers/Stories/StoryApiV1Controller.php +++ b/app/Http/Controllers/Stories/StoryApiV1Controller.php @@ -2,54 +2,56 @@ 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 { const RECENT_KEY = 'pf:stories:recent-by-id:'; + const RECENT_TTL = 300; public function carousel(Request $request) { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); $pid = $request->user()->profile_id; - if(config('database.default') == 'pgsql') { - $s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { + 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; + ->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(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { + $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) @@ -59,9 +61,9 @@ class StoryApiV1Controller extends Controller }); } - $nodes = $s->map(function($s) use($pid) { + $nodes = $s->map(function ($s) use ($pid) { $profile = AccountService::get($s->profile_id, true); - if(!$profile || !isset($profile['id'])) { + if (! $profile || ! isset($profile['id'])) { return false; } @@ -72,50 +74,51 @@ class StoryApiV1Controller extends Controller 'src' => url(Storage::url($s->path)), 'duration' => $s->duration ?? 3, 'seen' => StoryService::hasSeen($pid, $s->id), - 'created_at' => $s->created_at->format('c') + '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(); + ->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(); $res = [ 'self' => [], 'nodes' => $nodes, ]; - if(Story::whereProfileId($pid)->whereActive(true)->exists()) { + if (Story::whereProfileId($pid)->whereActive(true)->exists()) { $selfStories = Story::whereProfileId($pid) ->whereActive(true) ->get() - ->map(function($s) use($pid) { + ->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') + 'created_at' => $s->created_at->format('c'), ]; }) ->sortBy('id') @@ -127,38 +130,40 @@ class StoryApiV1Controller extends Controller 'username' => $selfProfile['acct'], 'avatar' => $selfProfile['avatar'], 'local' => $selfProfile['local'], - 'is_author' => true + 'is_author' => true, ], 'nodes' => $selfStories, ]; } - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + + return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } public function selfCarousel(Request $request) { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); $pid = $request->user()->profile_id; - if(config('database.default') == 'pgsql') { - $s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { + 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; + ->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(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { + $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) @@ -168,9 +173,9 @@ class StoryApiV1Controller extends Controller }); } - $nodes = $s->map(function($s) use($pid) { + $nodes = $s->map(function ($s) use ($pid) { $profile = AccountService::get($s->profile_id, true); - if(!$profile || !isset($profile['id'])) { + if (! $profile || ! isset($profile['id'])) { return false; } @@ -181,32 +186,33 @@ class StoryApiV1Controller extends Controller 'src' => url(Storage::url($s->path)), 'duration' => $s->duration ?? 3, 'seen' => StoryService::hasSeen($pid, $s->id), - 'created_at' => $s->created_at->format('c') + '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(); + ->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(); $selfProfile = AccountService::get($pid, true); $res = [ @@ -216,7 +222,7 @@ class StoryApiV1Controller extends Controller 'username' => $selfProfile['acct'], 'avatar' => $selfProfile['avatar'], 'local' => $selfProfile['local'], - 'is_author' => true + 'is_author' => true, ], 'nodes' => [], @@ -224,40 +230,41 @@ class StoryApiV1Controller extends Controller 'nodes' => $nodes, ]; - if(Story::whereProfileId($pid)->whereActive(true)->exists()) { + if (Story::whereProfileId($pid)->whereActive(true)->exists()) { $selfStories = Story::whereProfileId($pid) ->whereActive(true) ->get() - ->map(function($s) use($pid) { + ->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') + 'created_at' => $s->created_at->format('c'), ]; }) ->sortBy('id') ->values(); $res['self']['nodes'] = $selfStories; } - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + + return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } public function add(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() { + 'file' => function () { return [ 'required', 'mimetypes:image/jpeg,image/png,video/mp4', - 'max:' . config_cache('pixelfed.max_photo_size'), + 'max:'.config_cache('pixelfed.max_photo_size'), ]; }, - 'duration' => 'sometimes|integer|min:0|max:30' + 'duration' => 'sometimes|integer|min:0|max:30', ]); $user = $request->user(); @@ -267,7 +274,7 @@ class StoryApiV1Controller extends Controller ->where('expires_at', '>', now()) ->count(); - if($count >= Story::MAX_PER_DAY) { + if ($count >= Story::MAX_PER_DAY) { abort(418, 'You have reached your limit for new Stories today.'); } @@ -277,7 +284,7 @@ class StoryApiV1Controller extends Controller $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->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo'; $story->mime = $photo->getMimeType(); $story->path = $path; $story->local = true; @@ -290,10 +297,10 @@ class StoryApiV1Controller extends Controller $res = [ 'code' => 200, - 'msg' => 'Successfully added', + 'msg' => 'Successfully added', 'media_id' => (string) $story->id, - 'media_url' => url(Storage::url($url)) . '?v=' . time(), - 'media_type' => $story->type + 'media_url' => url(Storage::url($url)).'?v='.time(), + 'media_type' => $story->type, ]; return $res; @@ -301,13 +308,13 @@ class StoryApiV1Controller extends Controller public function publish(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, [ 'media_id' => 'required', 'duration' => 'required|integer|min:0|max:30', 'can_reply' => 'required|boolean', - 'can_react' => 'required|boolean' + 'can_react' => 'required|boolean', ]); $id = $request->input('media_id'); @@ -327,13 +334,13 @@ class StoryApiV1Controller extends Controller return [ 'code' => 200, - 'msg' => 'Successfully published', + 'msg' => 'Successfully published', ]; } public function delete(Request $request, $id) { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); $user = $request->user(); @@ -346,16 +353,16 @@ class StoryApiV1Controller extends Controller return [ 'code' => 200, - 'msg' => 'Successfully deleted' + 'msg' => 'Successfully deleted', ]; } public function viewed(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, [ - 'id' => 'required|min:1', + 'id' => 'required|min:1', ]); $id = $request->input('id'); @@ -367,44 +374,45 @@ class StoryApiV1Controller extends Controller $profile = $story->profile; - if($story->profile_id == $authed->id) { + if ($story->profile_id == $authed->id) { return []; } $publicOnly = (bool) $profile->followedBy($authed); - abort_if(!$publicOnly, 403); + abort_if(! $publicOnly, 403); $v = StoryView::firstOrCreate([ 'story_id' => $id, - 'profile_id' => $authed->id + 'profile_id' => $authed->id, ]); - if($v->wasRecentlyCreated) { + if ($v->wasRecentlyCreated) { Story::findOrFail($story->id)->increment('view_count'); - if($story->local == false) { + if ($story->local == false) { StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); } } - Cache::forget('stories:recent:by_id:' . $authed->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(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); $this->validate($request, [ 'sid' => 'required', - 'caption' => 'required|string' + 'caption' => 'required|string', ]); $pid = $request->user()->profile_id; $text = $request->input('caption'); $story = Story::findOrFail($request->input('sid')); - abort_if(!$story->can_reply, 422); + abort_if(! $story->can_reply, 422); $status = new Status; $status->type = 'story:reply'; @@ -415,7 +423,7 @@ class StoryApiV1Controller extends Controller $status->visibility = 'direct'; $status->in_reply_to_profile_id = $story->profile_id; $status->entities = json_encode([ - 'story_id' => $story->id + 'story_id' => $story->id, ]); $status->save(); @@ -429,24 +437,24 @@ class StoryApiV1Controller extends Controller 'story_actor_username' => $request->user()->username, 'story_id' => $story->id, 'story_media_url' => url(Storage::url($story->path)), - 'caption' => $text + 'caption' => $text, ]); $dm->save(); Conversation::updateOrInsert( [ 'to_id' => $story->profile_id, - 'from_id' => $pid + 'from_id' => $pid, ], [ 'type' => 'story:comment', 'status_id' => $status->id, 'dm_id' => $dm->id, - 'is_hidden' => false + 'is_hidden' => false, ] ); - if($story->local) { + if ($story->local) { $n = new Notification; $n->profile_id = $dm->to_id; $n->actor_id = $dm->from_id; @@ -460,33 +468,35 @@ class StoryApiV1Controller extends Controller return [ 'code' => 200, - 'msg' => 'Sent!' + 'msg' => 'Sent!', ]; } protected function storeMedia($photo, $user) { $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), [ + if (in_array($photo->getMimeType(), [ 'image/jpeg', 'image/png', - 'video/mp4' + '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()); + $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(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); $this->validate($request, [ - 'sid' => 'required|string|min:1|max:50' + 'sid' => 'required|string|min:1|max:50', ]); $pid = $request->user()->profile_id; diff --git a/app/Http/Controllers/StoryComposeController.php b/app/Http/Controllers/StoryComposeController.php index eb2d859c0..c8b0599a6 100644 --- a/app/Http/Controllers/StoryComposeController.php +++ b/app/Http/Controllers/StoryComposeController.php @@ -2,59 +2,52 @@ 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() { + 'file' => function () { return [ 'required', 'mimetypes:image/jpeg,image/png,video/mp4', - 'max:' . config_cache('pixelfed.max_photo_size'), + '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'); + 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) { + if ($count >= Story::MAX_PER_DAY) { abort(418, 'You have reached your limit for new Stories today.'); } @@ -64,7 +57,7 @@ class StoryComposeController extends Controller $story = new Story(); $story->duration = 3; $story->profile_id = $user->profile_id; - $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; + $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo'; $story->mime = $photo->getMimeType(); $story->path = $path; $story->local = true; @@ -77,21 +70,22 @@ class StoryComposeController extends Controller $res = [ 'code' => 200, - 'msg' => 'Successfully added', + 'msg' => 'Successfully added', 'media_id' => (string) $story->id, - 'media_url' => url(Storage::url($url)) . '?v=' . time(), - 'media_type' => $story->type + 'media_url' => url(Storage::url($url)).'?v='.time(), + 'media_type' => $story->type, ]; - if($story->type === 'video') { + if ($story->type === 'video') { $video = FFMpeg::open($path); $duration = $video->getDurationInSeconds(); $res['media_duration'] = $duration; - if($duration > 500) { + if ($duration > 500) { Storage::delete($story->path); $story->delete(); + return response()->json([ - 'message' => 'Video duration cannot exceed 60 seconds' + 'message' => 'Video duration cannot exceed 60 seconds', ], 422); } } @@ -102,37 +96,39 @@ class StoryComposeController extends Controller protected function storePhoto($photo, $user) { $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), [ + if (in_array($photo->getMimeType(), [ 'image/jpeg', 'image/png', - 'video/mp4' + '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); + $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); + 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' + 'y' => 'required', ]); $user = $request->user(); @@ -144,13 +140,13 @@ class StoryComposeController extends Controller $story = Story::whereProfileId($user->profile_id)->findOrFail($id); - $path = storage_path('app/' . $story->path); + $path = storage_path('app/'.$story->path); - if(!is_file($path)) { + if (! is_file($path)) { abort(400, 'Invalid or missing media.'); } - if($story->type === 'photo') { + if ($story->type === 'photo') { $img = Intervention::make($path); $img->crop($width, $height, $x, $y); $img->resize(1080, 1920, function ($constraint) { @@ -161,24 +157,24 @@ class StoryComposeController extends Controller return [ 'code' => 200, - 'msg' => 'Successfully cropped', + 'msg' => 'Successfully cropped', ]; } public function publishStory(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, [ 'media_id' => 'required', 'duration' => 'required|integer|min:3|max:120', 'can_reply' => 'required|boolean', - 'can_react' => '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'); + 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); @@ -194,13 +190,13 @@ class StoryComposeController extends Controller return [ 'code' => 200, - 'msg' => 'Successfully published', + 'msg' => 'Successfully published', ]; } public function apiV1Delete(Request $request, $id) { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404); $user = $request->user(); @@ -213,40 +209,40 @@ class StoryComposeController extends Controller return [ 'code' => 200, - 'msg' => 'Successfully deleted' + 'msg' => 'Successfully deleted', ]; } public function compose(Request $request) { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + 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'); + 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(!config_cache('instance.stories.enabled') || !$request->user(), 404); - abort_if(!config_cache('instance.polls.enabled'), 404); + 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(!config_cache('instance.stories.enabled') || !$request->user(), 404); + 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' + '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'); + 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) @@ -254,7 +250,7 @@ class StoryComposeController extends Controller ->where('expires_at', '>', now()) ->count(); - if($count >= Story::MAX_PER_DAY) { + if ($count >= Story::MAX_PER_DAY) { abort(418, 'You have reached your limit for new Stories today.'); } @@ -262,7 +258,7 @@ class StoryComposeController extends Controller $story->type = 'poll'; $story->story = json_encode([ 'question' => $request->input('question'), - 'options' => $request->input('options') + 'options' => $request->input('options'), ]); $story->public = false; $story->local = true; @@ -278,7 +274,7 @@ class StoryComposeController extends Controller $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) { + $poll->cached_tallies = collect($poll->poll_options)->map(function ($o) { return 0; })->toArray(); $poll->save(); @@ -290,23 +286,23 @@ class StoryComposeController extends Controller return [ 'code' => 200, - 'msg' => 'Successfully published', + 'msg' => 'Successfully published', ]; } public function storyPollVote(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, [ 'sid' => 'required', - 'ci' => 'required|integer|min:0|max:3' + '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); + abort_if(! FollowerService::follows($pid, $story->profile_id), 403); $poll = Poll::whereStoryId($story->id)->firstOrFail(); $vote = new PollVote; @@ -318,7 +314,7 @@ class StoryComposeController extends Controller $vote->save(); $poll->votes_count = $poll->votes_count + 1; - $poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) { + $poll->cached_tallies = collect($poll->getTallies())->map(function ($tally, $key) use ($ci) { return $ci == $key ? $tally + 1 : $tally; })->toArray(); $poll->save(); @@ -328,15 +324,15 @@ class StoryComposeController extends Controller public function storeReport(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, [ - 'type' => 'required|alpha_dash', - 'id' => 'required|integer|min:1', + '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'); + 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'); @@ -353,24 +349,24 @@ 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) + if (Report::whereProfileId($pid) ->whereObjectType('App\Story') ->whereObjectId($story->id) ->exists() ) { return response()->json(['error' => [ 'code' => 409, - 'message' => 'Cannot report the same story again' + 'message' => 'Cannot report the same story again', ]], 409); } @@ -389,18 +385,18 @@ class StoryComposeController extends Controller public function react(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, [ 'sid' => 'required', - 'reaction' => 'required|string' + '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'); + abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); $story = Story::findOrFail($request->input('sid')); - abort_if(!$story->can_react, 422); + 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; @@ -413,7 +409,7 @@ class StoryComposeController extends Controller $status->in_reply_to_profile_id = $story->profile_id; $status->entities = json_encode([ 'story_id' => $story->id, - 'reaction' => $text + 'reaction' => $text, ]); $status->save(); @@ -427,24 +423,24 @@ class StoryComposeController extends Controller 'story_actor_username' => $request->user()->username, 'story_id' => $story->id, 'story_media_url' => url(Storage::url($story->path)), - 'reaction' => $text + 'reaction' => $text, ]); $dm->save(); Conversation::updateOrInsert( [ 'to_id' => $story->profile_id, - 'from_id' => $pid + 'from_id' => $pid, ], [ 'type' => 'story:react', 'status_id' => $status->id, 'dm_id' => $dm->id, - 'is_hidden' => false + 'is_hidden' => false, ] ); - if($story->local) { + if ($story->local) { // generate notification $n = new Notification; $n->profile_id = $dm->to_id; @@ -464,18 +460,18 @@ class StoryComposeController extends Controller public function comment(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, [ 'sid' => 'required', - 'caption' => 'required|string' + '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'); + abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); $story = Story::findOrFail($request->input('sid')); - abort_if(!$story->can_reply, 422); + abort_if(! $story->can_reply, 422); $status = new Status; $status->type = 'story:reply'; @@ -486,7 +482,7 @@ class StoryComposeController extends Controller $status->visibility = 'direct'; $status->in_reply_to_profile_id = $story->profile_id; $status->entities = json_encode([ - 'story_id' => $story->id + 'story_id' => $story->id, ]); $status->save(); @@ -500,24 +496,24 @@ class StoryComposeController extends Controller 'story_actor_username' => $request->user()->username, 'story_id' => $story->id, 'story_media_url' => url(Storage::url($story->path)), - 'caption' => $text + 'caption' => $text, ]); $dm->save(); Conversation::updateOrInsert( [ 'to_id' => $story->profile_id, - 'from_id' => $pid + 'from_id' => $pid, ], [ 'type' => 'story:comment', 'status_id' => $status->id, 'dm_id' => $dm->id, - 'is_hidden' => false + 'is_hidden' => false, ] ); - if($story->local) { + if ($story->local) { // generate notification $n = new Notification; $n->profile_id = $dm->to_id; diff --git a/app/Http/Controllers/StoryController.php b/app/Http/Controllers/StoryController.php index 692e27961..fede7c6d9 100644 --- a/app/Http/Controllers/StoryController.php +++ b/app/Http/Controllers/StoryController.php @@ -34,7 +34,7 @@ class StoryController extends StoryComposeController { public function recent(Request $request) { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + 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 []; @@ -117,7 +117,7 @@ class StoryController extends StoryComposeController public function profile(Request $request, $id) { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + 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)) { @@ -176,7 +176,7 @@ class StoryController extends StoryComposeController public function viewed(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, [ 'id' => 'required|min:1', @@ -221,7 +221,7 @@ class StoryController extends StoryComposeController public function exists(Request $request, $id) { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + 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); @@ -233,7 +233,7 @@ class StoryController extends StoryComposeController public function iRedirect(Request $request) { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404); $user = $request->user(); abort_if(!$user, 404); @@ -243,7 +243,7 @@ class StoryController extends StoryComposeController public function viewers(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, [ 'sid' => 'required|string' @@ -274,7 +274,7 @@ class StoryController extends StoryComposeController public function remoteStory(Request $request, $id) { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404); $profile = Profile::findOrFail($id); if($profile->user_id != null || $profile->domain == null) { @@ -286,7 +286,7 @@ class StoryController extends StoryComposeController public function pollResults(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, [ 'sid' => 'required|string' @@ -304,7 +304,7 @@ class StoryController extends StoryComposeController public function getActivityObject(Request $request, $username, $id) { - abort_if(!config_cache('instance.stories.enabled'), 404); + abort_if(!(bool) config_cache('instance.stories.enabled'), 404); if(!$request->wantsJson()) { return redirect('/stories/' . $username); diff --git a/app/Http/Controllers/UserEmailForgotController.php b/app/Http/Controllers/UserEmailForgotController.php index 33378c4d0..3889b9802 100644 --- a/app/Http/Controllers/UserEmailForgotController.php +++ b/app/Http/Controllers/UserEmailForgotController.php @@ -34,7 +34,7 @@ class UserEmailForgotController extends Controller 'username.exists' => 'This username is no longer active or does not exist!' ]; - if(config('captcha.enabled') || config('captcha.active.login') || config('captcha.active.register')) { + if((bool) config_cache('captcha.enabled')) { $rules['h-captcha-response'] = 'required|captcha'; $messages['h-captcha-response.required'] = 'You need to complete the captcha!'; } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index bb1931555..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, ]; /** 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/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/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/FollowPipeline/UnfollowPipeline.php b/app/Jobs/FollowPipeline/UnfollowPipeline.php index 99f763f5c..9417e535e 100644 --- a/app/Jobs/FollowPipeline/UnfollowPipeline.php +++ b/app/Jobs/FollowPipeline/UnfollowPipeline.php @@ -4,114 +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; -use App\Jobs\HomeFeedPipeline\FeedUnfollowPipeline; 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; - FeedUnfollowPipeline::dispatch($actor, $target)->onQueue('follow'); + $actorProfile = Profile::find($actor); + if (! $actorProfile) { + return; + } + $targetProfile = Profile::find($target); + if (! $targetProfile) { + return; + } - FollowerService::remove($actor, $target); + FeedUnfollowPipeline::dispatch($actor, $target)->onQueue('follow'); - $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); - } + FollowerService::remove($actor, $target); - $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); - } + $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($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(); - }); - } + $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); + } - if($actorProfile->domain == null && config('instance.timeline.home.cached')) { - Cache::forget('pf:timelines:home:' . $actor); - } + 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(); + }); + } - FollowRequest::whereFollowingId($target) - ->whereFollowerId($actor) - ->delete(); + if ($actorProfile->domain == null && config('instance.timeline.home.cached')) { + Cache::forget('pf:timelines:home:'.$actor); + } - return; - } + 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..4d8eeca5c --- /dev/null +++ b/app/Jobs/GroupPipeline/NewStatusPipeline.php @@ -0,0 +1,130 @@ +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); + + $autolink = str_replace('/discover/tags/', '/groups/' . $status->group_id . '/topics/', $autolink); + + $status->rendered = nl2br($autolink); + $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/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 c1b4ea7f0..2aa51a532 100644 --- a/app/Jobs/ImageOptimizePipeline/ImageResize.php +++ b/app/Jobs/ImageOptimizePipeline/ImageResize.php @@ -51,7 +51,7 @@ class ImageResize implements ShouldQueue return; } - if(!config('pixelfed.optimize_image')) { + if((bool) config_cache('pixelfed.optimize_image') === false) { ImageThumbnail::dispatch($media)->onQueue('mmo'); return; } 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/InstancePipeline/FetchNodeinfoPipeline.php b/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php index 943281bb4..38127b2aa 100644 --- a/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php +++ b/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php @@ -72,10 +72,12 @@ class FetchNodeinfoPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessin $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/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/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 4eca4e1ab..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,141 +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; -use App\Jobs\HomeFeedPipeline\FeedInsertPipeline; 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); - FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + Notification::firstOrCreate( + [ + 'profile_id' => $target->id, + 'actor_id' => $actor->id, + 'action' => 'share', + 'item_type' => 'App\Status', + 'item_id' => $status->reblog_of_id ?? $status->id, + ] + ); - return $this->remoteAnnounceDeliver(); - } + FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); - public function remoteAnnounceDeliver() - { - if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) { - return true; - } - $status = $this->status; - $profile = $status->profile; + return $this->remoteAnnounceDeliver(); + } - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($status, new Announce()); - $activity = $fractal->createData($resource)->toArray(); + public function remoteAnnounceDeliver() + { + if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) { + return true; + } + $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 Announce()); + $activity = $fractal->createData($resource)->toArray(); - if(empty($audience) || $status->scope != 'public') { - // Return on profiles with no remote followers - return; - } + $audience = $status->profile->getAudienceInbox(); - $payload = json_encode($activity); + if (empty($audience) || $status->scope != 'public') { + // Return on profiles with no remote followers + return; + } - $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(); - } + $promise->wait(); + + } } diff --git a/app/Jobs/SharePipeline/UndoSharePipeline.php b/app/Jobs/SharePipeline/UndoSharePipeline.php index 1435688d9..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,128 +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; -use App\Jobs\HomeFeedPipeline\FeedRemovePipeline; 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; - FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + public function __construct(Status $status) + { + $this->status = $status; + } - if($parent) { - $target = $parent->profile_id; - ReblogService::removePostReblog($parent->profile_id, $status->id); + public function handle() + { + $status = $this->status; + $actor = $status->profile; + $parent = Status::find($status->reblog_of_id); - if($parent->reblogs_count > 0) { - $parent->reblogs_count = $parent->reblogs_count - 1; - $parent->save(); - StatusService::del($parent->id); - } + FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); - $notification = Notification::whereProfileId($target) - ->whereActorId($status->profile_id) - ->whereAction('share') - ->whereItemId($status->reblog_of_id) - ->whereItemType('App\Status') - ->first(); + if ($parent) { + $target = $parent->profile_id; + ReblogService::removePostReblog($parent->profile_id, $status->id); - if($notification) { - $notification->forceDelete(); - } - } + if ($parent->reblogs_count > 0) { + $parent->reblogs_count = $parent->reblogs_count - 1; + $parent->save(); + StatusService::del($parent->id); + } - if ($status->uri != null) { - return; - } + $notification = Notification::whereProfileId($target) + ->whereActorId($status->profile_id) + ->whereAction('share') + ->whereItemId($status->reblog_of_id) + ->whereItemType('App\Status') + ->first(); - if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) { - return $status->delete(); - } else { - return $this->remoteAnnounceDeliver(); - } - } + if ($notification) { + $notification->forceDelete(); + } + } - public function remoteAnnounceDeliver() - { - if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) { + 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 a81607755..65b72c104 100644 --- a/app/Jobs/StatusPipeline/RemoteStatusDelete.php +++ b/app/Jobs/StatusPipeline/RemoteStatusDelete.php @@ -109,7 +109,7 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing } StatusService::del($status->id, true); - AccountStatService::decrementPostCount($status->profile_id); + // AccountStatService::decrementPostCount($status->profile_id); return $this->unlinkRemoveMedia($status); } @@ -176,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 dbbfad5ac..d85ebdc4a 100644 --- a/app/Jobs/StatusPipeline/StatusDelete.php +++ b/app/Jobs/StatusPipeline/StatusDelete.php @@ -2,126 +2,122 @@ 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\Services\NotificationService; -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(); - }); + }); $dms = DirectMessage::whereStatusId($status->id)->get(); - foreach($dms as $dm) { + foreach ($dms as $dm) { $not = Notification::whereItemType('App\DirectMessage') ->whereItemId($dm->id) ->first(); - if($not) { + if ($not) { NotificationService::del($not->profile_id, $not->id); $not->forceDeleteQuietly(); } @@ -130,11 +126,11 @@ class StatusDelete implements ShouldQueue Like::whereStatusId($status->id)->delete(); $mediaTags = MediaTag::where('status_id', $status->id)->get(); - foreach($mediaTags as $mtag) { + foreach ($mediaTags as $mtag) { $not = Notification::whereItemType('App\MediaTag') ->whereItemId($mtag->id) ->first(); - if($not) { + if ($not) { NotificationService::del($not->profile_id, $not->id); $not->forceDeleteQuietly(); } @@ -142,85 +138,85 @@ class StatusDelete implements ShouldQueue } 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 872594a96..4d19c7d8a 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,18 +23,15 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use App\Services\StatusService; -use App\Services\UserFilterService; -use App\Services\AdminShadowFilterService; -use App\Jobs\HomeFeedPipeline\FeedInsertPipeline; -use App\Jobs\HomeFeedPipeline\HashtagInsertFanoutPipeline; class StatusEntityLexer implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $status; + protected $entities; + protected $autolink; /** @@ -60,12 +61,12 @@ class StatusEntityLexer implements ShouldQueue $profile = $this->status->profile; $status = $this->status; - if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + if (in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { $profile->status_count = $profile->status_count + 1; $profile->save(); } - if($profile->no_autolink == false) { + if ($profile->no_autolink == false) { $this->parseEntities(); } } @@ -103,16 +104,16 @@ class StatusEntityLexer implements ShouldQueue $status = $this->status; foreach ($tags as $tag) { - if(mb_strlen($tag) > 124) { + if (mb_strlen($tag) > 124) { continue; } DB::transaction(function () use ($status, $tag) { $slug = str_slug($tag, '-', false); $hashtag = Hashtag::firstOrCreate([ - 'slug' => $slug + 'slug' => $slug, ], [ - 'name' => $tag + 'name' => $tag, ]); StatusHashtag::firstOrCreate( @@ -136,11 +137,11 @@ class StatusEntityLexer implements ShouldQueue foreach ($mentions as $mention) { $mentioned = Profile::whereUsername($mention)->first(); - if (empty($mentioned) || !isset($mentioned->id)) { + 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; } @@ -161,8 +162,8 @@ class StatusEntityLexer implements ShouldQueue $status = $this->status; StatusService::refresh($status->id); - if(config('exp.cached_home_timeline')) { - if( $status->in_reply_to_id === null && + 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'); @@ -179,28 +180,28 @@ class StatusEntityLexer implements ShouldQueue 'photo:album', 'video', 'video:album', - 'photo:video:album' + 'photo:video:album', ]; - if(config_cache('pixelfed.bouncer.enabled')) { + if ((bool) config_cache('pixelfed.bouncer.enabled')) { Bouncer::get($status); } - Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id); + Cache::forget('pf:atom:user-feed:by-id:'.$status->profile_id); $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); - if( $status->uri == null && + 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)) { + if (AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) { PublicTimelineService::add($status->id); } } - if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') { + 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 23b8716c1..7ef7a3366 100644 --- a/app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php +++ b/app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php @@ -2,172 +2,171 @@ 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::withOptions(['allow_redirects' => false])->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($activity['content']); + $status->rendered = 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/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/Media.php b/app/Media.php index b3f9ccba0..30a1b33bd 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 { @@ -21,7 +21,7 @@ class Media extends Model protected $casts = [ 'srcset' => 'array', - 'deleted_at' => 'datetime' + 'deleted_at' => 'datetime', ]; public function status() @@ -36,12 +36,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; } @@ -50,19 +50,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)); @@ -78,9 +78,10 @@ class Media extends Model public function mimeType() { - if(!$this->mime) { + if (! $this->mime) { return; } + return explode('/', $this->mime)[0]; } @@ -91,7 +92,7 @@ class Media extends Model case 'audio': $verb = 'Audio'; break; - + case 'image': $verb = 'Image'; break; @@ -99,11 +100,12 @@ class Media extends Model case 'video': $verb = 'Video'; break; - + default: $verb = 'Document'; break; } + return $verb; } @@ -114,11 +116,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']; } } @@ -127,11 +129,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; } @@ -140,7 +142,7 @@ class Media extends Model return [ 'id' => $res['id'], 'title' => $res['title'], - 'url' => $res['url'] + 'url' => $res['url'], ]; } } 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/Group.php b/app/Models/Group.php new file mode 100644 index 000000000..508ed98c0 --- /dev/null +++ b/app/Models/Group.php @@ -0,0 +1,67 @@ + '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 @@ +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/UserObserver.php b/app/Observers/UserObserver.php index d587bd7e8..2b5d46116 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -107,7 +107,7 @@ class UserObserver CreateAvatar::dispatch($profile); }); - if(config_cache('account.autofollow') == true) { + if((bool) config_cache('account.autofollow') == true) { $names = config_cache('account.autofollow_usernames'); $names = explode(',', $names); 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 1a41b9e51..dfd4518c3 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 { @@ -28,7 +28,7 @@ class AuthServiceProvider extends ServiceProvider 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')); } diff --git a/app/Rules/ValidUrl.php b/app/Rules/ValidUrl.php new file mode 100644 index 000000000..21c36d564 --- /dev/null +++ b/app/Rules/ValidUrl.php @@ -0,0 +1,21 @@ +setSerializer(new ArraySerializer()); $profile = Profile::find($id); - if(!$profile || $profile->status === 'delete') { + if (! $profile || $profile->status === 'delete') { return null; } $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); + return $fractal->createData($resource)->toArray(); }); - if(!$res) { + if (! $res) { return $softFail ? null : abort(404); } + return $res; } public static function getMastodon($id, $softFail = false) { $account = self::get($id, $softFail); - if(!$account) { + if (! $account) { return null; } - if(config('exp.emc') == false) { + if (config('exp.emc') == false) { return $account; } @@ -73,41 +77,86 @@ class AccountService public static function del($id) { - Cache::forget('pf:activitypub:user-object:by-id:' . $id); - return Cache::forget(self::CACHE_KEY . $id); + Cache::forget('pf:activitypub:user-object:by-id:'.$id); + + return Cache::forget(self::CACHE_KEY.$id); } public static function settings($id) { - return Cache::remember('profile:compose:settings:' . $id, 604800, function() use($id) { + return Cache::remember('profile:compose:settings:'.$id, 604800, function () use ($id) { $settings = UserSetting::whereUserId($id)->first(); - if(!$settings) { + 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); - } - if($key == 'other') { - $other = self::defaultSettings()['other']; - $mo = is_array($item) ? $item : []; - return array_merge($other, $mo); - } - return $item; - }); + 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); + } + + if ($key == 'other') { + $other = self::defaultSettings()['other']; + $mo = is_array($item) ? $item : []; + + return array_merge($other, $mo); + } + + return $item; + }); }); } + public static function getAccountSettings($pid) + { + $key = self::CACHE_PF_ACCT_SETTINGS_KEY.$pid; + + return Cache::remember($key, 14400, function () use ($pid) { + $user = User::with('profile')->whereProfileId($pid)->whereNull('status')->first(); + if (! $user) { + return []; + } + + $settings = $user->settings; + $other = array_merge(self::defaultSettings()['other'], $settings->other ?? []); + + 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 forgetAccountSettings($pid) + { + return Cache::forget(self::CACHE_PF_ACCT_SETTINGS_KEY.$pid); + } + public static function canEmbed($id) { - return self::settings($id)['other']['disable_embeds'] == false; + $res = self::getAccountSettings($id); + if (! $res || ! isset($res['disable_embeds'])) { + return false; + } + + return ! $res['disable_embeds']; } public static function defaultSettings() @@ -123,7 +172,7 @@ class AccountService 'compose_settings' => [ 'default_scope' => 'public', 'default_license' => 1, - 'media_descriptions' => false + 'media_descriptions' => false, ], 'other' => [ 'advanced_atom' => false, @@ -134,7 +183,7 @@ class AccountService 'hide_groups' => false, 'hide_stories' => false, 'disable_cw' => false, - ] + ], ]; } @@ -142,43 +191,45 @@ class AccountService { $profile = Profile::find($id); - if(!$profile) { + if (! $profile) { return false; } - $key = self::CACHE_KEY . 'pcs:' . $id; + $key = self::CACHE_KEY.'pcs:'.$id; - if(Cache::has($key)) { + if (Cache::has($key)) { return; } $count = Status::whereProfileId($id) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_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, 900); + 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) { + $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('@')) { + if ($s->contains('@') && ! $s->startsWith('@')) { $username = "@{$username}"; } $profile = DB::table('profiles') ->whereUsername($username) ->first(); - if(!$profile) { + if (! $profile) { return null; } + return (string) $profile->id; }); } @@ -186,19 +237,20 @@ class AccountService public static function hiddenFollowers($id) { $account = self::get($id, true); - if(!$account || !isset($account['local']) || $account['local'] == false) { + if (! $account || ! isset($account['local']) || $account['local'] == false) { return false; } - return Cache::remember('pf:acct:settings:hidden-followers:' . $id, 43200, function() use($id) { + return Cache::remember('pf:acct:settings:hidden-followers:'.$id, 43200, function () use ($id) { $user = User::whereProfileId($id)->first(); - if(!$user) { + if (! $user) { return false; } $settings = UserSetting::whereUserId($user->id)->first(); - if($settings) { + if ($settings) { return $settings->show_profile_follower_count == false; } + return false; }); } @@ -206,60 +258,66 @@ class AccountService public static function hiddenFollowing($id) { $account = self::get($id, true); - if(!$account || !isset($account['local']) || $account['local'] == false) { + if (! $account || ! isset($account['local']) || $account['local'] == false) { return false; } - return Cache::remember('pf:acct:settings:hidden-following:' . $id, 43200, function() use($id) { + return Cache::remember('pf:acct:settings:hidden-following:'.$id, 43200, function () use ($id) { $user = User::whereProfileId($id)->first(); - if(!$user) { + if (! $user) { return false; } $settings = UserSetting::whereUserId($user->id)->first(); - if($settings) { + 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)) { + if (! $id) { + return; + } + $key = 'user:last_active_at:id:'.$id; + if (! Cache::has($key)) { $user = User::find($id); - if(!$user) { return; } + if (! $user) { + return; + } $user->last_active_at = now(); $user->save(); Cache::put($key, 1, 14400); } - return; + } public static function blocksDomain($pid, $domain = false) { - if(!$domain) { + if (! $domain) { return; } return UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->exists(); } - public static function formatNumber($num) { - if(!$num || $num < 1) { - return "0"; + 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'; - } else if ($num >= 1000000) { - return $formatter->format($num / 1000000) . 'M'; + return $formatter->format($num / 1000000000).'B'; + } elseif ($num >= 1000000) { + return $formatter->format($num / 1000000).'M'; } elseif ($num >= 1000) { - return $formatter->format($num / 1000) . 'K'; + return $formatter->format($num / 1000).'K'; } else { return $formatter->format($num); } @@ -269,14 +327,17 @@ class AccountService { $account = self::get($id, true); - if(!$account) return ""; + if (! $account) { + return ''; + } - $posts = self::formatNumber($account['statuses_count']) . ' Posts, '; - $following = self::formatNumber($account['following_count']) . ' Following, '; - $followers = self::formatNumber($account['followers_count']) . ' Followers'; + $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'])))) : + ' · '.\Purify::clean(strip_tags(str_replace("\n", '', str_replace("\r", '', $account['note'])))) : ''; - return $posts . $following . $followers . $note; + + return $posts.$following.$followers.$note; } } diff --git a/app/Services/ActivityPubFetchService.php b/app/Services/ActivityPubFetchService.php index 4b515859c..2e9f68402 100644 --- a/app/Services/ActivityPubFetchService.php +++ b/app/Services/ActivityPubFetchService.php @@ -2,70 +2,132 @@ 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 { + const CACHE_KEY = 'pf:services:apfetchs:'; + public static function get($url, $validateUrl = true) { - if($validateUrl === true) { - if(!Helpers::validateUrl($url)) { - return 0; + 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, 3600, function () use ($url) { + $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 $res->body(); + }); + } + + public static function validateUrl($url) + { + if (is_array($url)) { + $url = $url[0]; + } + + $localhosts = [ + '127.0.0.1', 'localhost', '::1', + ]; + + if (strtolower(mb_substr($url, 0, 8)) !== 'https://') { + 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) { + return false; } } - $baseHeaders = [ - 'Accept' => 'application/activity+json, application/ld+json', - ]; - - $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').')'; - - try { - $res = Http::withOptions(['allow_redirects' => false]) - ->withHeaders($headers) - ->timeout(30) - ->connectTimeout(5) - ->retry(3, 500) - ->get($url); - } catch (RequestException $e) { - return; - } catch (ConnectionException $e) { - return; - } catch (Exception $e) { - return; + if (app()->environment() === 'production') { + $bannedInstances = InstanceService::getBannedDomains(); + if (in_array($host, $bannedInstances)) { + return false; + } } - 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 $res->body(); + return $url; } } diff --git a/app/Services/AdminSettingsService.php b/app/Services/AdminSettingsService.php new file mode 100644 index 000000000..57fb6e96f --- /dev/null +++ b/app/Services/AdminSettingsService.php @@ -0,0 +1,166 @@ + 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'), + '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/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 7537830fc..4f2b006cc 100644 --- a/app/Services/ConfigCacheService.php +++ b/app/Services/ConfigCacheService.php @@ -4,11 +4,21 @@ namespace App\Services; use App\Models\ConfigCache as ConfigCacheModel; use Cache; +use Illuminate\Database\QueryException; class ConfigCacheService { const CACHE_KEY = 'config_cache:_v0-key:'; + const PROTECTED_KEYS = [ + 'filesystems.disks.s3.key', + 'filesystems.disks.s3.secret', + 'filesystems.disks.spaces.key', + 'filesystems.disks.spaces.secret', + 'captcha.secret', + 'captcha.sitekey', + ]; + public static function get($key) { $cacheKey = self::CACHE_KEY.$key; @@ -17,115 +27,176 @@ class ConfigCacheService return config($key); } - return Cache::remember($cacheKey, $ttl, function () use ($key) { + try { + return Cache::remember($cacheKey, $ttl, function () use ($key) { + $allowed = [ + 'app.name', + 'app.short_description', + 'app.description', + 'app.rules', - $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', + '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', - 'autospam.nlp.enabled', + 'instance.curated_registration.enabled', - 'instance.curated_registration.enabled', + 'federation.migration', - 'federation.migration', + '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', - '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', - // 'system.user_mode' - ]; + '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 (! config('instance.enable_cc')) { - return config($key); - } + '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', - if (! in_array($key, $allowed)) { - return config($key); - } + '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', - $v = config($key); - $c = ConfigCacheModel::where('k', $key)->first(); + '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', - if ($c) { - return $c->v ?? config($key); - } + 'instance.stats.total_local_posts', + // 'system.user_mode' + ]; - if (! $v) { - return; - } + if (! config('instance.enable_cc')) { + return config($key); + } - $cc = new ConfigCacheModel; - $cc->k = $key; - $cc->v = $v; - $cc->save(); + if (! in_array($key, $allowed)) { + return config($key); + } - return $v; - }); + $protect = false; + $protected = null; + if (in_array($key, self::PROTECTED_KEYS)) { + $protect = true; + } + + $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 = $val; + $exists->v = $protect ? $protected : $val; $exists->save(); Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12)); @@ -134,7 +205,7 @@ class ConfigCacheService $cc = new ConfigCacheModel; $cc->k = $key; - $cc->v = $val; + $cc->v = $protect ? $protected : $val; $cc->save(); Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12)); 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/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/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/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..3c8fcf104 --- /dev/null +++ b/app/Services/Internal/BeagleService.php @@ -0,0 +1,119 @@ +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() + { + 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() + { + 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/LandingService.php b/app/Services/LandingService.php index 20759ecf4..d6180771d 100644 --- a/app/Services/LandingService.php +++ b/app/Services/LandingService.php @@ -2,7 +2,6 @@ namespace App\Services; -use App\Status; use App\User; use App\Util\Site\Nodeinfo; use Illuminate\Support\Facades\Cache; @@ -18,9 +17,7 @@ class LandingService 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')) { @@ -53,8 +50,8 @@ class LandingService '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'), + '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'), @@ -85,7 +82,7 @@ class LandingService 'media_types' => config_cache('pixelfed.media_types'), ], 'features' => [ - 'federation' => config_cache('federation.activitypub.enabled'), + 'federation' => (bool) config_cache('federation.activitypub.enabled'), 'timelines' => [ 'local' => true, 'network' => (bool) config_cache('federation.network_timeline'), diff --git a/app/Services/MediaStorageService.php b/app/Services/MediaStorageService.php index 216e37497..87bb9a586 100644 --- a/app/Services/MediaStorageService.php +++ b/app/Services/MediaStorageService.php @@ -2,44 +2,38 @@ 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(config_cache('pixelfed.cloud_storage') == true) { + if ((bool) config_cache('pixelfed.cloud_storage') == true) { (new self())->cloudStore($media); } - return; } public static function move(Media $media) { - if($media->remote_media) { + if ($media->remote_media) { return; } - if(config_cache('pixelfed.cloud_storage') == true) { + if ((bool) config_cache('pixelfed.cloud_storage') == true) { return (new self())->cloudMove($media); } - return; + } public static function avatar($avatar, $local = false, $skipRecentCheck = false) @@ -56,31 +50,31 @@ class MediaStorageService { return false; } - $h = Arr::mapWithKeys($r->getHeaders(), function($item, $key) { + $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))) { + if ($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) { return false; } return [ 'length' => $len, - 'mime' => $mime + 'mime' => $mime, ]; } protected function cloudStore($media) { - if($media->remote_media == true) { - if(config('media.storage.remote.cloud')) { + if ($media->remote_media == true) { + if (config('media.storage.remote.cloud')) { (new self())->remoteToCloud($media); } } else { @@ -100,7 +94,7 @@ class MediaStorageService { $storagePath = implode('/', $p); $url = ResilientMediaStorageService::store($storagePath, $path, $name); - if($thumb) { + if ($thumb) { $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); $media->thumbnail_url = $thumbUrl; } @@ -108,8 +102,8 @@ class MediaStorageService { $media->optimized_url = $url; $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); MediaService::del($media->status_id); StatusService::del($media->status_id, false); } @@ -119,20 +113,20 @@ class MediaStorageService { { $url = $media->remote_url; - if(!Helpers::validateUrl($url)) { + if (! Helpers::validateUrl($url)) { return; } $head = $this->head($media->remote_url); - if(!$head) { + if (! $head) { return; } $mimes = [ 'image/jpeg', 'image/png', - 'video/mp4' + 'video/mp4', ]; $mime = $head['mime']; @@ -141,11 +135,11 @@ class MediaStorageService { $media->remote_media = true; $media->save(); - if(!in_array($mime, $mimes)) { + if (! in_array($mime, $mimes)) { return; } - if($head['length'] >= $max_size) { + if ($head['length'] >= $max_size) { return; } @@ -168,10 +162,10 @@ class MediaStorageService { } $base = MediaPathService::get($media->profile); - $path = Str::random(40) . $ext; + $path = Str::random(40).$ext; $tmpBase = storage_path('app/remcache/'); - $tmpPath = $media->profile_id . '-' . $path; - $tmpName = $tmpBase . $tmpPath; + $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); @@ -186,8 +180,8 @@ class MediaStorageService { $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); @@ -199,13 +193,13 @@ class MediaStorageService { $url = $avatar->remote_url; $driver = $local ? 'local' : config('filesystems.cloud'); - if(empty($url) || Helpers::validateUrl($url) == false) { + if (empty($url) || Helpers::validateUrl($url) == false) { return; } $head = $this->head($url); - if($head == false) { + if ($head == false) { return; } @@ -218,46 +212,47 @@ class MediaStorageService { $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))) { + if (! $skipRecentCheck) { + if ($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) { return; } } - Cache::forget('avatar:' . $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')) { + if (Str::endsWith($mime, '; charset=utf-8')) { $mime = str_replace('; charset=utf-8', '', $mime); } - if(!in_array($mime, $mimes)) { + if (! in_array($mime, $mimes)) { return; } - if($head['length'] >= $max_size) { + if ($head['length'] >= $max_size) { return; } - $base = ($local ? 'public/cache/' : 'cache/') . 'avatars/' . $avatar->profile_id; + $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; + $path = 'avatar_'.strtolower(Str::random(random_int(3, 6))).'.'.$ext; $tmpBase = storage_path('app/remcache/'); - $tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path; - $tmpName = $tmpBase . $tmpPath; + $tmpPath = 'avatar_'.$avatar->profile_id.'-'.$path; + $tmpName = $tmpBase.$tmpPath; $data = @file_get_contents($url, false, null, 0, $head['length']); - if(!$data) { + 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'])) { + if (! $mimeCheck || ! in_array($mimeCheck, ['image/png', 'image/jpeg'])) { $avatar->last_fetched_at = now(); $avatar->save(); unlink($tmpName); + return; } @@ -265,15 +260,15 @@ class MediaStorageService { $file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); $permalink = $disk->url($file); - $avatar->media_path = $base . '/' . $path; + $avatar->media_path = $base.'/'.$path; $avatar->is_remote = true; - $avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink; + $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(); - Cache::forget('avatar:' . $avatar->profile_id); + Cache::forget('avatar:'.$avatar->profile_id); AccountService::del($avatar->profile_id); AvatarStorageCleanup::dispatch($avatar)->onQueue($queue)->delay(now()->addMinutes(random_int(3, 15))); @@ -282,7 +277,7 @@ class MediaStorageService { public static function delete(Media $media, $confirm = false) { - if(!$confirm) { + if (! $confirm) { return; } MediaDeletePipeline::dispatch($media)->onQueue('mmo'); @@ -290,13 +285,13 @@ class MediaStorageService { protected function cloudMove($media) { - if(!Storage::exists($media->media_path)) { + if (! Storage::exists($media->media_path)) { return 'invalid file'; } $path = storage_path('app/'.$media->media_path); $thumb = false; - if($media->thumbnail_path) { + if ($media->thumbnail_path) { $thumb = storage_path('app/'.$media->thumbnail_path); $pt = explode('/', $media->thumbnail_path); $thumbname = array_pop($pt); @@ -307,7 +302,7 @@ class MediaStorageService { $storagePath = implode('/', $p); $url = ResilientMediaStorageService::store($storagePath, $path, $name); - if($thumb) { + if ($thumb) { $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); $media->thumbnail_url = $thumbUrl; } @@ -316,8 +311,8 @@ class MediaStorageService { $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); MediaService::del($media->status_id); StatusService::del($media->status_id, false); } diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php index 4051bede4..d73f6c018 100644 --- a/app/Services/StatusService.php +++ b/app/Services/StatusService.php @@ -2,15 +2,11 @@ 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 { @@ -19,18 +15,19 @@ class StatusService 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(); @@ -38,32 +35,34 @@ class StatusService $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) { 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()); + 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/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/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/User.php b/app/User.php index a39f650be..30b502308 100644 --- a/app/User.php +++ b/app/User.php @@ -2,28 +2,34 @@ 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\Expo\ExpoPushToken; +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 +44,12 @@ class User extends Authenticatable 'app_register_ip', 'email_verified_at', 'last_active_at', - 'register_source' + 'register_source', + 'expo_token', + '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/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index bcf4f359c..9e03beef1 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -2,49 +2,34 @@ 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\Jobs\StatusPipeline\StatusReplyPipeline; -use App\Jobs\StatusPipeline\StatusTagsPipeline; -use App\Util\ActivityPub\HttpSignature; -use Illuminate\Support\Str; -use App\Services\ActivityPubFetchService; -use App\Services\ActivityPubDeliveryService; -use App\Services\CustomEmojiService; -use App\Services\InstanceService; -use App\Services\MediaPathService; -use App\Services\MediaStorageService; -use App\Services\NetworkTimelineService; -use App\Jobs\MediaPipeline\MediaStoragePipeline; +use App\Instance; use App\Jobs\AvatarPipeline\RemoteAvatarFetch; use App\Jobs\HomeFeedPipeline\FeedInsertRemotePipeline; -use App\Util\Media\License; +use App\Jobs\MediaPipeline\MediaStoragePipeline; +use App\Jobs\StatusPipeline\StatusReplyPipeline; +use App\Jobs\StatusPipeline\StatusTagsPipeline; +use App\Media; use App\Models\Poll; -use Illuminate\Contracts\Cache\LockTimeoutException; -use App\Services\DomainService; -use App\Services\UserFilterService; +use App\Profile; use App\Services\Account\AccountStatService; +use App\Services\ActivityPubDeliveryService; +use App\Services\ActivityPubFetchService; +use App\Services\DomainService; +use App\Services\InstanceService; +use App\Services\MediaPathService; +use App\Services\NetworkTimelineService; +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 Purify; +use Validator; -class Helpers { - +class Helpers +{ public static function validateObject($data) { $verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone']; @@ -53,14 +38,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 +53,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 +65,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 +85,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 +104,7 @@ class Helpers { public static function normalizeAudience($data, $localOnly = true) { - if(!isset($data['to'])) { + if (! isset($data['to'])) { return; } @@ -128,32 +113,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,56 +149,57 @@ 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) { - if(is_array($url)) { + if (is_array($url)) { $url = $url[0]; } $hash = hash('sha256', $url); $key = "helpers:url:valid:sha256-{$hash}"; - $valid = Cache::remember($key, 900, function() use($url) { + $valid = Cache::remember($key, 900, function () use ($url) { $localhosts = [ - '127.0.0.1', 'localhost', '::1' + '127.0.0.1', 'localhost', '::1', ]; - if(strtolower(mb_substr($url, 0, 8)) !== 'https://') { + if (strtolower(mb_substr($url, 0, 8)) !== 'https://') { return false; } - if(substr_count($url, '://') !== 1) { + if (substr_count($url, '://') !== 1) { return false; } - if(mb_substr($url, 0, 8) !== 'https://') { - $url = 'https://' . substr($url, 8); + if (mb_substr($url, 0, 8) !== 'https://') { + $url = 'https://'.substr($url, 8); } $valid = filter_var($url, FILTER_VALIDATE_URL); - if(!$valid) { + if (! $valid) { return false; } $host = parse_url($valid, PHP_URL_HOST); - if(in_array($host, $localhosts)) { + if (in_array($host, $localhosts)) { return false; } - if(config('security.url.verify_dns')) { - if(DomainService::hasValidDns($host) === false) { + if (config('security.url.verify_dns')) { + if (DomainService::hasValidDns($host) === false) { return false; } } - if(app()->environment() === 'production') { + if (app()->environment() === 'production') { $bannedInstances = InstanceService::getBannedDomains(); - if(in_array($host, $bannedInstances)) { + if (in_array($host, $bannedInstances)) { return false; } } @@ -224,12 +213,14 @@ class Helpers { 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); $url = strtolower($domain) === strtolower($host) ? $url : false; + return $url; } + return false; } @@ -237,15 +228,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 +245,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,12 +266,12 @@ 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; @@ -288,51 +280,52 @@ class Helpers { 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(config('autospam.live_filters.enabled')) { + if (config('autospam.live_filters.enabled')) { $filters = config('autospam.live_filters.filters'); - if(!empty($filters) && isset($res['content']) && !empty($res['content']) && strlen($filters) > 3) { + 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) { + foreach ($filters as $filter) { $filter = trim(strtolower($filter)); - if(!$filter || !strlen($filter)) { + if (! $filter || ! strlen($filter)) { continue; } - if(str_contains(strtolower($content), $filter)) { + if (str_contains(strtolower($content), $filter)) { return; } } } } - if(isset($res['object'])) { + if (isset($res['object'])) { $activity = $res; } else { $activity = ['object' => $res]; @@ -342,37 +335,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 { @@ -380,7 +373,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; } } @@ -389,15 +382,15 @@ class Helpers { $idDomain = parse_url($id, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST); - if($idDomain && $urlDomain && strtolower($idDomain) !== strtolower($urlDomain)) { + if ($idDomain && $urlDomain && strtolower($idDomain) !== strtolower($urlDomain)) { return; } - if(!self::validateUrl($id)) { + if (! self::validateUrl($id)) { return; } - if(!isset($activity['object']['attributedTo'])) { + if (! isset($activity['object']['attributedTo'])) { return; } @@ -405,39 +398,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; } } @@ -447,15 +439,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, @@ -466,6 +458,7 @@ class Helpers { $scope, $id ); + return $status; } else { $status = self::storeStatus($url, $profile, $res); @@ -482,12 +475,12 @@ class Helpers { $idDomain = parse_url($id, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST); $originalUrlDomain = parse_url($originalUrl, PHP_URL_HOST); - if(!self::validateUrl($id) || !self::validateUrl($url)) { + if (! self::validateUrl($id) || ! self::validateUrl($url)) { return; } - if( strtolower($originalUrlDomain) !== strtolower($idDomain) || - strtolower($originalUrlDomain) !== strtolower($urlDomain) ) { + if (strtolower($originalUrlDomain) !== strtolower($idDomain) || + strtolower($originalUrlDomain) !== strtolower($urlDomain)) { return; } @@ -498,21 +491,21 @@ 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'; } } $status = Status::updateOrCreate( [ - 'uri' => $url + 'uri' => $url, ], [ 'profile_id' => $pid, 'url' => $url, @@ -527,24 +520,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']) && @@ -556,8 +549,8 @@ class Helpers { ->unique() ->values() ->toArray(); - if(!in_array($urlDomain, $filteredDomains)) { - if(!$isUnlisted) { + if (! in_array($urlDomain, $filteredDomains)) { + if (! $isUnlisted) { NetworkTimelineService::add($status->id); } } @@ -565,7 +558,7 @@ class Helpers { AccountStatService::incrementPostCount($pid); - if( $status->in_reply_to_id === null && + 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'); @@ -576,14 +569,14 @@ class Helpers { public static function getSensitive($activity, $url) { - if(!$url || !strlen($url)) { + 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; } @@ -593,13 +586,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 { @@ -616,25 +609,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'; } @@ -643,15 +636,15 @@ 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(); @@ -697,9 +690,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']; @@ -712,11 +706,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; @@ -735,50 +729,52 @@ 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; } @@ -788,42 +784,42 @@ class Helpers { public static function profileUpdateOrCreate($url) { $res = self::fetchProfileFromUrl($url); - if(!$res || isset($res['id']) == false) { + if (! $res || isset($res['id']) == false) { return; } $urlDomain = parse_url($url, PHP_URL_HOST); $domain = parse_url($res['id'], PHP_URL_HOST); - if(strtolower($urlDomain) !== strtolower($domain)) { + if (strtolower($urlDomain) !== strtolower($domain)) { return; } - if(!isset($res['preferredUsername']) && !isset($res['nickname'])) { + if (! isset($res['preferredUsername']) && ! isset($res['nickname'])) { return; } // skip invalid usernames - if(!ctype_alnum($res['preferredUsername'])) { + if (! ctype_alnum($res['preferredUsername'])) { $tmpUsername = str_replace(['_', '.', '-'], '', $res['preferredUsername']); - if(!ctype_alnum($tmpUsername)) { + 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'])) { + if (! self::validateUrl($res['inbox'])) { return; } - if(!self::validateUrl($res['id'])) { + if (! self::validateUrl($res['id'])) { return; } $instance = Instance::updateOrCreate([ - 'domain' => $domain + 'domain' => $domain, ]); - if($instance->wasRecentlyCreated == true) { + if ($instance->wasRecentlyCreated == true) { \App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low'); } @@ -846,13 +842,14 @@ class Helpers { ] ); - 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; } @@ -861,9 +858,14 @@ 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') { + if (app()->environment() !== 'production') { return; } ActivityPubDeliveryService::queue() 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/Lexer/RestrictedNames.php b/app/Util/Lexer/RestrictedNames.php index 4224ae96c..9d88b0da1 100644 --- a/app/Util/Lexer/RestrictedNames.php +++ b/app/Util/Lexer/RestrictedNames.php @@ -4,376 +4,378 @@ 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', + // Static Assets + 'assets', + 'public', + 'storage', + 'htaccess', + '.htaccess', + 'favicon.ico', + 'embed.js', + 'index.php', + 'manifest.json', + 'mix-manifest.json', + 'robots.txt', - // Laravel Horizon - 'horizon', + // Laravel Horizon + 'horizon', - // 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', - ]; + // 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', + ]; - public static function get() - { - $banned = []; + public static function get() + { + $banned = []; - if(config('instance.username.banned')) { - $banned = array_map('trim', explode(',', config('instance.username.banned'))); - } + if (config('instance.username.banned')) { + $banned = array_map('trim', explode(',', config('instance.username.banned'))); + } - $additional = self::$additional; - $reserved = self::$reserved; + $additional = self::$additional; + $reserved = self::$reserved; - $res = array_merge($additional, $reserved, $banned); - $res = array_unique($res); - sort($res); - - return $res; - } + $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 038eef99e..530dc7108 100644 --- a/app/Util/Site/Config.php +++ b/app/Util/Site/Config.php @@ -7,7 +7,7 @@ use Illuminate\Support\Str; class Config { - const CACHE_KEY = 'api:site:configuration:_v0.8'; + const CACHE_KEY = 'api:site:configuration:_v0.9'; public static function get() { @@ -36,10 +36,10 @@ class Config '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')) : [], @@ -97,6 +97,7 @@ class Config ], ], 'hls' => $hls, + 'groups' => (bool) config('groups.enabled'), ], ]; }); 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/composer.json b/composer.json index a1adcf9e9..b97ec1187 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "AGPL-3.0-only", "type": "project", "require": { - "php": "^8.1|^8.2|^8.3", + "php": "^8.2|^8.3", "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", @@ -14,18 +14,18 @@ "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", "doctrine/dbal": "^3.0", "intervention/image": "^2.4", "jenssegers/agent": "^2.6", - "laravel-notification-channels/webpush": "^7.1", - "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", @@ -33,24 +33,22 @@ "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", "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/pint": "^1.14", - "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": [ @@ -86,6 +84,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 df57a6b03..e6fd309e8 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": "e166bdd0755f5304899546c97d9b7d4c", + "content-hash": "c055c4b1ba26004ab6951e9dba4b4508", "packages": [ { "name": "aws/aws-crt-php", - "version": "v1.2.4", + "version": "v1.2.6", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2" + "reference": "a63485b65b6b3367039306496d49737cf1995408" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/eb0c6e4e142224a10b08f49ebf87f32611d162b2", - "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/a63485b65b6b3367039306496d49737cf1995408", + "reference": "a63485b65b6b3367039306496d49737cf1995408", "shasum": "" }, "require": { @@ -56,22 +56,22 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.4" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.6" }, - "time": "2023-11-08T00:42:13+00:00" + "time": "2024-06-13T17:21:28+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.300.10", + "version": "3.316.3", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b24bf7882fed0ef029996dcdcba6c273b69db8fe" + "reference": "e832e594b3c213760e067e15ef2739f77505e832" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b24bf7882fed0ef029996dcdcba6c273b69db8fe", - "reference": "b24bf7882fed0ef029996dcdcba6c273b69db8fe", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e832e594b3c213760e067e15ef2739f77505e832", + "reference": "e832e594b3c213760e067e15ef2739f77505e832", "shasum": "" }, "require": { @@ -151,9 +151,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.300.10" + "source": "https://github.com/aws/aws-sdk-php/tree/3.316.3" }, - "time": "2024-03-04T19:06:07+00:00" + "time": "2024-07-12T18:07:23+00:00" }, { "name": "bacon/bacon-qr-code", @@ -209,88 +209,6 @@ }, "time": "2022-12-07T17:46:57+00:00" }, - { - "name": "beyondcode/laravel-websockets", - "version": "1.14.1", - "source": { - "type": "git", - "url": "https://github.com/beyondcode/laravel-websockets.git", - "reference": "fee9a81e42a096d2aaca216ce91acf6e25d8c06d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/beyondcode/laravel-websockets/zipball/fee9a81e42a096d2aaca216ce91acf6e25d8c06d", - "reference": "fee9a81e42a096d2aaca216ce91acf6e25d8c06d", - "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.1" - }, - "time": "2023-08-30T07:23:12+00:00" - }, { "name": "brick/math", "version": "0.9.3", @@ -487,69 +405,6 @@ ], "time": "2023-12-11T17:09:12+00:00" }, - { - "name": "cboden/ratchet", - "version": "v0.4.4", - "source": { - "type": "git", - "url": "https://github.com/ratchetphp/Ratchet.git", - "reference": "5012dc954541b40c5599d286fd40653f5716a38f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/Ratchet/zipball/5012dc954541b40c5599d286fd40653f5716a38f", - "reference": "5012dc954541b40c5599d286fd40653f5716a38f", - "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" - }, - "require-dev": { - "phpunit/phpunit": "~4.8" - }, - "type": "library", - "autoload": { - "psr-4": { - "Ratchet\\": "src/Ratchet" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "role": "Developer" - }, - { - "name": "Matt Bonneau", - "role": "Developer" - } - ], - "description": "PHP WebSocket library", - "homepage": "http://socketo.me", - "keywords": [ - "Ratchet", - "WebSockets", - "server", - "sockets", - "websocket" - ], - "support": { - "chat": "https://gitter.im/reactphp/reactphp", - "issues": "https://github.com/ratchetphp/Ratchet/issues", - "source": "https://github.com/ratchetphp/Ratchet/tree/v0.4.4" - }, - "time": "2021-12-14T00:20:41+00:00" - }, { "name": "dasprid/enum", "version": "1.0.5", @@ -669,16 +524,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": { @@ -738,9 +593,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", @@ -837,16 +692,16 @@ }, { "name": "doctrine/dbal", - "version": "3.8.3", + "version": "3.8.6", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "db922ba9436b7b18a23d1653a0b41ff2369ca41c" + "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/db922ba9436b7b18a23d1653a0b41ff2369ca41c", - "reference": "db922ba9436b7b18a23d1653a0b41ff2369ca41c", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/b7411825cf7efb7e51f9791dea19d86e43b399a1", + "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1", "shasum": "" }, "require": { @@ -862,12 +717,12 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.58", - "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.16", + "phpstan/phpstan": "1.11.5", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "9.6.19", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.9.0", + "squizlabs/php_codesniffer": "3.10.1", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" @@ -930,7 +785,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.8.3" + "source": "https://github.com/doctrine/dbal/tree/3.8.6" }, "funding": [ { @@ -946,7 +801,7 @@ "type": "tidelift" } ], - "time": "2024-03-03T15:55:06+00:00" + "time": "2024-06-19T10:38:17+00:00" }, { "name": "doctrine/deprecations", @@ -997,16 +852,16 @@ }, { "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": { @@ -1016,10 +871,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": { @@ -1068,7 +923,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": [ { @@ -1084,7 +939,7 @@ "type": "tidelift" } ], - "time": "2022-10-12T20:59:15+00:00" + "time": "2024-05-22T20:47:39+00:00" }, { "name": "doctrine/inflector", @@ -1490,59 +1345,6 @@ }, "time": "2023-11-17T15:01:25+00:00" }, - { - "name": "facade/ignition-contracts", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/facade/ignition-contracts.git", - "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/3c921a1cdba35b68a7f0ccffc6dffc1995b18267", - "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267", - "shasum": "" - }, - "require": { - "php": "^7.3|^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": "fgrosse/phpasn1", "version": "v2.5.0", @@ -1619,84 +1421,28 @@ "abandoned": true, "time": "2022-12-19T11:08:26+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" - }, - "suggest": { - "psr/http-message": "The package containing the PSR-7 interfaces" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Fig\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "issues": "https://github.com/php-fig/http-message-util/issues", - "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" - }, - "time": "2020-11-24T22:02:12+00:00" - }, { "name": "firebase/php-jwt", - "version": "v6.10.0", + "version": "v6.10.1", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" + "reference": "500501c2ce893c824c801da135d02661199f60c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", + "reference": "500501c2ce893c824c801da135d02661199f60c5", "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" }, @@ -1734,9 +1480,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.10.1" }, - "time": "2023-12-01T16:26:39+00:00" + "time": "2024-05-18T18:05:11+00:00" }, { "name": "fruitcake/php-cors", @@ -1811,24 +1557,24 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.1.2", + "version": "v1.1.3", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862" + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/fbd48bce38f73f8a4ec8583362e732e4095e5862", - "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862", + "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.2" + "phpoption/phpoption": "^1.9.3" }, "require-dev": { - "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" }, "type": "library", "autoload": { @@ -1857,7 +1603,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.2" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" }, "funding": [ { @@ -1869,26 +1615,26 @@ "type": "tidelift" } ], - "time": "2023-11-12T22:16:48+00:00" + "time": "2024-07-20T21:45:45+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.8.1", + "version": "7.9.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + "reference": "a629e5b69db96eb4939c1b34114130077dd4c6fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a629e5b69db96eb4939c1b34114130077dd4c6fc", + "reference": "a629e5b69db96eb4939c1b34114130077dd4c6fc", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "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" @@ -1899,9 +1645,9 @@ "require-dev": { "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.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -1979,7 +1725,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + "source": "https://github.com/guzzle/guzzle/tree/7.9.1" }, "funding": [ { @@ -1995,20 +1741,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:35:24+00:00" + "time": "2024-07-19T16:19:57+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", "shasum": "" }, "require": { @@ -2016,7 +1762,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "type": "library", "extra": { @@ -2062,7 +1808,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.2" + "source": "https://github.com/guzzle/promises/tree/2.0.3" }, "funding": [ { @@ -2078,20 +1824,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:19:20+00:00" + "time": "2024-07-18T10:29:17+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.6.2", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", "shasum": "" }, "require": { @@ -2106,8 +1852,8 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -2178,7 +1924,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.2" + "source": "https://github.com/guzzle/psr7/tree/2.7.0" }, "funding": [ { @@ -2194,7 +1940,7 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:05:35+00:00" + "time": "2024-07-18T11:15:46+00:00" }, { "name": "guzzlehttp/uri-template", @@ -2368,16 +2114,16 @@ }, { "name": "jaybizzle/crawler-detect", - "version": "v1.2.116", + "version": "v1.2.119", "source": { "type": "git", "url": "https://github.com/JayBizzle/Crawler-Detect.git", - "reference": "97e9fe30219e60092e107651abb379a38b342921" + "reference": "275002e22b0333c15a7c6792fdae5d5deefc9ef0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/97e9fe30219e60092e107651abb379a38b342921", - "reference": "97e9fe30219e60092e107651abb379a38b342921", + "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/275002e22b0333c15a7c6792fdae5d5deefc9ef0", + "reference": "275002e22b0333c15a7c6792fdae5d5deefc9ef0", "shasum": "" }, "require": { @@ -2414,9 +2160,9 @@ ], "support": { "issues": "https://github.com/JayBizzle/Crawler-Detect/issues", - "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.116" + "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.119" }, - "time": "2023-07-21T15:49:49+00:00" + "time": "2024-06-07T07:58:43+00:00" }, { "name": "jenssegers/agent", @@ -2502,29 +2248,93 @@ "time": "2020-06-13T08:05:20+00:00" }, { - "name": "laravel-notification-channels/webpush", - "version": "7.1.0", + "name": "laravel-notification-channels/expo", + "version": "v2.0.0", "source": { "type": "git", - "url": "https://github.com/laravel-notification-channels/webpush.git", - "reference": "b31f7d807d30c80e7391063291ebfe9683bb7de5" + "url": "https://github.com/laravel-notification-channels/expo.git", + "reference": "29d038b6409077ac4c671cc5587a4dc7986260b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel-notification-channels/webpush/zipball/b31f7d807d30c80e7391063291ebfe9683bb7de5", - "reference": "b31f7d807d30c80e7391063291ebfe9683bb7de5", + "url": "https://api.github.com/repos/laravel-notification-channels/expo/zipball/29d038b6409077ac4c671cc5587a4dc7986260b0", + "reference": "29d038b6409077ac4c671cc5587a4dc7986260b0", "shasum": "" }, "require": { - "illuminate/notifications": "^8.0|^9.0|^10.0", - "illuminate/support": "^8.0|^9.0|^10.0", + "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.0" + "php": "^8.1" }, "require-dev": { "mockery/mockery": "~1.0", - "orchestra/testbench": "^6.0|^7.0|^8.0", - "phpunit/phpunit": "^9.0" + "orchestra/testbench": "^7.0|^8.0|^9.0", + "phpunit/phpunit": "^9.5|^10.5" }, "type": "library", "extra": { @@ -2555,22 +2365,22 @@ "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/7.1.0" + "source": "https://github.com/laravel-notification-channels/webpush/tree/8.0.0" }, - "time": "2023-03-14T11:20:02+00:00" + "time": "2024-03-16T05:36:52+00:00" }, { "name": "laravel/framework", - "version": "v10.46.0", + "version": "v11.16.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "5e95946a8283a8d5c015035793f9c61c297e937f" + "reference": "bd4808aaf103ccb5cb4b00bcee46140c070c0ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/5e95946a8283a8d5c015035793f9c61c297e937f", - "reference": "5e95946a8283a8d5c015035793f9c61c297e937f", + "url": "https://api.github.com/repos/laravel/framework/zipball/bd4808aaf103ccb5cb4b00bcee46140c070c0ec4", + "reference": "bd4808aaf103ccb5cb4b00bcee46140c070c0ec4", "shasum": "" }, "require": { @@ -2586,43 +2396,44 @@ "ext-openssl": "*", "ext-session": "*", "ext-tokenizer": "*", - "fruitcake/php-cors": "^1.2", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.1.9", + "laravel/prompts": "^0.1.18", "laravel/serializable-closure": "^1.3", "league/commonmark": "^2.2.1", "league/flysystem": "^3.8.0", "monolog/monolog": "^3.0", - "nesbot/carbon": "^2.67", - "nunomaduro/termwind": "^1.13", - "php": "^8.1", + "nesbot/carbon": "^2.72.2|^3.0", + "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.4", - "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", + "symfony/error-handler": "^7.0", + "symfony/finder": "^7.0", + "symfony/http-foundation": "^7.0", + "symfony/http-kernel": "^7.0", + "symfony/mailer": "^7.0", + "symfony/mime": "^7.0", + "symfony/polyfill-php83": "^1.28", + "symfony/process": "^7.0", + "symfony/routing": "^7.0", + "symfony/uid": "^7.0", + "symfony/var-dumper": "^7.0", "tijsverkoyen/css-to-inline-styles": "^2.2.5", "vlucas/phpdotenv": "^5.4.1", "voku/portable-ascii": "^2.0" }, "conflict": { - "carbonphp/carbon-doctrine-types": ">=3.0", - "doctrine/dbal": ">=4.0", - "phpunit/phpunit": ">=11.0.0", + "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": { @@ -2658,36 +2469,35 @@ "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", "ext-gmp": "*", - "fakerphp/faker": "^1.21", - "guzzlehttp/guzzle": "^7.5", + "fakerphp/faker": "^1.23", "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", + "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^8.18", - "pda/pheanstalk": "^4.0", - "phpstan/phpstan": "^1.4.7", - "phpunit/phpunit": "^10.0.7", + "orchestra/testbench-core": "^9.1.5", + "pda/pheanstalk": "^5.0", + "phpstan/phpstan": "^1.11.5", + "phpunit/phpunit": "^10.5|^11.0", "predis/predis": "^2.0.2", - "symfony/cache": "^6.2", - "symfony/http-client": "^6.2.4", - "symfony/psr-http-message-bridge": "^2.0" + "resend/resend-php": "^0.10.0", + "symfony/cache": "^7.0", + "symfony/http-client": "^7.0", + "symfony/psr-http-message-bridge": "^7.0" }, "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).", + "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.", @@ -2696,34 +2506,34 @@ "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).", + "mockery/mockery": "Required to use mocking (^1.6).", "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).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).", "predis/predis": "Required to use the predis connector (^2.0.2).", "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": { @@ -2763,7 +2573,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-02-27T16:46:54+00:00" + "time": "2024-07-16T14:33:07+00:00" }, { "name": "laravel/helpers", @@ -2824,16 +2634,16 @@ }, { "name": "laravel/horizon", - "version": "v5.23.1", + "version": "v5.25.0", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "7475de7eb5b465c2da84218002fe1a62b8175da0" + "reference": "81e62cee5b3feaf169d683b8890e33bf454698ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/7475de7eb5b465c2da84218002fe1a62b8175da0", - "reference": "7475de7eb5b465c2da84218002fe1a62b8175da0", + "url": "https://api.github.com/repos/laravel/horizon/zipball/81e62cee5b3feaf169d683b8890e33bf454698ab", + "reference": "81e62cee5b3feaf169d683b8890e33bf454698ab", "shasum": "" }, "require": { @@ -2846,6 +2656,7 @@ "nesbot/carbon": "^2.17|^3.0", "php": "^8.0", "ramsey/uuid": "^4.0", + "symfony/console": "^6.0|^7.0", "symfony/error-handler": "^6.0|^7.0", "symfony/process": "^6.0|^7.0" }, @@ -2896,54 +2707,52 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.23.1" + "source": "https://github.com/laravel/horizon/tree/v5.25.0" }, - "time": "2024-02-20T15:14:10+00:00" + "time": "2024-07-05T16:46:31+00:00" }, { "name": "laravel/passport", - "version": "v11.10.5", + "version": "v12.2.0", "source": { "type": "git", "url": "https://github.com/laravel/passport.git", - "reference": "4d81207941d6efc198857847d9e4c17520f28d75" + "reference": "b24c6462835a16163141fbe588533d16603212b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/passport/zipball/4d81207941d6efc198857847d9e4c17520f28d75", - "reference": "4d81207941d6efc198857847d9e4c17520f28d75", + "url": "https://api.github.com/repos/laravel/passport/zipball/b24c6462835a16163141fbe588533d16603212b7", + "reference": "b24c6462835a16163141fbe588533d16603212b7", "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.31|^8.11", + "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" @@ -2976,20 +2785,20 @@ "issues": "https://github.com/laravel/passport/issues", "source": "https://github.com/laravel/passport" }, - "time": "2024-02-09T16:27:49+00:00" + "time": "2024-04-17T17:56:14+00:00" }, { "name": "laravel/prompts", - "version": "v0.1.16", + "version": "v0.1.24", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "ca6872ab6aec3ab61db3a61f83a6caf764ec7781" + "reference": "409b0b4305273472f3754826e68f4edbd0150149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/ca6872ab6aec3ab61db3a61f83a6caf764ec7781", - "reference": "ca6872ab6aec3ab61db3a61f83a6caf764ec7781", + "url": "https://api.github.com/repos/laravel/prompts/zipball/409b0b4305273472f3754826e68f4edbd0150149", + "reference": "409b0b4305273472f3754826e68f4edbd0150149", "shasum": "" }, "require": { @@ -3029,11 +2838,12 @@ "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.1.16" + "source": "https://github.com/laravel/prompts/tree/v0.1.24" }, - "time": "2024-02-21T19:25:27+00:00" + "time": "2024-06-17T13:58:22+00:00" }, { "name": "laravel/serializable-closure", @@ -3163,16 +2973,16 @@ }, { "name": "laravel/ui", - "version": "v4.4.0", + "version": "v4.5.2", "source": { "type": "git", "url": "https://github.com/laravel/ui.git", - "reference": "7335d7049b2cde345c029e9d2de839b80af62bc0" + "reference": "c75396f63268c95b053c8e4814eb70e0875e9628" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/ui/zipball/7335d7049b2cde345c029e9d2de839b80af62bc0", - "reference": "7335d7049b2cde345c029e9d2de839b80af62bc0", + "url": "https://api.github.com/repos/laravel/ui/zipball/c75396f63268c95b053c8e4814eb70e0875e9628", + "reference": "c75396f63268c95b053c8e4814eb70e0875e9628", "shasum": "" }, "require": { @@ -3180,11 +2990,12 @@ "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" + "php": "^8.0", + "symfony/console": "^6.0|^7.0" }, "require-dev": { - "orchestra/testbench": "^7.0|^8.0|^9.0", - "phpunit/phpunit": "^9.3|^10.4" + "orchestra/testbench": "^7.35|^8.15|^9.0", + "phpunit/phpunit": "^9.3|^10.4|^11.0" }, "type": "library", "extra": { @@ -3219,40 +3030,40 @@ "ui" ], "support": { - "source": "https://github.com/laravel/ui/tree/v4.4.0" + "source": "https://github.com/laravel/ui/tree/v4.5.2" }, - "time": "2024-01-12T15:56:45+00:00" + "time": "2024-05-08T18:07:10+00:00" }, { "name": "lcobucci/clock", - "version": "3.0.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/lcobucci/clock.git", - "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc" + "reference": "6f28b826ea01306b07980cb8320ab30b966cd715" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/039ef98c6b57b101d10bd11d8fdfda12cbd996dc", - "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/6f28b826ea01306b07980cb8320ab30b966cd715", + "reference": "6f28b826ea01306b07980cb8320ab30b966cd715", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0", + "php": "~8.2.0 || ~8.3.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.27", + "lcobucci/coding-standard": "^11.0.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": "^10.2.3" }, "type": "library", "autoload": { @@ -3273,7 +3084,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.2.0" }, "funding": [ { @@ -3285,20 +3096,20 @@ "type": "patreon" } ], - "time": "2022-12-19T15:00:24+00:00" + "time": "2023-11-17T17:00:27+00:00" }, { "name": "lcobucci/jwt", - "version": "5.2.0", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "0ba88aed12c04bd2ed9924f500673f32b67a6211" + "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/0ba88aed12c04bd2ed9924f500673f32b67a6211", - "reference": "0ba88aed12c04bd2ed9924f500673f32b67a6211", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", "shasum": "" }, "require": { @@ -3346,7 +3157,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.2.0" + "source": "https://github.com/lcobucci/jwt/tree/5.3.0" }, "funding": [ { @@ -3358,7 +3169,7 @@ "type": "patreon" } ], - "time": "2023-11-20T21:17:42+00:00" + "time": "2024-04-11T23:07:54+00:00" }, { "name": "league/commonmark", @@ -3604,16 +3415,16 @@ }, { "name": "league/flysystem", - "version": "3.24.0", + "version": "3.28.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "b25a361508c407563b34fac6f64a8a17a8819675" + "reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/b25a361508c407563b34fac6f64a8a17a8819675", - "reference": "b25a361508c407563b34fac6f64a8a17a8819675", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c", + "reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c", "shasum": "" }, "require": { @@ -3637,11 +3448,14 @@ "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.34", + "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" @@ -3678,32 +3492,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.24.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.28.0" }, - "funding": [ - { - "url": "https://ecologi.com/frankdejonge", - "type": "custom" - }, - { - "url": "https://github.com/frankdejonge", - "type": "github" - } - ], - "time": "2024-02-04T12:10:17+00:00" + "time": "2024-05-22T10:09:12+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.24.0", + "version": "3.28.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513" + "reference": "22071ef1604bc776f5ff2468ac27a752514665c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513", - "reference": "809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/22071ef1604bc776f5ff2468ac27a752514665c8", + "reference": "22071ef1604bc776f5ff2468ac27a752514665c8", "shasum": "" }, "require": { @@ -3743,32 +3547,22 @@ "storage" ], "support": { - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.24.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.28.0" }, - "funding": [ - { - "url": "https://ecologi.com/frankdejonge", - "type": "custom" - }, - { - "url": "https://github.com/frankdejonge", - "type": "github" - } - ], - "time": "2024-01-26T18:43:21+00:00" + "time": "2024-05-06T20:05:52+00:00" }, { "name": "league/flysystem-local", - "version": "3.23.1", + "version": "3.28.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00" + "reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/b884d2bf9b53bb4804a56d2df4902bb51e253f00", - "reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/13f22ea8be526ea58c2ddff9e158ef7c296e4f40", + "reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40", "shasum": "" }, "require": { @@ -3802,20 +3596,9 @@ "local" ], "support": { - "issues": "https://github.com/thephpleague/flysystem-local/issues", - "source": "https://github.com/thephpleague/flysystem-local/tree/3.23.1" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.28.0" }, - "funding": [ - { - "url": "https://ecologi.com/frankdejonge", - "type": "custom" - }, - { - "url": "https://github.com/frankdejonge", - "type": "github" - } - ], - "time": "2024-01-26T18:25:23+00:00" + "time": "2024-05-06T20:05:52+00:00" }, { "name": "league/iso3166", @@ -4021,16 +3804,16 @@ }, { "name": "league/uri", - "version": "7.4.0", + "version": "7.4.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "bf414ba956d902f5d98bf9385fcf63954f09dce5" + "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/bf414ba956d902f5d98bf9385fcf63954f09dce5", - "reference": "bf414ba956d902f5d98bf9385fcf63954f09dce5", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/bedb6e55eff0c933668addaa7efa1e1f2c417cc4", + "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4", "shasum": "" }, "require": { @@ -4099,7 +3882,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.4.0" + "source": "https://github.com/thephpleague/uri/tree/7.4.1" }, "funding": [ { @@ -4107,20 +3890,20 @@ "type": "github" } ], - "time": "2023-12-01T06:24:25+00:00" + "time": "2024-03-23T07:42:40+00:00" }, { "name": "league/uri-interfaces", - "version": "7.4.0", + "version": "7.4.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "bd8c487ec236930f7bbc42b8d374fa882fbba0f3" + "reference": "8d43ef5c841032c87e2de015972c06f3865ef718" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/bd8c487ec236930f7bbc42b8d374fa882fbba0f3", - "reference": "bd8c487ec236930f7bbc42b8d374fa882fbba0f3", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/8d43ef5c841032c87e2de015972c06f3865ef718", + "reference": "8d43ef5c841032c87e2de015972c06f3865ef718", "shasum": "" }, "require": { @@ -4183,7 +3966,7 @@ "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.4.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.4.1" }, "funding": [ { @@ -4191,7 +3974,7 @@ "type": "github" } ], - "time": "2023-11-24T15:40:42+00:00" + "time": "2024-03-23T07:42:40+00:00" }, { "name": "minishlink/web-push", @@ -4324,16 +4107,16 @@ }, { "name": "monolog/monolog", - "version": "3.5.0", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448" + "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c915e2634718dbc8a4a15c61b0e62e7a44e14448", - "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f4393b648b78a5408747de94fca38beb5f7e9ef8", + "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8", "shasum": "" }, "require": { @@ -4356,7 +4139,7 @@ "phpstan/phpstan": "^1.9", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.4", - "phpunit/phpunit": "^10.1", + "phpunit/phpunit": "^10.5.17", "predis/predis": "^1.1 || ^2", "ruflin/elastica": "^7", "symfony/mailer": "^5.4 || ^6", @@ -4409,7 +4192,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.5.0" + "source": "https://github.com/Seldaek/monolog/tree/3.7.0" }, "funding": [ { @@ -4421,7 +4204,7 @@ "type": "tidelift" } ], - "time": "2023-10-27T15:32:31+00:00" + "time": "2024-06-28T09:40:51+00:00" }, { "name": "mtdowling/jmespath.php", @@ -4491,42 +4274,41 @@ }, { "name": "nesbot/carbon", - "version": "2.72.3", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83" + "reference": "cb4374784c87d0a0294e8513a52eb63c0aff3139" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/0c6fd108360c562f6e4fd1dedb8233b423e91c83", - "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cb4374784c87d0a0294e8513a52eb63c0aff3139", + "reference": "cb4374784c87d0a0294e8513a52eb63c0aff3139", "shasum": "" }, "require": { "carbonphp/carbon-doctrine-types": "*", "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 || ^4.0", - "doctrine/orm": "^2.7 || ^3.0", - "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" @@ -4534,8 +4316,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-3.x": "3.x-dev", - "dev-master": "2.x-dev" + "dev-master": "3.x-dev", + "dev-2.x": "2.x-dev" }, "laravel": { "providers": [ @@ -4594,7 +4376,7 @@ "type": "tidelift" } ], - "time": "2024-01-25T10:35:09+00:00" + "time": "2024-07-16T22:29:20+00:00" }, { "name": "nette/schema", @@ -4746,16 +4528,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.0.1", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "2218c2252c874a4624ab2f613d86ac32d227bc69" + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/2218c2252c874a4624ab2f613d86ac32d227bc69", - "reference": "2218c2252c874a4624ab2f613d86ac32d227bc69", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", "shasum": "" }, "require": { @@ -4766,7 +4548,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -4798,39 +4580,38 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" }, - "time": "2024-02-21T19:24:10+00:00" + "time": "2024-07-01T20:03:41+00:00" }, { "name": "nunomaduro/termwind", - "version": "v1.15.1", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc" + "reference": "58c4c58cf23df7f498daeb97092e34f5259feb6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/8ab0b32c8caa4a2e09700ea32925441385e4a5dc", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/58c4c58cf23df7f498daeb97092e34f5259feb6a", + "reference": "58c4c58cf23df7f498daeb97092e34f5259feb6a", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^8.0", - "symfony/console": "^5.3.0|^6.0.0" + "php": "^8.2", + "symfony/console": "^7.0.4" }, "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", + "ergebnis/phpstan-rules": "^2.2.0", + "illuminate/console": "^11.0.0", + "laravel/pint": "^1.14.0", + "mockery/mockery": "^1.6.7", + "pestphp/pest": "^2.34.1", + "phpstan/phpstan": "^1.10.59", + "phpstan/phpstan-strict-rules": "^1.5.2", + "symfony/var-dumper": "^7.0.4", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -4839,6 +4620,9 @@ "providers": [ "Termwind\\Laravel\\TermwindServiceProvider" ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -4870,7 +4654,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.0.1" }, "funding": [ { @@ -4886,7 +4670,7 @@ "type": "github" } ], - "time": "2023-02-08T01:06:31+00:00" + "time": "2024-03-06T16:17:14+00:00" }, { "name": "nyholm/psr7", @@ -4968,16 +4752,16 @@ }, { "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": { @@ -5031,7 +4815,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", @@ -5085,16 +4869,16 @@ }, { "name": "paragonie/sodium_compat", - "version": "v1.20.0", + "version": "v1.21.1", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6" + "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37" }, "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/bb312875dcdd20680419564fe42ba1d9564b9e37", + "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37", "shasum": "" }, "require": { @@ -5165,26 +4949,26 @@ ], "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/v1.21.1" }, - "time": "2023-04-30T00:54:53+00:00" + "time": "2024-04-22T22:05:04+00:00" }, { "name": "pbmedia/laravel-ffmpeg", - "version": "8.4.0", + "version": "8.5.0", "source": { "type": "git", "url": "https://github.com/protonemedia/laravel-ffmpeg.git", - "reference": "b16fd89fab3a37727711d3abc3e89229e3c4f14d" + "reference": "44f260839e68ce8c785d502f99b998729cdb5321" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/b16fd89fab3a37727711d3abc3e89229e3c4f14d", - "reference": "b16fd89fab3a37727711d3abc3e89229e3c4f14d", + "url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/44f260839e68ce8c785d502f99b998729cdb5321", + "reference": "44f260839e68ce8c785d502f99b998729cdb5321", "shasum": "" }, "require": { - "illuminate/contracts": "^10.0", + "illuminate/contracts": "^10.0|^11.0", "php": "^8.1|^8.2|^8.3", "php-ffmpeg/php-ffmpeg": "^1.2", "ramsey/collection": "^2.0" @@ -5192,10 +4976,10 @@ "require-dev": { "league/flysystem-memory": "^3.10", "mockery/mockery": "^1.4.4", - "nesbot/carbon": "^2.66", - "orchestra/testbench": "^8.0", + "nesbot/carbon": "^2.66|^3.0", + "orchestra/testbench": "^8.0|^9.0", "phpunit/phpunit": "^10.4", - "spatie/image": "^2.2", + "spatie/image": "^2.2|^3.3", "spatie/phpunit-snapshot-assertions": "^5.0" }, "type": "library", @@ -5237,7 +5021,7 @@ ], "support": { "issues": "https://github.com/protonemedia/laravel-ffmpeg/issues", - "source": "https://github.com/protonemedia/laravel-ffmpeg/tree/8.4.0" + "source": "https://github.com/protonemedia/laravel-ffmpeg/tree/8.5.0" }, "funding": [ { @@ -5245,7 +5029,7 @@ "type": "github" } ], - "time": "2024-01-02T23:13:28+00:00" + "time": "2024-03-12T11:20:32+00:00" }, { "name": "php-ffmpeg/php-ffmpeg", @@ -5338,16 +5122,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.2", + "version": "1.9.3", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820" + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/80735db690fe4fc5c76dfa7f9b770634285fa820", - "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", "shasum": "" }, "require": { @@ -5355,13 +5139,13 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + "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" @@ -5397,7 +5181,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.2" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" }, "funding": [ { @@ -5409,7 +5193,7 @@ "type": "tidelift" } ], - "time": "2023-11-12T21:59:55+00:00" + "time": "2024-07-20T21:41:07+00:00" }, { "name": "phpseclib/phpseclib", @@ -5647,55 +5431,6 @@ }, "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", @@ -6063,20 +5798,20 @@ }, { "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", @@ -6100,7 +5835,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", @@ -6112,22 +5847,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": { @@ -6136,7 +5871,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -6151,7 +5886,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -6165,9 +5900,9 @@ "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", @@ -6272,16 +6007,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.0", + "version": "v0.12.4", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "750bf031a48fd07c673dbe3f11f72362ea306d0d" + "reference": "2fd717afa05341b4f8152547f142cd2f130f6818" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/750bf031a48fd07c673dbe3f11f72362ea306d0d", - "reference": "750bf031a48fd07c673dbe3f11f72362ea306d0d", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/2fd717afa05341b4f8152547f142cd2f130f6818", + "reference": "2fd717afa05341b4f8152547f142cd2f130f6818", "shasum": "" }, "require": { @@ -6345,9 +6080,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.0" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.4" }, - "time": "2023-12-20T15:28:09+00:00" + "time": "2024-06-10T01:18:23+00:00" }, { "name": "pusher/pusher-php-server", @@ -6545,20 +6280,20 @@ }, { "name": "ramsey/uuid", - "version": "4.7.5", + "version": "4.7.6", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e" + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e", - "reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e", + "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" @@ -6621,7 +6356,7 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.5" + "source": "https://github.com/ramsey/uuid/tree/4.7.6" }, "funding": [ { @@ -6633,681 +6368,20 @@ "type": "tidelift" } ], - "time": "2023-11-08T05:53:05+00:00" - }, - { - "name": "ratchet/rfc6455", - "version": "v0.3.1", - "source": { - "type": "git", - "url": "https://github.com/ratchetphp/RFC6455.git", - "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/7c964514e93456a52a99a20fcfa0de242a43ccdb", - "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb", - "shasum": "" - }, - "require": { - "guzzlehttp/psr7": "^2 || ^1.7", - "php": ">=5.4.2" - }, - "require-dev": { - "phpunit/phpunit": "^5.7", - "react/socket": "^1.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Ratchet\\RFC6455\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "role": "Developer" - }, - { - "name": "Matt Bonneau", - "role": "Developer" - } - ], - "description": "RFC6455 WebSocket protocol handler", - "homepage": "http://socketo.me", - "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.12.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/dns.git", - "reference": "c134600642fa615b46b41237ef243daa65bb64ec" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/c134600642fa615b46b41237ef243daa65bb64ec", - "reference": "c134600642fa615b46b41237ef243daa65bb64ec", - "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.6 || ^5.7 || ^4.8.36", - "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.12.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-11-29T12:41:06+00:00" - }, - { - "name": "react/event-loop", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "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.5.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-11-13T13:48:05+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", - "client", - "event-driven", - "http", - "http client", - "http server", - "https", - "psr-7", - "reactphp", - "server", - "streaming" - ], - "support": { - "issues": "https://github.com/reactphp/http/issues", - "source": "https://github.com/reactphp/http/tree/v1.9.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-04-26T10:29:24+00:00" - }, - { - "name": "react/promise", - "version": "v3.1.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", - "shasum": "" - }, - "require": { - "php": ">=7.1.0" - }, - "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^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.1.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-11-16T16:21:57+00:00" - }, - { - "name": "react/socket", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/216d3aec0b87f04a40ca04f481e6af01bdd1d038", - "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038", - "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.6 || ^5.7 || ^4.8.36", - "react/async": "^4 || ^3 || ^2", - "react/promise-stream": "^1.4", - "react/promise-timer": "^1.10" - }, - "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.15.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-12-15T11:02:10+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-04-27T21:32:50+00:00" }, { "name": "spatie/db-dumper", - "version": "3.4.2", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/spatie/db-dumper.git", - "reference": "59beef7ad612ca7463dfddb64de6e038eb59e0d7" + "reference": "faca5056830bccea04eadf07e8074669cb9e905e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/db-dumper/zipball/59beef7ad612ca7463dfddb64de6e038eb59e0d7", - "reference": "59beef7ad612ca7463dfddb64de6e038eb59e0d7", + "url": "https://api.github.com/repos/spatie/db-dumper/zipball/faca5056830bccea04eadf07e8074669cb9e905e", + "reference": "faca5056830bccea04eadf07e8074669cb9e905e", "shasum": "" }, "require": { @@ -7345,7 +6419,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/db-dumper/tree/3.4.2" + "source": "https://github.com/spatie/db-dumper/tree/3.6.0" }, "funding": [ { @@ -7357,20 +6431,20 @@ "type": "github" } ], - "time": "2023-12-25T11:42:15+00:00" + "time": "2024-04-24T14:54:13+00:00" }, { "name": "spatie/image-optimizer", - "version": "1.7.2", + "version": "1.7.5", "source": { "type": "git", "url": "https://github.com/spatie/image-optimizer.git", - "reference": "62f7463483d1bd975f6f06025d89d42a29608fe1" + "reference": "43aff6725cd87bb78ccd8532633cfa8bdc962505" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/62f7463483d1bd975f6f06025d89d42a29608fe1", - "reference": "62f7463483d1bd975f6f06025d89d42a29608fe1", + "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/43aff6725cd87bb78ccd8532633cfa8bdc962505", + "reference": "43aff6725cd87bb78ccd8532633cfa8bdc962505", "shasum": "" }, "require": { @@ -7410,22 +6484,22 @@ ], "support": { "issues": "https://github.com/spatie/image-optimizer/issues", - "source": "https://github.com/spatie/image-optimizer/tree/1.7.2" + "source": "https://github.com/spatie/image-optimizer/tree/1.7.5" }, - "time": "2023-11-03T10:08:02+00:00" + "time": "2024-05-16T08:48:33+00:00" }, { "name": "spatie/laravel-backup", - "version": "8.6.0", + "version": "8.8.1", "source": { "type": "git", "url": "https://github.com/spatie/laravel-backup.git", - "reference": "c6a7607c0eea80efc2cf6628ffcd172f73a2088f" + "reference": "a9c2d2f726f4c60c2dc5d7c0c8380f72492638c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/c6a7607c0eea80efc2cf6628ffcd172f73a2088f", - "reference": "c6a7607c0eea80efc2cf6628ffcd172f73a2088f", + "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/a9c2d2f726f4c60c2dc5d7c0c8380f72492638c2", + "reference": "a9c2d2f726f4c60c2dc5d7c0c8380f72492638c2", "shasum": "" }, "require": { @@ -7499,7 +6573,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-backup/issues", - "source": "https://github.com/spatie/laravel-backup/tree/8.6.0" + "source": "https://github.com/spatie/laravel-backup/tree/8.8.1" }, "funding": [ { @@ -7511,7 +6585,7 @@ "type": "other" } ], - "time": "2024-02-06T20:39:11+00:00" + "time": "2024-06-04T11:31:33+00:00" }, { "name": "spatie/laravel-image-optimizer", @@ -7583,16 +6657,16 @@ }, { "name": "spatie/laravel-package-tools", - "version": "1.16.2", + "version": "1.16.4", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "e62eeb1fe8a8a0b2e83227a6c279c8c59f7d3a15" + "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/e62eeb1fe8a8a0b2e83227a6c279c8c59f7d3a15", - "reference": "e62eeb1fe8a8a0b2e83227a6c279c8c59f7d3a15", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", + "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", "shasum": "" }, "require": { @@ -7631,7 +6705,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.2" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.4" }, "funding": [ { @@ -7639,34 +6713,35 @@ "type": "github" } ], - "time": "2024-01-11T08:43:00+00:00" + "time": "2024-03-20T07:29:11+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", @@ -7705,7 +6780,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": [ { @@ -7713,7 +6788,7 @@ "type": "github" } ], - "time": "2023-01-14T21:10:59+00:00" + "time": "2024-02-05T13:37:25+00:00" }, { "name": "spatie/temporary-directory", @@ -7843,27 +6918,27 @@ }, { "name": "stevebauman/purify", - "version": "v6.0.2", + "version": "v6.2.0", "source": { "type": "git", "url": "https://github.com/stevebauman/purify.git", - "reference": "ce8d10c0dfe804d90470ff819b84d891037cd6bc" + "reference": "303d23e5756a1fd0e9b34f14459ec6aa327a8412" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stevebauman/purify/zipball/ce8d10c0dfe804d90470ff819b84d891037cd6bc", - "reference": "ce8d10c0dfe804d90470ff819b84d891037cd6bc", + "url": "https://api.github.com/repos/stevebauman/purify/zipball/303d23e5756a1fd0e9b34f14459ec6aa327a8412", + "reference": "303d23e5756a1fd0e9b34f14459ec6aa327a8412", "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", + "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": { @@ -7903,37 +6978,38 @@ ], "support": { "issues": "https://github.com/stevebauman/purify/issues", - "source": "https://github.com/stevebauman/purify/tree/v6.0.2" + "source": "https://github.com/stevebauman/purify/tree/v6.2.0" }, - "time": "2023-08-24T18:53:12+00:00" + "time": "2024-03-12T15:22:59+00:00" }, { "name": "symfony/cache", - "version": "v6.4.4", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "0ef36534694c572ff526d91c7181f3edede176e7" + "reference": "e933e1d947ffb88efcdd34a2bd51561cab7deaae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/0ef36534694c572ff526d91c7181f3edede176e7", - "reference": "0ef36534694c572ff526d91c7181f3edede176e7", + "url": "https://api.github.com/repos/symfony/cache/zipball/e933e1d947ffb88efcdd34a2bd51561cab7deaae", + "reference": "e933e1d947ffb88efcdd34a2bd51561cab7deaae", "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.3.6|^7.0" + "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", @@ -7942,15 +7018,15 @@ }, "require-dev": { "cache/integration-tests": "dev-master", - "doctrine/dbal": "^2.13.1|^3|^4", + "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|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/filesystem": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^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": { @@ -7985,7 +7061,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.4" + "source": "https://github.com/symfony/cache/tree/v7.1.2" }, "funding": [ { @@ -8001,20 +7077,20 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:27:10+00:00" + "time": "2024-06-11T13:32:38+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "1d74b127da04ffa87aa940abe15446fa89653778" + "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/1d74b127da04ffa87aa940abe15446fa89653778", - "reference": "1d74b127da04ffa87aa940abe15446fa89653778", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/df6a1a44c890faded49a5fca33c2d5c5fd3c2197", + "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197", "shasum": "" }, "require": { @@ -8024,7 +7100,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -8061,7 +7137,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.0" }, "funding": [ { @@ -8077,51 +7153,124 @@ "type": "tidelift" } ], - "time": "2023-09-25T12:52:38+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { - "name": "symfony/console", - "version": "v6.4.4", + "name": "symfony/clock", + "version": "v7.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "0d9e4eb5ad413075624378f474c4167ea202de78" + "url": "https://github.com/symfony/clock.git", + "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0d9e4eb5ad413075624378f474c4167ea202de78", - "reference": "0d9e4eb5ad413075624378f474c4167ea202de78", + "url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7", + "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7", "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.1.1" + }, + "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-05-31T14:57:53+00:00" + }, + { + "name": "symfony/console", + "version": "v7.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0aa29ca177f432ab68533432db0de059f39c92ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0aa29ca177f432ab68533432db0de059f39c92ae", + "reference": "0aa29ca177f432ab68533432db0de059f39c92ae", + "shasum": "" + }, + "require": { + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.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|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.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": "^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", - "symfony/var-dumper": "^5.4|^6.0|^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": { @@ -8155,7 +7304,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.4" + "source": "https://github.com/symfony/console/tree/v7.1.2" }, "funding": [ { @@ -8171,24 +7320,24 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:27:10+00:00" + "time": "2024-06-28T10:03:55+00:00" }, { "name": "symfony/css-selector", - "version": "v6.4.3", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "ee0f7ed5cf298cc019431bb3b3977ebc52b86229" + "reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/ee0f7ed5cf298cc019431bb3b3977ebc52b86229", - "reference": "ee0f7ed5cf298cc019431bb3b3977ebc52b86229", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/1c7cee86c6f812896af54434f8ce29c8d94f9ff4", + "reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -8220,7 +7369,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.4.3" + "source": "https://github.com/symfony/css-selector/tree/v7.1.1" }, "funding": [ { @@ -8236,20 +7385,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "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/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { @@ -8258,7 +7407,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -8287,7 +7436,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.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -8303,26 +7452,26 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/error-handler", - "version": "v6.4.4", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "c725219bdf2afc59423c32793d5019d2a904e13a" + "reference": "2412d3dddb5c9ea51a39cfbff1c565fc9844ca32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/c725219bdf2afc59423c32793d5019d2a904e13a", - "reference": "c725219bdf2afc59423c32793d5019d2a904e13a", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/2412d3dddb5c9ea51a39cfbff1c565fc9844ca32", + "reference": "2412d3dddb5c9ea51a39cfbff1c565fc9844ca32", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/var-dumper": "^6.4|^7.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", @@ -8331,7 +7480,7 @@ "require-dev": { "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^5.4|^6.0|^7.0" + "symfony/serializer": "^6.4|^7.0" }, "bin": [ "Resources/bin/patch-type-declarations" @@ -8362,7 +7511,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.4.4" + "source": "https://github.com/symfony/error-handler/tree/v7.1.2" }, "funding": [ { @@ -8378,28 +7527,28 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:27:10+00:00" + "time": "2024-06-25T19:55:06+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.3", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef" + "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae9d3a6f3003a6caf56acd7466d8d52378d44fef", - "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", + "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", "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": { @@ -8408,13 +7557,13 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.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|^7.0" + "symfony/stopwatch": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -8442,7 +7591,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.4.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1" }, "funding": [ { @@ -8458,20 +7607,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "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/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { @@ -8481,7 +7630,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -8518,7 +7667,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" }, "funding": [ { @@ -8534,27 +7683,27 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/finder", - "version": "v6.4.0", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" + "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", + "url": "https://api.github.com/repos/symfony/finder/zipball/fbb0ba67688b780efbc886c1a0a0948dcf7205d6", + "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.0|^7.0" + "symfony/filesystem": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -8582,7 +7731,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.0" + "source": "https://github.com/symfony/finder/tree/v7.1.1" }, "funding": [ { @@ -8598,27 +7747,27 @@ "type": "tidelift" } ], - "time": "2023-10-31T17:30:12+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.5", + "version": "v6.4.9", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "f3c86a60a3615f466333a11fd42010d4382a82c7" + "reference": "6e9db0025db565bcf8f1d46ed734b549e51e6045" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/f3c86a60a3615f466333a11fd42010d4382a82c7", - "reference": "f3c86a60a3615f466333a11fd42010d4382a82c7", + "url": "https://api.github.com/repos/symfony/http-client/zipball/6e9db0025db565bcf8f1d46ed734b549e51e6045", + "reference": "6e9db0025db565bcf8f1d46ed734b549e51e6045", "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.1", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -8636,7 +7785,7 @@ "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", @@ -8675,7 +7824,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.5" + "source": "https://github.com/symfony/http-client/tree/v6.4.9" }, "funding": [ { @@ -8691,20 +7840,20 @@ "type": "tidelift" } ], - "time": "2024-03-02T12:45:30+00:00" + "time": "2024-06-28T07:59:05+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "1ee70e699b41909c209a0c930f11034b93578654" + "reference": "20414d96f391677bf80078aa55baece78b82647d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/1ee70e699b41909c209a0c930f11034b93578654", - "reference": "1ee70e699b41909c209a0c930f11034b93578654", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", "shasum": "" }, "require": { @@ -8713,7 +7862,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -8753,7 +7902,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" }, "funding": [ { @@ -8769,40 +7918,40 @@ "type": "tidelift" } ], - "time": "2023-07-30T20:28:31+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.4.4", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "ebc713bc6e6f4b53f46539fc158be85dfcd77304" + "reference": "74d171d5b6a1d9e4bfee09a41937c17a7536acfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ebc713bc6e6f4b53f46539fc158be85dfcd77304", - "reference": "ebc713bc6e6f4b53f46539fc158be85dfcd77304", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/74d171d5b6a1d9e4bfee09a41937c17a7536acfa", + "reference": "74d171d5b6a1d9e4bfee09a41937c17a7536acfa", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.1", "symfony/polyfill-php83": "^1.27" }, "conflict": { - "symfony/cache": "<6.3" + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4" }, "require-dev": { - "doctrine/dbal": "^2.13.1|^3|^4", + "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.3|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", - "symfony/mime": "^5.4|^6.0|^7.0", - "symfony/rate-limiter": "^5.4|^6.0|^7.0" + "symfony/cache": "^6.4|^7.0", + "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": { @@ -8830,7 +7979,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.4.4" + "source": "https://github.com/symfony/http-foundation/tree/v7.1.1" }, "funding": [ { @@ -8846,76 +7995,77 @@ "type": "tidelift" } ], - "time": "2024-02-08T15:01:18+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.5", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "f6947cb939d8efee137797382cb4db1af653ef75" + "reference": "ae3fa717db4d41a55d14c2bd92399e37cf5bc0f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6947cb939d8efee137797382cb4db1af653ef75", - "reference": "f6947cb939d8efee137797382cb4db1af653ef75", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ae3fa717db4d41a55d14c2bd92399e37cf5bc0f6", + "reference": "ae3fa717db4d41a55d14c2bd92399e37cf5bc0f6", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^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/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", - "symfony/doctrine-bridge": "<5.4", - "symfony/form": "<5.4", - "symfony/http-client": "<5.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/twig-bridge": "<6.4", "symfony/validator": "<6.4", - "symfony/var-dumper": "<6.3", - "twig/twig": "<2.13" + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.0.4" }, "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|^7.0", - "symfony/clock": "^6.2|^7.0", - "symfony/config": "^6.1|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/css-selector": "^5.4|^6.0|^7.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": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^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|^7.0", - "symfony/property-access": "^5.4.5|^6.0.5|^7.0", - "symfony/routing": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.4.4|^7.0.4", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/translation": "^5.4|^6.0|^7.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|^7.0", + "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", - "symfony/var-exporter": "^6.2|^7.0", - "twig/twig": "^2.13|^3.0.4" + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.0.4" }, "type": "library", "autoload": { @@ -8943,7 +8093,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.4.5" + "source": "https://github.com/symfony/http-kernel/tree/v7.1.2" }, "funding": [ { @@ -8959,43 +8109,43 @@ "type": "tidelift" } ], - "time": "2024-03-04T21:00:47+00:00" + "time": "2024-06-28T13:13:31+00:00" }, { "name": "symfony/mailer", - "version": "v6.4.4", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "791c5d31a8204cf3db0c66faab70282307f4376b" + "reference": "8fcff0af9043c8f8a8e229437cea363e282f9aee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/791c5d31a8204cf3db0c66faab70282307f4376b", - "reference": "791c5d31a8204cf3db0c66faab70282307f4376b", + "url": "https://api.github.com/repos/symfony/mailer/zipball/8fcff0af9043c8f8a8e229437cea363e282f9aee", + "reference": "8fcff0af9043c8f8a8e229437cea363e282f9aee", "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|^7.0", - "symfony/mime": "^6.2|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", "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|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/messenger": "^6.2|^7.0", - "symfony/twig-bridge": "^6.2|^7.0" + "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": { @@ -9023,7 +8173,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.4.4" + "source": "https://github.com/symfony/mailer/tree/v7.1.2" }, "funding": [ { @@ -9039,20 +8189,20 @@ "type": "tidelift" } ], - "time": "2024-02-03T21:33:47+00:00" + "time": "2024-06-28T08:00:31+00:00" }, { "name": "symfony/mailgun-mailer", - "version": "v6.4.4", + "version": "v6.4.9", "source": { "type": "git", "url": "https://github.com/symfony/mailgun-mailer.git", - "reference": "8c018872b40ce050590b6d18cf741db0c8313435" + "reference": "c4917eb14f31fb5c21442375c6baf7f51bd924e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/8c018872b40ce050590b6d18cf741db0c8313435", - "reference": "8c018872b40ce050590b6d18cf741db0c8313435", + "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/c4917eb14f31fb5c21442375c6baf7f51bd924e8", + "reference": "c4917eb14f31fb5c21442375c6baf7f51bd924e8", "shasum": "" }, "require": { @@ -9092,7 +8242,7 @@ "description": "Symfony Mailgun Mailer Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailgun-mailer/tree/v6.4.4" + "source": "https://github.com/symfony/mailgun-mailer/tree/v6.4.9" }, "funding": [ { @@ -9108,25 +8258,24 @@ "type": "tidelift" } ], - "time": "2024-02-14T06:31:46+00:00" + "time": "2024-06-28T07:59:05+00:00" }, { "name": "symfony/mime", - "version": "v6.4.3", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "5017e0a9398c77090b7694be46f20eb796262a34" + "reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/5017e0a9398c77090b7694be46f20eb796262a34", - "reference": "5017e0a9398c77090b7694be46f20eb796262a34", + "url": "https://api.github.com/repos/symfony/mime/zipball/26a00b85477e69a4bab63b66c5dce64f18b0cbfc", + "reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -9134,17 +8283,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.3.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|^7.0", - "symfony/property-access": "^5.4|^6.0|^7.0", - "symfony/property-info": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.3.2|^7.0" + "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": { @@ -9176,7 +8326,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.4.3" + "source": "https://github.com/symfony/mime/tree/v7.1.2" }, "funding": [ { @@ -9192,20 +8342,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T08:32:12+00:00" + "time": "2024-06-28T10:03:55+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { @@ -9255,7 +8405,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" }, "funding": [ { @@ -9271,20 +8421,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -9333,7 +8483,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -9349,20 +8499,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "a287ed7475f85bf6f61890146edbc932c0fff919" + "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919", - "reference": "a287ed7475f85bf6f61890146edbc932c0fff919", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", + "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", "shasum": "" }, "require": { @@ -9417,7 +8567,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.30.0" }, "funding": [ { @@ -9433,20 +8583,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -9498,7 +8648,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -9514,20 +8664,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -9578,7 +8728,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -9594,20 +8744,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" + "reference": "10112722600777e02d2745716b70c5db4ca70442" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/10112722600777e02d2745716b70c5db4ca70442", + "reference": "10112722600777e02d2745716b70c5db4ca70442", "shasum": "" }, "require": { @@ -9651,7 +8801,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.30.0" }, "funding": [ { @@ -9667,20 +8817,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -9731,7 +8881,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -9747,25 +8897,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "86fcae159633351e5fd145d1c47de6c528f8caff" + "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff", - "reference": "86fcae159633351e5fd145d1c47de6c528f8caff", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", + "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-php80": "^1.14" + "php": ">=7.1" }, "type": "library", "extra": { @@ -9808,7 +8957,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0" }, "funding": [ { @@ -9824,20 +8973,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:35:24+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853" + "reference": "2ba1f33797470debcda07fe9dce20a0003df18e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/3abdd21b0ceaa3000ee950097bc3cf9efc137853", - "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/2ba1f33797470debcda07fe9dce20a0003df18e9", + "reference": "2ba1f33797470debcda07fe9dce20a0003df18e9", "shasum": "" }, "require": { @@ -9887,7 +9036,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.30.0" }, "funding": [ { @@ -9903,24 +9052,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/process", - "version": "v6.4.4", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "710e27879e9be3395de2b98da3f52a946039f297" + "reference": "febf90124323a093c7ee06fdb30e765ca3c20028" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297", - "reference": "710e27879e9be3395de2b98da3f52a946039f297", + "url": "https://api.github.com/repos/symfony/process/zipball/febf90124323a093c7ee06fdb30e765ca3c20028", + "reference": "febf90124323a093c7ee06fdb30e765ca3c20028", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -9948,7 +9097,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.4" + "source": "https://github.com/symfony/process/tree/v7.1.1" }, "funding": [ { @@ -9964,47 +9113,42 @@ "type": "tidelift" } ], - "time": "2024-02-20T12:31:00+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v2.3.1", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e" + "reference": "9a5dbb606da711f5d40a7596ad577856f9402140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e", - "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/9a5dbb606da711f5d40a7596ad577856f9402140", + "reference": "9a5dbb606da711f5d40a7596ad577856f9402140", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/http-message": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.5 || ^3.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.3-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Bridge\\PsrHttpMessage\\": "" @@ -10024,11 +9168,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", @@ -10036,8 +9180,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.3.1" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.1" }, "funding": [ { @@ -10053,40 +9196,38 @@ "type": "tidelift" } ], - "time": "2023-07-26T11:53:26+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/routing", - "version": "v6.4.5", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "7fe30068e207d9c31c0138501ab40358eb2d49a4" + "reference": "60c31bab5c45af7f13091b87deb708830f3c96c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/7fe30068e207d9c31c0138501ab40358eb2d49a4", - "reference": "7fe30068e207d9c31c0138501ab40358eb2d49a4", + "url": "https://api.github.com/repos/symfony/routing/zipball/60c31bab5c45af7f13091b87deb708830f3c96c0", + "reference": "60c31bab5c45af7f13091b87deb708830f3c96c0", "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|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/yaml": "^5.4|^6.0|^7.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": { @@ -10120,7 +9261,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.5" + "source": "https://github.com/symfony/routing/tree/v7.1.1" }, "funding": [ { @@ -10136,25 +9277,26 @@ "type": "tidelift" } ], - "time": "2024-02-27T12:33:30+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.4.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -10162,7 +9304,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -10202,7 +9344,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -10218,24 +9360,24 @@ "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/string", - "version": "v6.4.4", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9" + "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", - "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", + "url": "https://api.github.com/repos/symfony/string/zipball/14221089ac66cf82e3cf3d1c1da65de305587ff8", + "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8", "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", @@ -10245,11 +9387,12 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/intl": "^6.2|^7.0", + "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|^7.0" + "symfony/var-exporter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -10288,7 +9431,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.4" + "source": "https://github.com/symfony/string/tree/v7.1.2" }, "funding": [ { @@ -10304,37 +9447,36 @@ "type": "tidelift" } ], - "time": "2024-02-01T13:16:41+00:00" + "time": "2024-06-28T09:27:18+00:00" }, { "name": "symfony/translation", - "version": "v6.4.4", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "bce6a5a78e94566641b2594d17e48b0da3184a8e" + "reference": "cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/bce6a5a78e94566641b2594d17e48b0da3184a8e", - "reference": "bce6a5a78e94566641b2594d17e48b0da3184a8e", + "url": "https://api.github.com/repos/symfony/translation/zipball/cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3", + "reference": "cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "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" @@ -10342,17 +9484,17 @@ "require-dev": { "nikic/php-parser": "^4.18|^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.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|^7.0", - "symfony/intl": "^5.4|^6.0|^7.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|^7.0", + "symfony/routing": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -10383,7 +9525,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.4" + "source": "https://github.com/symfony/translation/tree/v7.1.1" }, "funding": [ { @@ -10399,20 +9541,20 @@ "type": "tidelift" } ], - "time": "2024-02-20T13:16:58+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.4.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "06450585bf65e978026bda220cdebca3f867fde7" + "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/06450585bf65e978026bda220cdebca3f867fde7", - "reference": "06450585bf65e978026bda220cdebca3f867fde7", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", + "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", "shasum": "" }, "require": { @@ -10421,7 +9563,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -10461,7 +9603,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.4.1" + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.0" }, "funding": [ { @@ -10477,28 +9619,28 @@ "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/uid", - "version": "v6.4.3", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "1d31267211cc3a2fff32bcfc7c1818dac41b6fc0" + "reference": "bb59febeecc81528ff672fad5dab7f06db8c8277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/1d31267211cc3a2fff32bcfc7c1818dac41b6fc0", - "reference": "1d31267211cc3a2fff32bcfc7c1818dac41b6fc0", + "url": "https://api.github.com/repos/symfony/uid/zipball/bb59febeecc81528ff672fad5dab7f06db8c8277", + "reference": "bb59febeecc81528ff672fad5dab7f06db8c8277", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0" + "symfony/console": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -10535,7 +9677,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.4.3" + "source": "https://github.com/symfony/uid/tree/v7.1.1" }, "funding": [ { @@ -10551,38 +9693,36 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.4", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "b439823f04c98b84d4366c79507e9da6230944b1" + "reference": "5857c57c6b4b86524c08cf4f4bc95327270a816d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b439823f04c98b84d4366c79507e9da6230944b1", - "reference": "b439823f04c98b84d4366c79507e9da6230944b1", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5857c57c6b4b86524c08cf4f4bc95327270a816d", + "reference": "5857c57c6b4b86524c08cf4f4bc95327270a816d", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "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|^7.0", - "symfony/error-handler": "^6.3|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/uid": "^5.4|^6.0|^7.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.0.4" }, "bin": [ "Resources/bin/var-dump-server" @@ -10620,7 +9760,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.4" + "source": "https://github.com/symfony/var-dumper/tree/v7.1.2" }, "funding": [ { @@ -10636,28 +9776,29 @@ "type": "tidelift" } ], - "time": "2024-02-15T11:23:52+00:00" + "time": "2024-06-28T08:00:31+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.4", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "0bd342e24aef49fc82a21bd4eedd3e665d177e5b" + "reference": "b80a669a2264609f07f1667f891dbfca25eba44c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0bd342e24aef49fc82a21bd4eedd3e665d177e5b", - "reference": "0bd342e24aef49fc82a21bd4eedd3e665d177e5b", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/b80a669a2264609f07f1667f891dbfca25eba44c", + "reference": "b80a669a2264609f07f1667f891dbfca25eba44c", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.2" }, "require-dev": { - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -10695,7 +9836,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.4" + "source": "https://github.com/symfony/var-exporter/tree/v7.1.2" }, "funding": [ { @@ -10711,61 +9852,7 @@ "type": "tidelift" } ], - "time": "2024-02-26T08:37:45+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-06-28T08:00:31+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -10822,23 +9909,23 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4" + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", - "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.2", + "graham-campbell/result-type": "^1.1.3", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.2", + "phpoption/phpoption": "^1.9.3", "symfony/polyfill-ctype": "^1.24", "symfony/polyfill-mbstring": "^1.24", "symfony/polyfill-php80": "^1.24" @@ -10855,7 +9942,7 @@ "extra": { "bamarni-bin": { "bin-links": true, - "forward-command": true + "forward-command": false }, "branch-alias": { "dev-master": "5.6-dev" @@ -10890,7 +9977,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.0" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" }, "funding": [ { @@ -10902,7 +9989,7 @@ "type": "tidelift" } ], - "time": "2023-11-12T22:43:29+00:00" + "time": "2024-07-20T21:52:34+00:00" }, { "name": "voku/portable-ascii", @@ -11417,168 +10504,6 @@ } ], "packages-dev": [ - { - "name": "brianium/paratest", - "version": "v6.11.0", - "source": { - "type": "git", - "url": "https://github.com/paratestphp/paratest.git", - "reference": "8083a421cee7dad847ee7c464529043ba30de380" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/8083a421cee7dad847ee7c464529043ba30de380", - "reference": "8083a421cee7dad847ee7c464529043ba30de380", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-simplexml": "*", - "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", - "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.28 || ^6.3.4 || ^7.0.0", - "symfony/process": "^5.4.28 || ^6.3.4 || ^7.0.0" - }, - "require-dev": { - "doctrine/coding-standard": "^12.0.0", - "ext-pcov": "*", - "ext-posix": "*", - "infection/infection": "^0.27.6", - "squizlabs/php_codesniffer": "^3.7.2", - "symfony/filesystem": "^5.4.25 || ^6.3.1 || ^7.0.0", - "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.11.0" - }, - "funding": [ - { - "url": "https://github.com/sponsors/Slamdunk", - "type": "github" - }, - { - "url": "https://paypal.me/filippotessarotto", - "type": "paypal" - } - ], - "time": "2023-10-31T09:13:57+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.1", @@ -11642,67 +10567,6 @@ }, "time": "2024-01-02T13:46:09+00:00" }, - { - "name": "fidry/cpu-core-counter", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "fidry/makefile": "^0.2.0", - "fidry/php-cs-fixer-config": "^1.1.2", - "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": "^8.5.31 || ^9.5.26", - "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/1.1.0" - }, - "funding": [ - { - "url": "https://github.com/theofidry", - "type": "github" - } - ], - "time": "2024-02-07T09:43:46+00:00" - }, { "name": "filp/whoops", "version": "2.15.4", @@ -11825,77 +10689,18 @@ }, "time": "2020-07-09T08:09:16+00:00" }, - { - "name": "jean85/pretty-package-versions", - "version": "2.0.5", - "source": { - "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "shasum": "" - }, - "require": { - "composer-runtime-api": "^2.0.0", - "php": "^7.1|^8.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" - } - }, - "autoload": { - "psr-4": { - "Jean85\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" - } - ], - "description": "A library to get pretty versions strings of installed dependencies", - "keywords": [ - "composer", - "package", - "release", - "versions" - ], - "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" - }, - "time": "2021-10-08T21:21:46+00:00" - }, { "name": "laravel/pint", - "version": "v1.14.0", + "version": "v1.16.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "6b127276e3f263f7bb17d5077e9e0269e61b2a0e" + "reference": "51f1ba679a6afe0315621ad143d788bd7ded0eca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/6b127276e3f263f7bb17d5077e9e0269e61b2a0e", - "reference": "6b127276e3f263f7bb17d5077e9e0269e61b2a0e", + "url": "https://api.github.com/repos/laravel/pint/zipball/51f1ba679a6afe0315621ad143d788bd7ded0eca", + "reference": "51f1ba679a6afe0315621ad143d788bd7ded0eca", "shasum": "" }, "require": { @@ -11906,13 +10711,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.49.0", - "illuminate/view": "^10.43.0", - "larastan/larastan": "^2.8.1", - "laravel-zero/framework": "^10.3.0", - "mockery/mockery": "^1.6.7", + "friendsofphp/php-cs-fixer": "^3.59.3", + "illuminate/view": "^10.48.12", + "larastan/larastan": "^2.9.7", + "laravel-zero/framework": "^10.4.0", + "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.33.6" + "pestphp/pest": "^2.34.8" }, "bin": [ "builds/pint" @@ -11948,41 +10753,39 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-02-20T17:38:05+00:00" + "time": "2024-07-09T15:58:08+00:00" }, { "name": "laravel/telescope", - "version": "v4.17.6", + "version": "v5.1.1", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "2d453dc629b27e8cf39fb1217aba062f8c54e690" + "reference": "7355643b998027f8fa9393e6c8c884f126204a80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/2d453dc629b27e8cf39fb1217aba062f8c54e690", - "reference": "2d453dc629b27e8cf39fb1217aba062f8c54e690", + "url": "https://api.github.com/repos/laravel/telescope/zipball/7355643b998027f8fa9393e6c8c884f126204a80", + "reference": "7355643b998027f8fa9393e6c8c884f126204a80", "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" @@ -12017,22 +10820,22 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v4.17.6" + "source": "https://github.com/laravel/telescope/tree/v5.1.1" }, - "time": "2024-02-08T15:04:38+00:00" + "time": "2024-06-27T07:12:23+00:00" }, { "name": "mockery/mockery", - "version": "1.6.7", + "version": "1.6.12", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06" + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", - "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", "shasum": "" }, "require": { @@ -12044,8 +10847,8 @@ "phpunit/phpunit": "<8.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.6.10", - "symplify/easy-coding-standard": "^12.0.8" + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" }, "type": "library", "autoload": { @@ -12102,20 +10905,20 @@ "security": "https://github.com/mockery/mockery/security/advisories", "source": "https://github.com/mockery/mockery" }, - "time": "2023-12-10T02:24:34+00:00" + "time": "2024-05-16T03:13:13+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -12123,11 +10926,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", @@ -12153,7 +10957,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.0" }, "funding": [ { @@ -12161,49 +10965,58 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nunomaduro/collision", - "version": "v6.4.0", + "version": "v8.3.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "f05978827b9343cba381ca05b8c7deee346b6015" + "reference": "b49f5b2891ce52726adfd162841c69d4e4c84229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/f05978827b9343cba381ca05b8c7deee346b6015", - "reference": "f05978827b9343cba381ca05b8c7deee346b6015", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/b49f5b2891ce52726adfd162841c69d4e4c84229", + "reference": "b49f5b2891ce52726adfd162841c69d4e4c84229", "shasum": "" }, "require": { - "filp/whoops": "^2.14.5", - "php": "^8.0.0", - "symfony/console": "^6.0.2" + "filp/whoops": "^2.15.4", + "nunomaduro/termwind": "^2.0.1", + "php": "^8.2.0", + "symfony/console": "^7.1.2" + }, + "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.16.0", + "laravel/pint": "^1.16.2", + "laravel/sail": "^1.30.2", + "laravel/sanctum": "^4.0.2", + "laravel/tinker": "^2.9.0", + "orchestra/testbench-core": "^9.2.1", + "pestphp/pest": "^2.34.9 || ^3.0.0", + "sebastian/environment": "^6.1.0 || ^7.0.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/" } @@ -12249,7 +11062,7 @@ "type": "patreon" } ], - "time": "2023-01-03T12:54:54+00:00" + "time": "2024-07-16T22:41:01+00:00" }, { "name": "phar-io/manifest", @@ -12371,35 +11184,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "11.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/19b6365ab8b59a64438c0c3f4241feeb480c9861", + "reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", - "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", + "nikic/php-parser": "^5.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.0", + "phpunit/php-text-template": "^4.0", + "sebastian/code-unit-reverse-lookup": "^4.0", + "sebastian/complexity": "^4.0", + "sebastian/environment": "^7.0", + "sebastian/lines-of-code": "^3.0", + "sebastian/version": "^5.0", "theseer/tokenizer": "^1.2.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -12408,7 +11221,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "11.0-dev" } }, "autoload": { @@ -12437,7 +11250,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.5" }, "funding": [ { @@ -12445,32 +11258,32 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-07-03T05:05:37+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26" }, "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/6ed896bf50bbbfe4d504a33ed5886278c78e4a26", + "reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26", "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": { @@ -12497,7 +11310,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.0.1" }, "funding": [ { @@ -12505,28 +11319,28 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2024-07-03T05:06:37+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": "*" @@ -12534,7 +11348,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -12560,7 +11374,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": [ { @@ -12568,32 +11383,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": { @@ -12619,7 +11434,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": [ { @@ -12627,32 +11443,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": { @@ -12678,7 +11494,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": [ { @@ -12686,54 +11503,51 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2024-07-03T05:09:35+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.17", + "version": "11.2.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd" + "reference": "a7a29e8d3113806f18f99d670f580a30e8ffff39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1a156980d78a6666721b7e8e8502fe210b587fcd", - "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a7a29e8d3113806f18f99d670f580a30e8ffff39", + "reference": "a7a29e8d3113806f18f99d670f580a30e8ffff39", "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.28", - "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.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.5", + "phpunit/php-file-iterator": "^5.0.1", + "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.0.1", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.1.3", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.0.1", + "sebastian/version": "^5.0.1" }, "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" @@ -12741,7 +11555,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-main": "11.2-dev" } }, "autoload": { @@ -12773,7 +11587,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.17" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.2.8" }, "funding": [ { @@ -12789,32 +11603,32 @@ "type": "tidelift" } ], - "time": "2024-02-23T13:14:51+00:00" + "time": "2024-07-18T14:56:37+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.2", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "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": { @@ -12837,7 +11651,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.2" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" }, "funding": [ { @@ -12845,32 +11660,32 @@ "type": "github" } ], - "time": "2024-03-02T06:27:43+00:00" + "time": "2024-07-03T04:41:36+00:00" }, { "name": "sebastian/code-unit", - "version": "1.0.8", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "reference": "6bb7d09d6623567178cf54126afa9c2310114268" }, "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/6bb7d09d6623567178cf54126afa9c2310114268", + "reference": "6bb7d09d6623567178cf54126afa9c2310114268", "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": { @@ -12893,7 +11708,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.1" }, "funding": [ { @@ -12901,32 +11717,32 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2024-07-03T04:44:28+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": { @@ -12948,7 +11764,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": [ { @@ -12956,34 +11773,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.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "131942b86d3587291067a94f295498ab6ac79c20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/131942b86d3587291067a94f295498ab6ac79c20", + "reference": "131942b86d3587291067a94f295498ab6ac79c20", "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.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -13022,7 +11841,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.0.1" }, "funding": [ { @@ -13030,33 +11850,33 @@ "type": "github" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2024-07-03T04:48:07+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.3", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "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": { @@ -13079,7 +11899,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" }, "funding": [ { @@ -13087,33 +11908,33 @@ "type": "github" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2024-07-03T04:49:50+00:00" }, { "name": "sebastian/diff", - "version": "4.0.6", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "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": { @@ -13145,7 +11966,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" }, "funding": [ { @@ -13153,27 +11975,27 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+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": "*" @@ -13181,7 +12003,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "7.2-dev" } }, "autoload": { @@ -13200,7 +12022,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", @@ -13208,7 +12030,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": [ { @@ -13216,34 +12039,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.6", + "version": "6.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", + "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", "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.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -13285,7 +12108,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.1.3" }, "funding": [ { @@ -13293,38 +12117,35 @@ "type": "github" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2024-07-03T04:56:19+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "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": { @@ -13343,13 +12164,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.7" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" }, "funding": [ { @@ -13357,33 +12179,33 @@ "type": "github" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2024-07-03T04:57:36+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.4", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "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": { @@ -13406,7 +12228,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.4" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" }, "funding": [ { @@ -13414,34 +12237,34 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+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": { @@ -13463,7 +12286,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": [ { @@ -13471,32 +12295,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": { @@ -13518,7 +12342,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": [ { @@ -13526,32 +12351,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": { @@ -13581,7 +12406,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": [ { @@ -13589,87 +12415,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.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb6a6566f9589e86661291d13eba708cce5eb4aa", + "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -13692,7 +12463,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.0.1" }, "funding": [ { @@ -13700,29 +12472,29 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2024-07-03T05:11:49+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/45c9debb7d039ce9b97de2f749c2cf5832a06ac4", + "reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -13745,7 +12517,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.1" }, "funding": [ { @@ -13753,7 +12526,7 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2024-07-03T05:13:08+00:00" }, { "name": "theseer/tokenizer", @@ -13812,7 +12585,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.1|^8.2|^8.3", + "php": "^8.2|^8.3", "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", 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/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/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 5aa37f2fe..a155e9536 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -90,6 +90,8 @@ return [ 'redis:story' => 30, 'redis:mmo' => 30, 'redis:intbg' => 30, + 'redis:adelete' => 30, + 'redis:groups' => 30, ], /* @@ -173,7 +175,7 @@ return [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg'], + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete'], 'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'), 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20), @@ -187,7 +189,7 @@ return [ 'local' => [ 'supervisor-1' => [ 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg'], + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete'], 'balance' => 'auto', 'minProcesses' => 1, 'maxProcesses' => 20, diff --git a/config/instance.php b/config/instance.php index cfc0468ab..c3912740f 100644 --- a/config/instance.php +++ b/config/instance.php @@ -1,136 +1,136 @@ 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), + ], + ], - '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' => [ - 'cached' => env('INSTANCE_PUBLIC_TIMELINE_CACHED', false), - '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', false) : 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), - 'max_domain_blocks' => env('PF_MAX_DOMAIN_BLOCKS', 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), @@ -143,14 +143,14 @@ return [ ], 'software-update' => [ - 'disable_failed_warning' => env('INSTANCE_SOFTWARE_UPDATE_DISABLE_FAILED_WARNING', false) + '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) - ] + 'delete_after_days' => env('INSTANCE_NOTIFY_AUTO_GC_DEL_AFTER_DAYS', 365), + ], ], 'curated_registration' => [ @@ -173,7 +173,9 @@ return [ 'max_per_day' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_MPD', 10), ], 'on_user_response' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_USER_RESPONSE', false), - ] + ], ], ], + + 'show_peers' => env('INSTANCE_SHOW_PEERS', false), ]; diff --git a/config/pixelfed.php b/config/pixelfed.php index 9da274434..b1b42daa1 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.13', + 'version' => '0.12.3', /* |-------------------------------------------------------------------------- diff --git a/config/services.php b/config/services.php index 58db77fe5..8f00697b5 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,7 @@ return [ 'secret' => env('STRIPE_SECRET'), ], + 'expo' => [ + 'access_token' => env('EXPO_ACCESS_TOKEN'), + ], ]; 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/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..e5d32f2db 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. * 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..c7cab9c5b 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. * 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_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_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 49a9b2c58..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 @@ -41,15 +41,15 @@ class AddComposeSettingsToUserSettingsTable extends Migration Schema::table('media', function (Blueprint $table) { $table->string('caption')->change(); - $schemaManager = Schema::getConnection()->getDoctrineSchemaManager(); - $indexesFound = $schemaManager->listTableIndexes('media'); - if (array_key_exists('media_profile_id_index', $indexesFound)) { + $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 (array_key_exists('media_mime_index', $indexesFound)) { + if (in_array('media_mime_index', $indexesFound)) { $table->dropIndex('media_mime_index'); } - if (array_key_exists('media_license_index', $indexesFound)) { + 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/2024_01_09_052419_create_parental_controls_table.php b/database/migrations/2024_01_09_052419_create_parental_controls_table.php index 6713e6849..9974c6241 100644 --- a/database/migrations/2024_01_09_052419_create_parental_controls_table.php +++ b/database/migrations/2024_01_09_052419_create_parental_controls_table.php @@ -25,9 +25,9 @@ return new class extends Migration }); Schema::table('user_roles', function (Blueprint $table) { - $schemaManager = Schema::getConnection()->getDoctrineSchemaManager(); - $indexesFound = $schemaManager->listTableIndexes('user_roles'); - if (array_key_exists('user_roles_profile_id_unique', $indexesFound)) { + $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(); @@ -42,9 +42,9 @@ return new class extends Migration Schema::dropIfExists('parental_controls'); Schema::table('user_roles', function (Blueprint $table) { - $schemaManager = Schema::getConnection()->getDoctrineSchemaManager(); - $indexesFound = $schemaManager->listTableIndexes('user_roles'); - if (array_key_exists('user_roles_profile_id_unique', $indexesFound)) { + $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_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..99f272be9 --- /dev/null +++ b/database/migrations/2024_05_20_062706_update_group_posts_table.php @@ -0,0 +1,48 @@ +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/docker-compose.migrate.yml b/docker-compose.migrate.yml index b31771f27..d0040b051 100644 --- a/docker-compose.migrate.yml +++ b/docker-compose.migrate.yml @@ -1,9 +1,7 @@ --- -version: "3" - services: migrate: - image: "secoresearch/rsync" + image: "servercontainers/rsync" entrypoint: "" working_dir: /migrate command: 'bash -c "exit 1"' diff --git a/docker-compose.yml b/docker-compose.yml index 5df433c83..2a805eb7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,4 @@ --- -# Require 3.8 to ensure people use a recent version of Docker + Compose -version: "3.8" - ############################################################### # Please see docker/README.md for usage information ############################################################### @@ -80,6 +77,7 @@ services: - "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:-}" @@ -131,6 +129,7 @@ services: - "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:-}" @@ -166,12 +165,12 @@ services: environment: TZ: "${TZ:?error}" # MySQL (Oracle) - "Environment Variables" at https://hub.docker.com/_/mysql - MYSQL_ROOT_PASSWORD: "${DB_PASSWORD:?error}" + 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: "${DB_PASSWORD:?error}" + MARIADB_ROOT_PASSWORD: "${DOCKER_DB_ROOT_PASSWORD:?error}" MARIADB_USER: "${DB_USERNAME:?error}" MARIADB_PASSWORD: "${DB_PASSWORD:?error}" MARIADB_DATABASE: "${DB_DATABASE:?error}" 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/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh index efff58110..dc9dc7591 100755 --- a/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -17,7 +17,7 @@ run-as-current-user chown --verbose --recursive "${RUNTIME_UID}:${RUNTIME_GID}" : "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS:=""}" declare -a ensure_ownership_paths=() -IFS=' ' read -ar ensure_ownership_paths <<<"${DOCKER_APP_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]." diff --git a/docker/shared/root/docker/entrypoint.d/05-templating.sh b/docker/shared/root/docker/entrypoint.d/05-templating.sh index 4d229b11c..e699778cf 100755 --- a/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -16,12 +16,8 @@ entrypoint-set-script-name "$0" declare template_file relative_template_file_path output_file_dir # load all dot-env config files -load-config-files +load-and-export-config-files -# export all dot-env variables so they are available in templating -# -# shellcheck disable=SC2068 -export ${seen_dot_env_variables[@]} 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 diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh index 73f6a4f3e..055cf25d7 100755 --- a/docker/shared/root/docker/entrypoint.sh +++ b/docker/shared/root/docker/entrypoint.sh @@ -28,7 +28,7 @@ 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 -ar skip_scripts <<< "$ENTRYPOINT_SKIP_SCRIPTS" +IFS=' ' read -r -a skip_scripts <<< "$ENTRYPOINT_SKIP_SCRIPTS" # Ensure the entrypoint root folder exists mkdir -p "${ENTRYPOINT_D_ROOT}" diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 3a21ee89e..631b0ef0e 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -27,9 +27,6 @@ declare -a dot_env_files=( /var/www/.env ) -# environment keys seen when source dot files (so we can [export] them) -declare -ga seen_dot_env_variables=() - declare -g docker_state_path docker_state_path="$(readlink -f ./storage/docker)" @@ -250,13 +247,23 @@ function log-info-stderr() fi } -# @description Loads the dot-env files used by Docker and track the keys present in the configuration. -# @sets seen_dot_env_variables array List of config keys discovered during loading -function load-config-files() -{ - # Associative array (aka map/dictionary) holding the unique keys found in dot-env files - local -A _tmp_dot_env_keys +# @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" @@ -264,19 +271,11 @@ function load-config-files() fi log-info "Sourcing ${file}" + if ((export_vars)); then set -o allexport; fi # shellcheck disable=SC1090 source "${file}" - - # find all keys in the dot-env file and store them in our temp associative array - for k in $(grep -v '^#' "${file}" | cut -d"=" -f1 | xargs); do - _tmp_dot_env_keys[$k]=1 - done + if ((export_vars)); then set +o allexport; fi done - - # Used in other scripts (like templating) for [export]-ing the values - # - # shellcheck disable=SC2034 - seen_dot_env_variables=("${!_tmp_dot_env_keys[@]}") } # @description Checks if $needle exists in $haystack @@ -467,14 +466,14 @@ function await-database-ready() case "${DB_CONNECTION:-}" in mysql) # shellcheck disable=SC2154 - while ! echo "SELECT 1" | mysql --user="${DB_USERNAME}" --password="${DB_PASSWORD}" --host="${DB_HOST}" "${DB_DATABASE}" --silent >/dev/null; do + 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}" "${DB_DATABASE}" >/dev/null; do + 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 ;; diff --git a/package-lock.json b/package-lock.json index 0b34d7765..27289344f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,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", @@ -71,108 +71,52 @@ } }, "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.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.2", + "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.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", + "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.4", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/helpers": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -196,13 +140,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -255,16 +199,16 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.10.tgz", - "integrity": "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz", + "integrity": "sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" @@ -309,9 +253,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz", + "integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", @@ -366,11 +310,11 @@ } }, "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.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -406,9 +350,9 @@ } }, "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.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", "engines": { "node": ">=6.9.0" } @@ -430,12 +374,12 @@ } }, "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.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { @@ -479,9 +423,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "engines": { "node": ">=6.9.0" } @@ -516,26 +460,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", + "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -598,9 +543,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -608,12 +553,27 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.23.3", - "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.23.3.tgz", - "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz", + "integrity": "sha512-qpl6vOOEEzTLLcsuqYYo8yDtrTocmu2xkGvgNebvPjT9DTtfFYGmgDqY+rBYXNlqL4s9qLDn6xkrJv4RxAPiTA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "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.24.1", + "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.24.1.tgz", + "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -623,13 +583,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", - "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", + "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.23.3" + "@babel/plugin-transform-optional-chaining": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -639,12 +599,12 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", - "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", + "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -742,11 +702,11 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", - "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -756,11 +716,11 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", - "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", + "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -901,11 +861,11 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", - "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", + "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -915,12 +875,12 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", - "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz", + "integrity": "sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-remap-async-to-generator": "^7.22.20", "@babel/plugin-syntax-async-generators": "^7.8.4" }, @@ -932,12 +892,12 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz", + "integrity": "sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-module-imports": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { @@ -948,11 +908,11 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", - "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", + "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -962,11 +922,11 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", - "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz", + "integrity": "sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -976,12 +936,12 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", - "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", + "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -991,12 +951,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", - "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", + "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.4", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { @@ -1007,16 +967,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", - "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz", + "integrity": "sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -1028,12 +988,12 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", - "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", + "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.15" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/template": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1043,11 +1003,11 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", - "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz", + "integrity": "sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1057,12 +1017,12 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", - "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", + "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1072,11 +1032,11 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", - "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", + "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1086,11 +1046,11 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", - "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", + "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { @@ -1101,12 +1061,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", - "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", + "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1116,11 +1076,11 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", - "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", + "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { @@ -1131,11 +1091,11 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", - "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", + "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { @@ -1146,13 +1106,13 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", - "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", + "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1162,11 +1122,11 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", - "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", + "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { @@ -1177,11 +1137,11 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", - "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", + "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1191,11 +1151,11 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", - "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", + "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -1206,11 +1166,11 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", - "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", + "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1220,12 +1180,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", - "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", + "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", "dependencies": { "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1235,12 +1195,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", "dependencies": { "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-simple-access": "^7.22.5" }, "engines": { @@ -1251,13 +1211,13 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", - "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", + "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { @@ -1268,12 +1228,12 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", - "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", + "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", "dependencies": { "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1298,11 +1258,11 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", - "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", + "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1312,11 +1272,11 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", - "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", + "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { @@ -1327,11 +1287,11 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", - "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", + "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { @@ -1342,15 +1302,14 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz", + "integrity": "sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA==", "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.23.3" + "@babel/plugin-transform-parameters": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -1360,12 +1319,12 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", - "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", + "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -1375,11 +1334,11 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", - "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", + "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { @@ -1390,11 +1349,11 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", - "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz", + "integrity": "sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, @@ -1406,11 +1365,11 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", - "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz", + "integrity": "sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1420,12 +1379,12 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", - "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", + "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1435,13 +1394,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", - "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz", + "integrity": "sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -1452,11 +1411,11 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", - "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", + "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1466,11 +1425,11 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", - "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", + "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "regenerator-transform": "^0.15.2" }, "engines": { @@ -1481,11 +1440,11 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", - "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", + "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1495,15 +1454,15 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz", - "integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.3.tgz", + "integrity": "sha512-J0BuRPNlNqlMTRJ72eVptpt9VcInbxO6iP3jaxr+1NPhC0UkKL+6oeX6VXMEYdADnuqmMmsBspt4d5w8Y/TCbQ==", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-plugin-utils": "^7.24.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "engines": { @@ -1522,11 +1481,11 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", - "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", + "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1536,11 +1495,11 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", - "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", + "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { @@ -1551,11 +1510,11 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", - "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", + "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1565,11 +1524,11 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", - "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", + "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1579,11 +1538,11 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", - "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz", + "integrity": "sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1593,11 +1552,11 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", - "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", + "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1607,12 +1566,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", - "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", + "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1622,12 +1581,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", - "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", + "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1637,12 +1596,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", - "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", + "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1652,25 +1611,26 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", - "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.4.tgz", + "integrity": "sha512-7Kl6cSmYkak0FK/FXjSEnLJ1N9T/WA2RkMhu17gZ/dsxKJUuTYNIylahPTzqpLyJN4WhDif8X0XK1R8Wsguo/A==", "dependencies": { - "@babel/compat-data": "^7.23.5", + "@babel/compat-data": "^7.24.4", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.4", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1", "@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.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.24.1", + "@babel/plugin-syntax-import-attributes": "^7.24.1", "@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", @@ -1682,58 +1642,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.9", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.9", - "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.24.1", + "@babel/plugin-transform-async-generator-functions": "^7.24.3", + "@babel/plugin-transform-async-to-generator": "^7.24.1", + "@babel/plugin-transform-block-scoped-functions": "^7.24.1", + "@babel/plugin-transform-block-scoping": "^7.24.4", + "@babel/plugin-transform-class-properties": "^7.24.1", + "@babel/plugin-transform-class-static-block": "^7.24.4", + "@babel/plugin-transform-classes": "^7.24.1", + "@babel/plugin-transform-computed-properties": "^7.24.1", + "@babel/plugin-transform-destructuring": "^7.24.1", + "@babel/plugin-transform-dotall-regex": "^7.24.1", + "@babel/plugin-transform-duplicate-keys": "^7.24.1", + "@babel/plugin-transform-dynamic-import": "^7.24.1", + "@babel/plugin-transform-exponentiation-operator": "^7.24.1", + "@babel/plugin-transform-export-namespace-from": "^7.24.1", + "@babel/plugin-transform-for-of": "^7.24.1", + "@babel/plugin-transform-function-name": "^7.24.1", + "@babel/plugin-transform-json-strings": "^7.24.1", + "@babel/plugin-transform-literals": "^7.24.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.1", + "@babel/plugin-transform-member-expression-literals": "^7.24.1", + "@babel/plugin-transform-modules-amd": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-modules-systemjs": "^7.24.1", + "@babel/plugin-transform-modules-umd": "^7.24.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.23.4", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-new-target": "^7.24.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", + "@babel/plugin-transform-numeric-separator": "^7.24.1", + "@babel/plugin-transform-object-rest-spread": "^7.24.1", + "@babel/plugin-transform-object-super": "^7.24.1", + "@babel/plugin-transform-optional-catch-binding": "^7.24.1", + "@babel/plugin-transform-optional-chaining": "^7.24.1", + "@babel/plugin-transform-parameters": "^7.24.1", + "@babel/plugin-transform-private-methods": "^7.24.1", + "@babel/plugin-transform-private-property-in-object": "^7.24.1", + "@babel/plugin-transform-property-literals": "^7.24.1", + "@babel/plugin-transform-regenerator": "^7.24.1", + "@babel/plugin-transform-reserved-words": "^7.24.1", + "@babel/plugin-transform-shorthand-properties": "^7.24.1", + "@babel/plugin-transform-spread": "^7.24.1", + "@babel/plugin-transform-sticky-regex": "^7.24.1", + "@babel/plugin-transform-template-literals": "^7.24.1", + "@babel/plugin-transform-typeof-symbol": "^7.24.1", + "@babel/plugin-transform-unicode-escapes": "^7.24.1", + "@babel/plugin-transform-unicode-property-regex": "^7.24.1", + "@babel/plugin-transform-unicode-regex": "^7.24.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -1771,9 +1731,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1782,31 +1742,31 @@ } }, "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dependencies": { "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", "@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.9", - "@babel/types": "^7.23.9", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1815,9 +1775,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -1861,13 +1821,13 @@ } }, "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.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "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" @@ -1882,20 +1842,20 @@ } }, "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": { @@ -1904,18 +1864,18 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "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", @@ -2083,9 +2043,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.56.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", - "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "version": "8.56.7", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz", + "integrity": "sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2127,9 +2087,9 @@ } }, "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.17.43", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", - "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", + "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -2206,9 +2166,9 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/mime": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.4.tgz", - "integrity": "sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==" + "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", @@ -2216,9 +2176,9 @@ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.12.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", + "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==", "dependencies": { "undici-types": "~5.26.4" } @@ -2237,9 +2197,9 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/qs": { - "version": "6.9.11", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", - "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==" + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", + "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==" }, "node_modules/@types/range-parser": { "version": "1.2.7", @@ -2260,11 +2220,6 @@ "@types/node": "*" } }, - "node_modules/@types/send/node_modules/@types/mime": { - "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/serve-index": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", @@ -2274,13 +2229,13 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", - "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "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": { @@ -2395,9 +2350,9 @@ "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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -2414,9 +2369,9 @@ "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" }, "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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.6", @@ -2434,14 +2389,14 @@ "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" }, "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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { @@ -2466,26 +2421,26 @@ "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" }, "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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@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/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -2493,22 +2448,22 @@ } }, "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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "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.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -2517,11 +2472,11 @@ } }, "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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, @@ -2569,9 +2524,9 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, "node_modules/@zip.js/zip.js": { - "version": "2.7.34", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.34.tgz", - "integrity": "sha512-SWAK+hLYKRHswhakNUirPYrdsflSFOxykUckfbWDcPvP8tjLuV5EWyd3GHV0hVaJLDps40jJnv8yQVDbWnQDfg==", + "version": "2.7.41", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.41.tgz", + "integrity": "sha512-EMxPWXlEqqvsK9jxPmNvEShrIXP2LYTdQnEfsBH6OQCnlZRVo/dJIgtzbKvtK9A8PBTDQdxwxulj+QKplmW2Xg==", "engines": { "bun": ">=0.7.0", "deno": ">=1.0.0", @@ -2763,14 +2718,13 @@ } }, "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": { @@ -2807,9 +2761,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.17", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", "funding": [ { "type": "opencollective", @@ -2825,8 +2779,8 @@ } ], "dependencies": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -2843,12 +2797,12 @@ } }, "node_modules/axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", "dev": true, "dependencies": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -2877,12 +2831,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", - "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz", + "integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.5.0", + "@babel/helper-define-polyfill-provider": "^0.6.1", "semver": "^6.3.1" }, "peerDependencies": { @@ -2898,23 +2852,23 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", - "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0", - "core-js-compat": "^3.34.0" + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", - "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz", + "integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0" + "@babel/helper-define-polyfill-provider": "^0.6.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -2968,11 +2922,14 @@ "integrity": "sha512-IZmRDr7ZSJLDtDvOP/dfqvJhMBqV/tGQAl3UgvkaCzePIkHYlYV/uCg9349E4RPF3ng7QM+eqFZ2CY7tE6pLMA==" }, "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": { @@ -3065,12 +3022,12 @@ "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.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "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", @@ -3078,7 +3035,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -3237,35 +3194,23 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", - "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "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.2.1", "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.4", + "elliptic": "^6.5.5", + "hash-base": "~3.0", "inherits": "^2.0.4", - "parse-asn1": "^5.1.6", - "readable-stream": "^3.6.2", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 4" - } - }, - "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" - }, - "engines": { - "node": ">= 6" + "node": ">= 0.12" } }, "node_modules/browserify-zlib": { @@ -3400,9 +3345,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001588", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz", - "integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==", + "version": "1.0.30001606", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz", + "integrity": "sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg==", "funding": [ { "type": "opencollective", @@ -3564,9 +3509,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.4", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.4.tgz", + "integrity": "sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==", "dependencies": { "string-width": "^4.2.0" }, @@ -3806,9 +3751,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.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -3819,9 +3764,9 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "node_modules/core-js": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz", - "integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==", + "version": "3.36.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz", + "integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -3829,11 +3774,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", - "integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==", + "version": "3.36.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz", + "integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==", "dependencies": { - "browserslist": "^4.22.3" + "browserslist": "^4.23.0" }, "funding": { "type": "opencollective", @@ -3977,23 +3922,23 @@ } }, "node_modules/css-loader": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", - "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.0.0.tgz", + "integrity": "sha512-WrO4FVoamxt5zY9CauZjoJgXRi/LZKIk+Ta7YvpSGr5r/eMYPNp5/T9ODlMe4/1rF5DYlycG1avhV4g3A/tiAw==", "dev": true, "peer": true, "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.4", - "postcss-modules-scope": "^3.1.1", + "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.5.4" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -4001,7 +3946,7 @@ }, "peerDependencies": { "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" + "webpack": "^5.27.0" }, "peerDependenciesMeta": { "@rspack/core": { @@ -4519,14 +4464,14 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.673", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.673.tgz", - "integrity": "sha512-zjqzx4N7xGdl5468G+vcgzDhaHkaYgVcf9MqgexcTqsl2UHSCmOj/Bi3HAprg4BZCpC7HyD8a6nZl6QAZf72gw==" + "version": "1.4.729", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz", + "integrity": "sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==" }, "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.5.5", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.5.tgz", + "integrity": "sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -4564,9 +4509,9 @@ } }, "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.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -4584,9 +4529,9 @@ } }, "node_modules/envinfo": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", - "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.12.0.tgz", + "integrity": "sha512-Iw9rQJBGpJRd3rwXm9ft/JiGoAZmLxxJZELYDQoPRZ4USVhkKtIcNBPw6U+/K2mBpaqM25JSV6Yl4Az9vO2wJg==", "bin": { "envinfo": "dist/cli.js" }, @@ -4627,9 +4572,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", + "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==" }, "node_modules/es6-object-assign": { "version": "1.1.0", @@ -4816,16 +4761,16 @@ } }, "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.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -5073,9 +5018,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -5343,9 +5288,9 @@ } }, "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==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -5365,31 +5310,17 @@ } }, "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.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==", "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.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_modules/hash-sum": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", @@ -5405,9 +5336,9 @@ } }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "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" }, @@ -5424,9 +5355,9 @@ } }, "node_modules/hls.js": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.6.tgz", - "integrity": "sha512-rmlaIEfLuSwqRtYLeTk30ebYli5qNK2urdkEcqYoBezRpV+MFHhZnMX77lHWW+EMjNlwr2sx2apfqq54E3yXnA==" + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.13.tgz", + "integrity": "sha512-xRgKo84nsC7clEvSfIdgn/Tc0NOT+d7vdiL/wvkLO+0k0juc26NRBPPG1SfB8pd5bHXIjMW/F5VM8VYYkOYYdw==" }, "node_modules/hmac-drbg": { "version": "1.0.1", @@ -5455,9 +5386,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", @@ -6146,9 +6077,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.16.0", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.16.0.tgz", + "integrity": "sha512-BJGUa4tcKvYmTkzTmcBGMHiO2tq+k7Do5wPmLbRswWfzKwyfZEUR+J5iwBTPEfLLwNPZlA9Kjo6R/NV6pmyIpg==", "dev": true, "engines": { "node": ">=10" @@ -7132,15 +7063,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": { @@ -7298,9 +7233,9 @@ } }, "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -7318,7 +7253,7 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -7584,9 +7519,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" }, @@ -7595,9 +7530,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", - "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^6.0.2", @@ -7611,9 +7546,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", - "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", "dependencies": { "postcss-selector-parser": "^6.0.4" }, @@ -7808,9 +7743,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", - "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -7967,11 +7902,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.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -8043,9 +7978,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", @@ -8399,9 +8334,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.71.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.0.tgz", - "integrity": "sha512-HKKIKf49Vkxlrav3F/w6qRuPcmImGVbIXJ2I3Kg0VMA+3Bav+8yE9G5XmP5lMj6nl4OlqbPftGAscNaNu28b8w==", + "version": "1.74.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.74.1.tgz", + "integrity": "sha512-w0Z9p/rWZWelb88ISOLyvqTWGmtmu2QJICqDBGyNnfG4OUnPX9BBjjYIXUpXCMOOg5MQWNpqzt876la1fsTvUA==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -8630,16 +8565,16 @@ } }, "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "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.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8713,11 +8648,11 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, "node_modules/side-channel": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" @@ -8908,9 +8843,9 @@ } }, "node_modules/socks": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.3.tgz", - "integrity": "sha512-vfuYK48HXCTFD03G/1/zkIls3Ebr2YNa4qU9gHDZdblHLiqhJrJGkY3+0Nx0JpN9qBhJbVObc1CNciT1bIZJxw==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz", + "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==", "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -8934,9 +8869,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.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -9201,9 +9136,9 @@ } }, "node_modules/terser": { - "version": "5.27.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.1.tgz", - "integrity": "sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==", + "version": "5.30.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz", + "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -9795,9 +9730,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.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -9820,25 +9755,25 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.90.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.2.tgz", - "integrity": "sha512-ziXu8ABGr0InCMEYFnHrYweinHK2PWrMqnwdHk2oK3rRhv/1B+2FnfwYv5oD+RrknK/Pp/Hmyvu+eAsaMYhzCw==", + "version": "5.91.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", + "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.16.0", "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", @@ -9846,7 +9781,7 @@ "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.0", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -9966,9 +9901,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", @@ -10037,9 +9972,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", @@ -10069,7 +10004,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": { diff --git a/package.json b/package.json index 8ecb08ae7..0fced8c08 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,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", 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/spa.css b/public/css/spa.css index fd4124d27..b1a22b331 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 1acfa8245..94c3a597c 100644 Binary files a/public/js/account-import.js and b/public/js/account-import.js differ diff --git a/public/js/admin.js b/public/js/admin.js index 4c442370f..90d4a9b64 100644 Binary files a/public/js/admin.js and b/public/js/admin.js differ diff --git a/public/js/app.js b/public/js/app.js index 943809518..2e9fb9439 100644 Binary files a/public/js/app.js and b/public/js/app.js differ diff --git a/public/js/changelog.bundle.8b5073534a5f04a4.js b/public/js/changelog.bundle.8b5073534a5f04a4.js new file mode 100644 index 000000000..3bd469ea0 Binary files /dev/null and b/public/js/changelog.bundle.8b5073534a5f04a4.js differ diff --git a/public/js/changelog.bundle.bf44edbbfa14bd53.js b/public/js/changelog.bundle.bf44edbbfa14bd53.js deleted file mode 100644 index 455a31a10..000000000 Binary files a/public/js/changelog.bundle.bf44edbbfa14bd53.js and /dev/null differ diff --git a/public/js/compose.chunk.85fe3af81758fb89.js b/public/js/compose.chunk.85fe3af81758fb89.js new file mode 100644 index 000000000..35f99d046 Binary files /dev/null and b/public/js/compose.chunk.85fe3af81758fb89.js differ diff --git a/public/js/compose.chunk.a0cfdf07f5062445.js b/public/js/compose.chunk.a0cfdf07f5062445.js deleted file mode 100644 index e3069dc9f..000000000 Binary files a/public/js/compose.chunk.a0cfdf07f5062445.js and /dev/null differ diff --git a/public/js/daci.chunk.34dc7bad3a0792cc.js b/public/js/daci.chunk.34dc7bad3a0792cc.js deleted file mode 100644 index 22489abf0..000000000 Binary files a/public/js/daci.chunk.34dc7bad3a0792cc.js and /dev/null differ diff --git a/public/js/daci.chunk.b0b7e6318a10b31f.js b/public/js/daci.chunk.b0b7e6318a10b31f.js new file mode 100644 index 000000000..53aa7d934 Binary files /dev/null and b/public/js/daci.chunk.b0b7e6318a10b31f.js differ diff --git a/public/js/discover.chunk.249a98cf3d09bf6b.js b/public/js/discover.chunk.249a98cf3d09bf6b.js new file mode 100644 index 000000000..9083e34bb Binary files /dev/null and b/public/js/discover.chunk.249a98cf3d09bf6b.js differ diff --git a/public/js/discover.chunk.c2229e1d15bd3ada.js b/public/js/discover.chunk.c2229e1d15bd3ada.js deleted file mode 100644 index be3e46280..000000000 Binary files a/public/js/discover.chunk.c2229e1d15bd3ada.js and /dev/null differ diff --git a/public/js/discover~findfriends.chunk.4cef4cef03ddc4b0.js b/public/js/discover~findfriends.chunk.4cef4cef03ddc4b0.js new file mode 100644 index 000000000..f927a637e Binary files /dev/null and b/public/js/discover~findfriends.chunk.4cef4cef03ddc4b0.js differ diff --git a/public/js/discover~findfriends.chunk.b1858bea66d9723b.js b/public/js/discover~findfriends.chunk.b1858bea66d9723b.js deleted file mode 100644 index e5774d45e..000000000 Binary files a/public/js/discover~findfriends.chunk.b1858bea66d9723b.js and /dev/null differ diff --git a/public/js/discover~hashtag.bundle.a0f00fc7df1f313c.js b/public/js/discover~hashtag.bundle.a0f00fc7df1f313c.js deleted file mode 100644 index d49e228c0..000000000 Binary files a/public/js/discover~hashtag.bundle.a0f00fc7df1f313c.js and /dev/null differ diff --git a/public/js/discover~hashtag.bundle.bf7ca8af8eb870e8.js b/public/js/discover~hashtag.bundle.bf7ca8af8eb870e8.js new file mode 100644 index 000000000..b453b258a Binary files /dev/null and b/public/js/discover~hashtag.bundle.bf7ca8af8eb870e8.js differ diff --git a/public/js/discover~memories.chunk.0c7a7f02f0d3f440.js b/public/js/discover~memories.chunk.0c7a7f02f0d3f440.js new file mode 100644 index 000000000..0e6007c7b Binary files /dev/null and b/public/js/discover~memories.chunk.0c7a7f02f0d3f440.js differ diff --git a/public/js/discover~memories.chunk.37e0c325f900e163.js b/public/js/discover~memories.chunk.37e0c325f900e163.js deleted file mode 100644 index b4c14378c..000000000 Binary files a/public/js/discover~memories.chunk.37e0c325f900e163.js and /dev/null differ diff --git a/public/js/discover~myhashtags.chunk.8886fc0d4736d819.js b/public/js/discover~myhashtags.chunk.8886fc0d4736d819.js deleted file mode 100644 index d2f1999c3..000000000 Binary files a/public/js/discover~myhashtags.chunk.8886fc0d4736d819.js and /dev/null differ diff --git a/public/js/discover~myhashtags.chunk.8f27f5212a72d5aa.js b/public/js/discover~myhashtags.chunk.8f27f5212a72d5aa.js new file mode 100644 index 000000000..41eb3850a Binary files /dev/null and b/public/js/discover~myhashtags.chunk.8f27f5212a72d5aa.js differ diff --git a/public/js/discover~serverfeed.chunk.262bf7e3bce843c3.js b/public/js/discover~serverfeed.chunk.262bf7e3bce843c3.js deleted file mode 100644 index ae43f826b..000000000 Binary files a/public/js/discover~serverfeed.chunk.262bf7e3bce843c3.js and /dev/null differ diff --git a/public/js/discover~serverfeed.chunk.a7cf7e32b5cf22f5.js b/public/js/discover~serverfeed.chunk.a7cf7e32b5cf22f5.js new file mode 100644 index 000000000..898f8c652 Binary files /dev/null and b/public/js/discover~serverfeed.chunk.a7cf7e32b5cf22f5.js differ diff --git a/public/js/discover~settings.chunk.4a39509b86a31bed.js b/public/js/discover~settings.chunk.4a39509b86a31bed.js new file mode 100644 index 000000000..e909db822 Binary files /dev/null and b/public/js/discover~settings.chunk.4a39509b86a31bed.js differ diff --git a/public/js/discover~settings.chunk.65d6f3cbe5323ed4.js b/public/js/discover~settings.chunk.65d6f3cbe5323ed4.js deleted file mode 100644 index e94440894..000000000 Binary files a/public/js/discover~settings.chunk.65d6f3cbe5323ed4.js and /dev/null differ diff --git a/public/js/dms.chunk.2b55effc0e8ba89f.js b/public/js/dms.chunk.2b55effc0e8ba89f.js deleted file mode 100644 index 6f8722c17..000000000 Binary files a/public/js/dms.chunk.2b55effc0e8ba89f.js and /dev/null differ diff --git a/public/js/dms.chunk.ee50a6d42af9b051.js b/public/js/dms.chunk.ee50a6d42af9b051.js new file mode 100644 index 000000000..a409df0c2 Binary files /dev/null and b/public/js/dms.chunk.ee50a6d42af9b051.js differ diff --git a/public/js/dms~message.chunk.976f7edaa6f71137.js b/public/js/dms~message.chunk.976f7edaa6f71137.js deleted file mode 100644 index cdfa20951..000000000 Binary files a/public/js/dms~message.chunk.976f7edaa6f71137.js and /dev/null differ diff --git a/public/js/dms~message.chunk.c3448c8fda82f05a.js b/public/js/dms~message.chunk.c3448c8fda82f05a.js new file mode 100644 index 000000000..4a9c8c330 Binary files /dev/null and b/public/js/dms~message.chunk.c3448c8fda82f05a.js differ diff --git a/public/js/error404.bundle.b397483e3991ab20.js b/public/js/error404.bundle.54601f9cdd0f7719.js similarity index 100% rename from public/js/error404.bundle.b397483e3991ab20.js rename to public/js/error404.bundle.54601f9cdd0f7719.js diff --git a/public/js/group-status.js b/public/js/group-status.js new file mode 100644 index 000000000..3f0e8cb2c 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..c4ed457fc Binary files /dev/null and b/public/js/group-topic-feed.js differ diff --git a/public/js/group.create.8aec8e805eef6c57.js b/public/js/group.create.8aec8e805eef6c57.js new file mode 100644 index 000000000..00d3dba84 Binary files /dev/null and b/public/js/group.create.8aec8e805eef6c57.js differ diff --git a/public/js/groups-page-about.150f2f899988e65c.js b/public/js/groups-page-about.150f2f899988e65c.js new file mode 100644 index 000000000..8941d3183 Binary files /dev/null and b/public/js/groups-page-about.150f2f899988e65c.js differ diff --git a/public/js/groups-page-media.a57186ce36fd8972.js b/public/js/groups-page-media.a57186ce36fd8972.js new file mode 100644 index 000000000..eab30646b Binary files /dev/null and b/public/js/groups-page-media.a57186ce36fd8972.js differ diff --git a/public/js/groups-page-members.20f9217256d06bf3.js b/public/js/groups-page-members.20f9217256d06bf3.js new file mode 100644 index 000000000..e6be9561b Binary files /dev/null and b/public/js/groups-page-members.20f9217256d06bf3.js differ diff --git a/public/js/groups-page-topics.c856bf15dc42b2fb.js b/public/js/groups-page-topics.c856bf15dc42b2fb.js new file mode 100644 index 000000000..fd2375947 Binary files /dev/null and b/public/js/groups-page-topics.c856bf15dc42b2fb.js differ diff --git a/public/js/groups-page.6c5fbadccf05f783.js b/public/js/groups-page.6c5fbadccf05f783.js new file mode 100644 index 000000000..678d0ad5c Binary files /dev/null and b/public/js/groups-page.6c5fbadccf05f783.js differ diff --git a/public/js/groups-post.c9083f5a20000208.js b/public/js/groups-post.c9083f5a20000208.js new file mode 100644 index 000000000..6db341dc2 Binary files /dev/null and b/public/js/groups-post.c9083f5a20000208.js differ diff --git a/public/js/groups-profile.9049d02d06606680.js b/public/js/groups-profile.9049d02d06606680.js new file mode 100644 index 000000000..d8e8563e7 Binary files /dev/null and b/public/js/groups-profile.9049d02d06606680.js differ diff --git a/public/js/groups.js b/public/js/groups.js new file mode 100644 index 000000000..c2a9a04b9 Binary files /dev/null and b/public/js/groups.js differ diff --git a/public/js/home.chunk.264eeb47bfac56c1.js b/public/js/home.chunk.264eeb47bfac56c1.js deleted file mode 100644 index a789de6af..000000000 Binary files a/public/js/home.chunk.264eeb47bfac56c1.js and /dev/null differ diff --git a/public/js/home.chunk.6cdfc32fcb0f1ef1.js b/public/js/home.chunk.6cdfc32fcb0f1ef1.js new file mode 100644 index 000000000..31027f9cd Binary files /dev/null and b/public/js/home.chunk.6cdfc32fcb0f1ef1.js differ diff --git a/public/js/home.chunk.264eeb47bfac56c1.js.LICENSE.txt b/public/js/home.chunk.6cdfc32fcb0f1ef1.js.LICENSE.txt similarity index 100% rename from public/js/home.chunk.264eeb47bfac56c1.js.LICENSE.txt rename to public/js/home.chunk.6cdfc32fcb0f1ef1.js.LICENSE.txt diff --git a/public/js/i18n.bundle.93a02e275ac1a708.js b/public/js/i18n.bundle.93a02e275ac1a708.js deleted file mode 100644 index 6a134edcb..000000000 Binary files a/public/js/i18n.bundle.93a02e275ac1a708.js and /dev/null differ diff --git a/public/js/i18n.bundle.eb796c04a5b36379.js b/public/js/i18n.bundle.eb796c04a5b36379.js new file mode 100644 index 000000000..564d5e4ba Binary files /dev/null and b/public/js/i18n.bundle.eb796c04a5b36379.js differ diff --git a/public/js/landing.js b/public/js/landing.js index 8c059cc7d..55cb59cc8 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 a61ead488..f8fe581f1 100644 Binary files a/public/js/manifest.js and b/public/js/manifest.js differ diff --git a/public/js/notifications.chunk.080d9f494cc6ddc3.js b/public/js/notifications.chunk.080d9f494cc6ddc3.js new file mode 100644 index 000000000..786abd110 Binary files /dev/null and b/public/js/notifications.chunk.080d9f494cc6ddc3.js differ diff --git a/public/js/notifications.chunk.0c5151643e4534aa.js b/public/js/notifications.chunk.0c5151643e4534aa.js deleted file mode 100644 index 2cae0b11d..000000000 Binary files a/public/js/notifications.chunk.0c5151643e4534aa.js and /dev/null differ diff --git a/public/js/portfolio.js b/public/js/portfolio.js index 0ed99db68..660533b6e 100644 Binary files a/public/js/portfolio.js and b/public/js/portfolio.js differ diff --git a/public/js/post.chunk.9184101a2b809af1.js b/public/js/post.chunk.9184101a2b809af1.js deleted file mode 100644 index 0b75e0bf9..000000000 Binary files a/public/js/post.chunk.9184101a2b809af1.js and /dev/null differ diff --git a/public/js/post.chunk.aa37cf5357a5233e.js b/public/js/post.chunk.aa37cf5357a5233e.js new file mode 100644 index 000000000..f30ca2307 Binary files /dev/null and b/public/js/post.chunk.aa37cf5357a5233e.js differ diff --git a/public/js/post.chunk.9184101a2b809af1.js.LICENSE.txt b/public/js/post.chunk.aa37cf5357a5233e.js.LICENSE.txt similarity index 100% rename from public/js/post.chunk.9184101a2b809af1.js.LICENSE.txt rename to public/js/post.chunk.aa37cf5357a5233e.js.LICENSE.txt diff --git a/public/js/profile.chunk.14906ecdcfc5b0b1.js b/public/js/profile.chunk.14906ecdcfc5b0b1.js new file mode 100644 index 000000000..d6f3e984e Binary files /dev/null and b/public/js/profile.chunk.14906ecdcfc5b0b1.js differ diff --git a/public/js/profile.chunk.f74967e7910990ca.js b/public/js/profile.chunk.f74967e7910990ca.js deleted file mode 100644 index 3792754dd..000000000 Binary files a/public/js/profile.chunk.f74967e7910990ca.js and /dev/null differ diff --git a/public/js/profile.js b/public/js/profile.js index 22a867555..55d85fc51 100644 Binary files a/public/js/profile.js and b/public/js/profile.js differ diff --git a/public/js/profile~followers.bundle.50a39058d98e16eb.js b/public/js/profile~followers.bundle.50a39058d98e16eb.js new file mode 100644 index 000000000..ebf4e6440 Binary files /dev/null and b/public/js/profile~followers.bundle.50a39058d98e16eb.js differ diff --git a/public/js/profile~followers.bundle.5d796e79f32d066c.js b/public/js/profile~followers.bundle.5d796e79f32d066c.js deleted file mode 100644 index 519145111..000000000 Binary files a/public/js/profile~followers.bundle.5d796e79f32d066c.js and /dev/null differ diff --git a/public/js/profile~following.bundle.7ca7cfa5aaae75e2.js b/public/js/profile~following.bundle.7ca7cfa5aaae75e2.js deleted file mode 100644 index 258c66ede..000000000 Binary files a/public/js/profile~following.bundle.7ca7cfa5aaae75e2.js and /dev/null differ diff --git a/public/js/profile~following.bundle.9294aa1b560387c7.js b/public/js/profile~following.bundle.9294aa1b560387c7.js new file mode 100644 index 000000000..8f4ee4175 Binary files /dev/null and b/public/js/profile~following.bundle.9294aa1b560387c7.js differ diff --git a/public/js/search.js b/public/js/search.js index b61629ca2..5e33f2250 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 9e8a5537f..04dc214fc 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 0d903b164..a6d1aea94 100644 Binary files a/public/js/status.js and b/public/js/status.js differ diff --git a/public/js/timeline.js b/public/js/timeline.js index 18066913f..5e30f3783 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 2cc6228ee..2f0dfd79b 100644 Binary files a/public/js/vendor.js and b/public/js/vendor.js differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 1d908832a..f9484fb51 100644 Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ diff --git a/resources/assets/components/GroupCreate.vue b/resources/assets/components/GroupCreate.vue new file mode 100644 index 000000000..26c48948b --- /dev/null +++ b/resources/assets/components/GroupCreate.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/resources/assets/components/GroupDiscover.vue b/resources/assets/components/GroupDiscover.vue new file mode 100644 index 000000000..bfdc537d3 --- /dev/null +++ b/resources/assets/components/GroupDiscover.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/resources/assets/components/GroupFeed.vue b/resources/assets/components/GroupFeed.vue new file mode 100644 index 000000000..8d752f271 --- /dev/null +++ b/resources/assets/components/GroupFeed.vue @@ -0,0 +1,315 @@ + + + + + diff --git a/resources/assets/components/GroupJoins.vue b/resources/assets/components/GroupJoins.vue new file mode 100644 index 000000000..81295f56d --- /dev/null +++ b/resources/assets/components/GroupJoins.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/resources/assets/components/GroupNotifications.vue b/resources/assets/components/GroupNotifications.vue new file mode 100644 index 000000000..69b33f355 --- /dev/null +++ b/resources/assets/components/GroupNotifications.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/resources/assets/components/GroupPage.vue b/resources/assets/components/GroupPage.vue new file mode 100644 index 000000000..ea9edfdc8 --- /dev/null +++ b/resources/assets/components/GroupPage.vue @@ -0,0 +1,1190 @@ + + + + + diff --git a/resources/assets/components/GroupPost.vue b/resources/assets/components/GroupPost.vue new file mode 100644 index 000000000..8eb5dc9d4 --- /dev/null +++ b/resources/assets/components/GroupPost.vue @@ -0,0 +1,33 @@ + + + diff --git a/resources/assets/components/GroupProfile.vue b/resources/assets/components/GroupProfile.vue new file mode 100644 index 000000000..8affdee26 --- /dev/null +++ b/resources/assets/components/GroupProfile.vue @@ -0,0 +1,443 @@ + + + + + diff --git a/resources/assets/components/GroupSearch.vue b/resources/assets/components/GroupSearch.vue new file mode 100644 index 000000000..e23a75112 --- /dev/null +++ b/resources/assets/components/GroupSearch.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/resources/assets/components/Groups.vue b/resources/assets/components/Groups.vue new file mode 100644 index 000000000..af0dc2e61 --- /dev/null +++ b/resources/assets/components/Groups.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/resources/assets/components/Hashtag.vue b/resources/assets/components/Hashtag.vue index e2ac77681..01094a85b 100644 --- a/resources/assets/components/Hashtag.vue +++ b/resources/assets/components/Hashtag.vue @@ -202,16 +202,12 @@ if(res.data && res.data.length) { this.feed = res.data; this.maxId = res.data[res.data.length - 1].id; - return true; + this.canLoadMore = true; } else { this.feedLoaded = true; this.isLoaded = true; - return false; } }) - .then(res => { - this.canLoadMore = res; - }) .finally(() => { this.feedLoaded = true; this.isLoaded = true; @@ -242,14 +238,11 @@ if(res.data && res.data.length) { this.feed.push(...res.data); this.maxId = res.data[res.data.length - 1].id; - return true; + this.canLoadMore = true; } else { - return false; + this.canLoadMore = false; } }) - .then(res => { - this.canLoadMore = res; - }) .finally(() => { this.isIntersecting = false; }) diff --git a/resources/assets/components/admin/AdminInstances.vue b/resources/assets/components/admin/AdminInstances.vue index e1c242d14..b0ecc098b 100644 --- a/resources/assets/components/admin/AdminInstances.vue +++ b/resources/assets/components/admin/AdminInstances.vue @@ -251,10 +251,11 @@
- Close + View More diff --git a/resources/assets/components/admin/AdminSettings.vue b/resources/assets/components/admin/AdminSettings.vue new file mode 100644 index 000000000..78ffd1b14 --- /dev/null +++ b/resources/assets/components/admin/AdminSettings.vue @@ -0,0 +1,1550 @@ + + + + + diff --git a/resources/assets/components/admin/partial/AdminReadMore.vue b/resources/assets/components/admin/partial/AdminReadMore.vue index 1a1b3d6ee..b5c1e47f2 100644 --- a/resources/assets/components/admin/partial/AdminReadMore.vue +++ b/resources/assets/components/admin/partial/AdminReadMore.vue @@ -1,7 +1,7 @@ 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..7459275f4 --- /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..cd6631df3 --- /dev/null +++ b/resources/assets/components/groups/partials/CommentDrawer.vue @@ -0,0 +1,841 @@ + + + + + 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..439fd03b4 --- /dev/null +++ b/resources/assets/components/groups/partials/GroupStatus.vue @@ -0,0 +1,873 @@ + + + + + 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..6663b4dfa --- /dev/null +++ b/resources/assets/components/groups/partials/SelfFeed.vue @@ -0,0 +1,146 @@ + + + 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..1a782302a --- /dev/null +++ b/resources/assets/components/groups/partials/Status/GroupHeader.vue @@ -0,0 +1,304 @@ + + + + + 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/partials/post/ContextMenu.vue b/resources/assets/components/partials/post/ContextMenu.vue index 655499d33..ad94da335 100644 --- a/resources/assets/components/partials/post/ContextMenu.vue +++ b/resources/assets/components/partials/post/ContextMenu.vue @@ -1,803 +1,1021 @@ + + diff --git a/resources/assets/components/partials/post/PostContent.vue b/resources/assets/components/partials/post/PostContent.vue index 0a88acb19..9672d52e9 100644 --- a/resources/assets/components/partials/post/PostContent.vue +++ b/resources/assets/components/partials/post/PostContent.vue @@ -169,7 +169,7 @@ diff --git a/resources/assets/components/presenter/PhotoAlbumPresenter.vue b/resources/assets/components/presenter/PhotoAlbumPresenter.vue new file mode 100644 index 000000000..3adda10df --- /dev/null +++ b/resources/assets/components/presenter/PhotoAlbumPresenter.vue @@ -0,0 +1,188 @@ + + + + + + diff --git a/resources/assets/components/presenter/PhotoPresenter.vue b/resources/assets/components/presenter/PhotoPresenter.vue new file mode 100644 index 000000000..eca896b0f --- /dev/null +++ b/resources/assets/components/presenter/PhotoPresenter.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/resources/assets/components/presenter/VideoAlbumPresenter.vue b/resources/assets/components/presenter/VideoAlbumPresenter.vue new file mode 100644 index 000000000..97285b158 --- /dev/null +++ b/resources/assets/components/presenter/VideoAlbumPresenter.vue @@ -0,0 +1,44 @@ + + + diff --git a/resources/assets/components/presenter/VideoPlayer.vue b/resources/assets/components/presenter/VideoPlayer.vue index 8f5ba0102..af1b2e71e 100644 --- a/resources/assets/components/presenter/VideoPlayer.vue +++ b/resources/assets/components/presenter/VideoPlayer.vue @@ -29,10 +29,10 @@
diff --git a/resources/assets/components/presenter/VideoPresenter.vue b/resources/assets/components/presenter/VideoPresenter.vue new file mode 100644 index 000000000..217dbb3de --- /dev/null +++ b/resources/assets/components/presenter/VideoPresenter.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/resources/assets/components/sections/Timeline.vue b/resources/assets/components/sections/Timeline.vue index ea215d895..2d23d5909 100644 --- a/resources/assets/components/sections/Timeline.vue +++ b/resources/assets/components/sections/Timeline.vue @@ -91,6 +91,8 @@ v-on:delete="deletePost" v-on:report-modal="handleReport" v-on:edit="handleEdit" + v-on:muted="handleMuted" + v-on:unfollow="handleUnfollow" /> { }) }, + + handleMuted(post) { + this.feed = this.feed.filter(p => { + return p.account.id !== post.account.id; + }); + }, + + handleUnfollow(post) { + if(this.scope === 'home') { + this.feed = this.feed.filter(p => { + return p.account.id !== post.account.id; + }); + } + this.updateProfile({ following_count: this.profile.following_count - 1 }); + }, }, watch: { diff --git a/resources/assets/js/admin.js b/resources/assets/js/admin.js index 8d2b82ca1..e5a74d8e6 100644 --- a/resources/assets/js/admin.js +++ b/resources/assets/js/admin.js @@ -36,11 +36,21 @@ Vue.component( require('./../components/admin/AdminReports.vue').default ); +Vue.component( + 'admin-settings', + require('./../components/admin/AdminSettings.vue').default +); + Vue.component( 'instances-component', require('./../components/admin/AdminInstances.vue').default ); +// Vue.component( +// 'instance-details-component', +// require('./../components/admin/AdminInstanceDetails.vue').default +// ); + Vue.component( 'hashtag-component', require('./../components/admin/AdminHashtags.vue').default diff --git a/resources/assets/js/components/GroupStatusPermalink.vue b/resources/assets/js/components/GroupStatusPermalink.vue new file mode 100644 index 000000000..14f827e99 --- /dev/null +++ b/resources/assets/js/components/GroupStatusPermalink.vue @@ -0,0 +1,25 @@ + + + diff --git a/resources/assets/js/components/presenter/VideoPresenter.vue b/resources/assets/js/components/presenter/VideoPresenter.vue index 333a482f2..217dbb3de 100644 --- a/resources/assets/js/components/presenter/VideoPresenter.vue +++ b/resources/assets/js/components/presenter/VideoPresenter.vue @@ -22,7 +22,7 @@ :alt="altText(status)"/>
-
diff --git a/resources/assets/js/group-status.js b/resources/assets/js/group-status.js new file mode 100644 index 000000000..a95ac270f --- /dev/null +++ b/resources/assets/js/group-status.js @@ -0,0 +1,4 @@ +Vue.component( + 'gs-permalink', + require('./components/GroupStatusPermalink.vue').default +); diff --git a/resources/assets/js/group-topic-feed.js b/resources/assets/js/group-topic-feed.js new file mode 100644 index 000000000..d913336bd --- /dev/null +++ b/resources/assets/js/group-topic-feed.js @@ -0,0 +1,4 @@ +Vue.component( + 'group-topic-feed', + require('./../components/groups/GroupTopicFeed.vue').default +); diff --git a/resources/assets/js/groups.js b/resources/assets/js/groups.js new file mode 100644 index 000000000..94d20cbb2 --- /dev/null +++ b/resources/assets/js/groups.js @@ -0,0 +1,29 @@ +Vue.component( + 'group-component', + require('./../components/Groups.vue').default +); + +Vue.component( + 'groups-home', + require('./../components/groups/GroupsHome.vue').default +); + +Vue.component( + 'group-feed', + require('./../components/groups/GroupFeed.vue').default +); + +Vue.component( + 'group-settings', + require('./../components/groups/GroupSettings.vue').default +); + +Vue.component( + 'group-profile', + require('./../components/groups/GroupProfile.vue').default +); + +Vue.component( + 'groups-invite', + require('./../components/groups/GroupInvite.vue').default +); diff --git a/resources/assets/js/landing.js b/resources/assets/js/landing.js index 6d903bcb5..cdaf0233b 100644 --- a/resources/assets/js/landing.js +++ b/resources/assets/js/landing.js @@ -48,27 +48,27 @@ Vue.use(VueTimeago, { Vue.component( 'photo-presenter', - require('./components/presenter/PhotoPresenter.vue').default + require('./../components/presenter/PhotoPresenter.vue').default ); Vue.component( 'video-presenter', - require('./components/presenter/VideoPresenter.vue').default + require('./../components/presenter/VideoPresenter.vue').default ); Vue.component( 'photo-album-presenter', - require('./components/presenter/PhotoAlbumPresenter.vue').default + require('./../components/presenter/PhotoAlbumPresenter.vue').default ); Vue.component( 'video-album-presenter', - require('./components/presenter/VideoAlbumPresenter.vue').default + require('./../components/presenter/VideoAlbumPresenter.vue').default ); Vue.component( 'mixed-album-presenter', - require('./components/presenter/MixedAlbumPresenter.vue').default + require('./../components/presenter/MixedAlbumPresenter.vue').default ); Vue.component( diff --git a/resources/assets/js/profile.js b/resources/assets/js/profile.js index 452445ed6..ab72c974e 100644 --- a/resources/assets/js/profile.js +++ b/resources/assets/js/profile.js @@ -1,26 +1,26 @@ Vue.component( 'photo-presenter', - require('./components/presenter/PhotoPresenter.vue').default + require('./../components/presenter/PhotoPresenter.vue').default ); Vue.component( 'video-presenter', - require('./components/presenter/VideoPresenter.vue').default + require('./../components/presenter/VideoPresenter.vue').default ); Vue.component( 'photo-album-presenter', - require('./components/presenter/PhotoAlbumPresenter.vue').default + require('./../components/presenter/PhotoAlbumPresenter.vue').default ); Vue.component( 'video-album-presenter', - require('./components/presenter/VideoAlbumPresenter.vue').default + require('./../components/presenter/VideoAlbumPresenter.vue').default ); Vue.component( 'mixed-album-presenter', - require('./components/presenter/MixedAlbumPresenter.vue').default + require('./../components/presenter/MixedAlbumPresenter.vue').default ); Vue.component( diff --git a/resources/assets/js/spa.js b/resources/assets/js/spa.js index be31b5371..c06a3793e 100644 --- a/resources/assets/js/spa.js +++ b/resources/assets/js/spa.js @@ -60,27 +60,27 @@ Vue.component( Vue.component( 'photo-presenter', - require('./components/presenter/PhotoPresenter.vue').default + require('./../components/presenter/PhotoPresenter.vue').default ); Vue.component( 'video-presenter', - require('./components/presenter/VideoPresenter.vue').default + require('./../components/presenter/VideoPresenter.vue').default ); Vue.component( 'photo-album-presenter', - require('./components/presenter/PhotoAlbumPresenter.vue').default + require('./../components/presenter/PhotoAlbumPresenter.vue').default ); Vue.component( 'video-album-presenter', - require('./components/presenter/VideoAlbumPresenter.vue').default + require('./../components/presenter/VideoAlbumPresenter.vue').default ); Vue.component( 'mixed-album-presenter', - require('./components/presenter/MixedAlbumPresenter.vue').default + require('./../components/presenter/MixedAlbumPresenter.vue').default ); Vue.component( @@ -134,7 +134,13 @@ const ChangelogComponent = () => import(/* webpackChunkName: "changelog.bundle" // import SettingsComponent from "./../components/Settings.vue"; // import ProfileComponent from "./components/ProfileNext.vue"; // import VideosComponent from "./../components/Videos.vue"; -// import GroupsComponent from "./../components/Groups.vue"; +import GroupsComponent from "./../components/Groups.vue"; +import GroupFeedComponent from "./../components/GroupFeed.vue"; +import GroupDiscoverComponent from "./../components/GroupDiscover.vue"; +import GroupJoinsComponent from "./../components/GroupJoins.vue"; +import GroupNotificationsComponent from "./../components/GroupNotifications.vue"; +import GroupSearchComponent from "./../components/GroupSearch.vue"; +const CreateGroupComponent = () => import(/* webpackChunkName: "group.create" */ "./../components/GroupCreate.vue"); const router = new VueRouter({ mode: "history", @@ -161,12 +167,78 @@ const router = new VueRouter({ // component: DriveComponent, // props: true // }, + { + path: "/groups/feed", + name: 'groups', + component: GroupFeedComponent, + }, + { + path: "/groups/joins", + name: 'groupjoins', + component: GroupJoinsComponent, + }, + { + path: "/groups/discover", + name: 'groupdiscover', + component: GroupDiscoverComponent, + props: true + }, + { + path: "/groups/notifications", + name: 'groupnotify', + component: GroupNotificationsComponent, + }, + { + path: "/groups/search", + name: 'groupsearch', + component: GroupSearchComponent, + }, + { + path: "/groups/create", + name: 'groupscreate', + component: CreateGroupComponent, + }, // { // path: "/i/web/groups", // name: 'groups', // component: GroupsComponent, // props: true // }, + { + path: "/groups/:gid/p/:sid", + component: () => import(/* webpackChunkName: "groups-post" */ './../components/GroupPost.vue'), + props: true + }, + { + path: "/groups/:gid/user/:pid", + component: () => import(/* webpackChunkName: "groups-profile" */ './../components/GroupProfile.vue'), + props: true + }, + { + path: "/groups/:groupId/about", + component: () => import(/* webpackChunkName: "groups-page-about" */ './../components/groups/Page/GroupAbout.vue'), + props: true + }, + { + path: "/groups/:groupId/topics", + component: () => import(/* webpackChunkName: "groups-page-topics" */ './../components/groups/Page/GroupTopics.vue'), + props: true + }, + { + path: "/groups/:groupId/members", + component: () => import(/* webpackChunkName: "groups-page-members" */ './../components/groups/Page/GroupMembers.vue'), + props: true + }, + { + path: "/groups/:groupId/media", + component: () => import(/* webpackChunkName: "groups-page-media" */ './../components/groups/Page/GroupMedia.vue'), + props: true + }, + { + path: "/groups/:groupId", + component: () => import(/* webpackChunkName: "groups-page" */ './../components/GroupPage.vue'), + props: true + }, { path: "/i/web/post/:id", name: 'post', @@ -178,18 +250,18 @@ const router = new VueRouter({ // component: LivePlayerComponent, // props: true // }, - { - path: "/i/web/profile/:id/followers", - name: 'profile-followers', - component: ProfileFollowersComponent, - props: true - }, - { - path: "/i/web/profile/:id/following", - name: 'profile-following', - component: ProfileFollowingComponent, - props: true - }, + { + path: "/i/web/profile/:id/followers", + name: 'profile-followers', + component: ProfileFollowersComponent, + props: true + }, + { + path: "/i/web/profile/:id/following", + name: 'profile-following', + component: ProfileFollowingComponent, + props: true + }, { path: "/i/web/profile/:id", name: 'profile', diff --git a/resources/assets/js/status.js b/resources/assets/js/status.js index ddd29a4ef..43a1a76ae 100644 --- a/resources/assets/js/status.js +++ b/resources/assets/js/status.js @@ -1,26 +1,26 @@ Vue.component( 'photo-presenter', - require('./components/presenter/PhotoPresenter.vue').default + require('./../components/presenter/PhotoPresenter.vue').default ); Vue.component( 'video-presenter', - require('./components/presenter/VideoPresenter.vue').default + require('./../components/presenter/VideoPresenter.vue').default ); Vue.component( 'photo-album-presenter', - require('./components/presenter/PhotoAlbumPresenter.vue').default + require('./../components/presenter/PhotoAlbumPresenter.vue').default ); Vue.component( 'video-album-presenter', - require('./components/presenter/VideoAlbumPresenter.vue').default + require('./../components/presenter/VideoAlbumPresenter.vue').default ); Vue.component( 'mixed-album-presenter', - require('./components/presenter/MixedAlbumPresenter.vue').default + require('./../components/presenter/MixedAlbumPresenter.vue').default ); Vue.component( @@ -32,3 +32,13 @@ Vue.component( 'post-component', require('./components/PostComponent.vue').default ); + +// Vue.component( +// 'post-next', +// require('./components/PostNext.vue').default +// ); + +// Vue.component( +// 'video-component', +// require('./components/VideoComponent.vue').default +// ); diff --git a/resources/assets/js/timeline.js b/resources/assets/js/timeline.js index 5858ac8e3..8858c3691 100644 --- a/resources/assets/js/timeline.js +++ b/resources/assets/js/timeline.js @@ -5,27 +5,27 @@ Vue.component( Vue.component( 'photo-presenter', - require('./components/presenter/PhotoPresenter.vue').default + require('./../components/presenter/PhotoPresenter.vue').default ); Vue.component( 'video-presenter', - require('./components/presenter/VideoPresenter.vue').default + require('./../components/presenter/VideoPresenter.vue').default ); Vue.component( 'photo-album-presenter', - require('./components/presenter/PhotoAlbumPresenter.vue').default + require('./../components/presenter/PhotoAlbumPresenter.vue').default ); Vue.component( 'video-album-presenter', - require('./components/presenter/VideoAlbumPresenter.vue').default + require('./../components/presenter/VideoAlbumPresenter.vue').default ); Vue.component( 'mixed-album-presenter', - require('./components/presenter/MixedAlbumPresenter.vue').default + require('./../components/presenter/MixedAlbumPresenter.vue').default ); Vue.component( @@ -46,4 +46,4 @@ Vue.component( Vue.component( 'story-component', require('./components/StoryTimelineComponent.vue').default -); \ No newline at end of file +); diff --git a/resources/assets/sass/lib/fontawesome.scss b/resources/assets/sass/lib/fontawesome.scss index 2a70ab85e..f41e42adc 100644 --- a/resources/assets/sass/lib/fontawesome.scss +++ b/resources/assets/sass/lib/fontawesome.scss @@ -2,6 +2,7 @@ .fas, .far, .fal, +.fad, .fab { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; @@ -187,12 +188,18 @@ readers do not read off random characters that represent icons */ .fa-500px:before { content: "\f26e"; } +.fa-abacus:before { + content: "\f640"; } + .fa-accessible-icon:before { content: "\f368"; } .fa-accusoft:before { content: "\f369"; } +.fa-acorn:before { + content: "\f6ae"; } + .fa-acquisitions-incorporated:before { content: "\f6af"; } @@ -211,21 +218,51 @@ readers do not read off random characters that represent icons */ .fa-adn:before { content: "\f170"; } -.fa-adobe:before { - content: "\f778"; } - .fa-adversal:before { content: "\f36a"; } .fa-affiliatetheme:before { content: "\f36b"; } +.fa-air-conditioner:before { + content: "\f8f4"; } + .fa-air-freshener:before { content: "\f5d0"; } +.fa-airbnb:before { + content: "\f834"; } + +.fa-alarm-clock:before { + content: "\f34e"; } + +.fa-alarm-exclamation:before { + content: "\f843"; } + +.fa-alarm-plus:before { + content: "\f844"; } + +.fa-alarm-snooze:before { + content: "\f845"; } + +.fa-album:before { + content: "\f89f"; } + +.fa-album-collection:before { + content: "\f8a0"; } + .fa-algolia:before { content: "\f36c"; } +.fa-alicorn:before { + content: "\f6b0"; } + +.fa-alien:before { + content: "\f8f5"; } + +.fa-alien-monster:before { + content: "\f8f6"; } + .fa-align-center:before { content: "\f037"; } @@ -238,6 +275,9 @@ readers do not read off random characters that represent icons */ .fa-align-right:before { content: "\f038"; } +.fa-align-slash:before { + content: "\f846"; } + .fa-alipay:before { content: "\f642"; } @@ -259,12 +299,21 @@ readers do not read off random characters that represent icons */ .fa-amilia:before { content: "\f36d"; } +.fa-amp-guitar:before { + content: "\f8a1"; } + +.fa-analytics:before { + content: "\f643"; } + .fa-anchor:before { content: "\f13d"; } .fa-android:before { content: "\f17b"; } +.fa-angel:before { + content: "\f779"; } + .fa-angellist:before { content: "\f209"; } @@ -319,6 +368,9 @@ readers do not read off random characters that represent icons */ .fa-apple-alt:before { content: "\f5d1"; } +.fa-apple-crate:before { + content: "\f6b1"; } + .fa-apple-pay:before { content: "\f415"; } @@ -340,6 +392,54 @@ readers do not read off random characters that represent icons */ .fa-arrow-alt-circle-up:before { content: "\f35b"; } +.fa-arrow-alt-down:before { + content: "\f354"; } + +.fa-arrow-alt-from-bottom:before { + content: "\f346"; } + +.fa-arrow-alt-from-left:before { + content: "\f347"; } + +.fa-arrow-alt-from-right:before { + content: "\f348"; } + +.fa-arrow-alt-from-top:before { + content: "\f349"; } + +.fa-arrow-alt-left:before { + content: "\f355"; } + +.fa-arrow-alt-right:before { + content: "\f356"; } + +.fa-arrow-alt-square-down:before { + content: "\f350"; } + +.fa-arrow-alt-square-left:before { + content: "\f351"; } + +.fa-arrow-alt-square-right:before { + content: "\f352"; } + +.fa-arrow-alt-square-up:before { + content: "\f353"; } + +.fa-arrow-alt-to-bottom:before { + content: "\f34a"; } + +.fa-arrow-alt-to-left:before { + content: "\f34b"; } + +.fa-arrow-alt-to-right:before { + content: "\f34c"; } + +.fa-arrow-alt-to-top:before { + content: "\f34d"; } + +.fa-arrow-alt-up:before { + content: "\f357"; } + .fa-arrow-circle-down:before { content: "\f0ab"; } @@ -355,15 +455,54 @@ readers do not read off random characters that represent icons */ .fa-arrow-down:before { content: "\f063"; } +.fa-arrow-from-bottom:before { + content: "\f342"; } + +.fa-arrow-from-left:before { + content: "\f343"; } + +.fa-arrow-from-right:before { + content: "\f344"; } + +.fa-arrow-from-top:before { + content: "\f345"; } + .fa-arrow-left:before { content: "\f060"; } .fa-arrow-right:before { content: "\f061"; } +.fa-arrow-square-down:before { + content: "\f339"; } + +.fa-arrow-square-left:before { + content: "\f33a"; } + +.fa-arrow-square-right:before { + content: "\f33b"; } + +.fa-arrow-square-up:before { + content: "\f33c"; } + +.fa-arrow-to-bottom:before { + content: "\f33d"; } + +.fa-arrow-to-left:before { + content: "\f33e"; } + +.fa-arrow-to-right:before { + content: "\f340"; } + +.fa-arrow-to-top:before { + content: "\f341"; } + .fa-arrow-up:before { content: "\f062"; } +.fa-arrows:before { + content: "\f047"; } + .fa-arrows-alt:before { content: "\f0b2"; } @@ -373,6 +512,12 @@ readers do not read off random characters that represent icons */ .fa-arrows-alt-v:before { content: "\f338"; } +.fa-arrows-h:before { + content: "\f07e"; } + +.fa-arrows-v:before { + content: "\f07d"; } + .fa-artstation:before { content: "\f77a"; } @@ -397,6 +542,9 @@ readers do not read off random characters that represent icons */ .fa-atom:before { content: "\f5d2"; } +.fa-atom-alt:before { + content: "\f5d3"; } + .fa-audible:before { content: "\f373"; } @@ -418,12 +566,21 @@ readers do not read off random characters that represent icons */ .fa-aws:before { content: "\f375"; } +.fa-axe:before { + content: "\f6b2"; } + +.fa-axe-battle:before { + content: "\f6b3"; } + .fa-baby:before { content: "\f77c"; } .fa-baby-carriage:before { content: "\f77d"; } +.fa-backpack:before { + content: "\f5d4"; } + .fa-backspace:before { content: "\f55a"; } @@ -433,9 +590,54 @@ readers do not read off random characters that represent icons */ .fa-bacon:before { content: "\f7e5"; } +.fa-bacteria:before { + content: "\e059"; } + +.fa-bacterium:before { + content: "\e05a"; } + +.fa-badge:before { + content: "\f335"; } + +.fa-badge-check:before { + content: "\f336"; } + +.fa-badge-dollar:before { + content: "\f645"; } + +.fa-badge-percent:before { + content: "\f646"; } + +.fa-badge-sheriff:before { + content: "\f8a2"; } + +.fa-badger-honey:before { + content: "\f6b4"; } + +.fa-bags-shopping:before { + content: "\f847"; } + +.fa-bahai:before { + content: "\f666"; } + .fa-balance-scale:before { content: "\f24e"; } +.fa-balance-scale-left:before { + content: "\f515"; } + +.fa-balance-scale-right:before { + content: "\f516"; } + +.fa-ball-pile:before { + content: "\f77e"; } + +.fa-ballot:before { + content: "\f732"; } + +.fa-ballot-check:before { + content: "\f733"; } + .fa-ban:before { content: "\f05e"; } @@ -445,21 +647,45 @@ readers do not read off random characters that represent icons */ .fa-bandcamp:before { content: "\f2d5"; } +.fa-banjo:before { + content: "\f8a3"; } + .fa-barcode:before { content: "\f02a"; } +.fa-barcode-alt:before { + content: "\f463"; } + +.fa-barcode-read:before { + content: "\f464"; } + +.fa-barcode-scan:before { + content: "\f465"; } + .fa-bars:before { content: "\f0c9"; } +.fa-baseball:before { + content: "\f432"; } + .fa-baseball-ball:before { content: "\f433"; } .fa-basketball-ball:before { content: "\f434"; } +.fa-basketball-hoop:before { + content: "\f435"; } + +.fa-bat:before { + content: "\f6b5"; } + .fa-bath:before { content: "\f2cd"; } +.fa-battery-bolt:before { + content: "\f376"; } + .fa-battery-empty:before { content: "\f244"; } @@ -472,12 +698,27 @@ readers do not read off random characters that represent icons */ .fa-battery-quarter:before { content: "\f243"; } +.fa-battery-slash:before { + content: "\f377"; } + .fa-battery-three-quarters:before { content: "\f241"; } +.fa-battle-net:before { + content: "\f835"; } + .fa-bed:before { content: "\f236"; } +.fa-bed-alt:before { + content: "\f8f7"; } + +.fa-bed-bunk:before { + content: "\f8f8"; } + +.fa-bed-empty:before { + content: "\f8f9"; } + .fa-beer:before { content: "\f0fc"; } @@ -490,9 +731,30 @@ readers do not read off random characters that represent icons */ .fa-bell:before { content: "\f0f3"; } +.fa-bell-exclamation:before { + content: "\f848"; } + +.fa-bell-on:before { + content: "\f8fa"; } + +.fa-bell-plus:before { + content: "\f849"; } + +.fa-bell-school:before { + content: "\f5d5"; } + +.fa-bell-school-slash:before { + content: "\f5d6"; } + .fa-bell-slash:before { content: "\f1f6"; } +.fa-bells:before { + content: "\f77f"; } + +.fa-betamax:before { + content: "\f8a4"; } + .fa-bezier-curve:before { content: "\f55b"; } @@ -502,6 +764,12 @@ readers do not read off random characters that represent icons */ .fa-bicycle:before { content: "\f206"; } +.fa-biking:before { + content: "\f84a"; } + +.fa-biking-mountain:before { + content: "\f84b"; } + .fa-bimobject:before { content: "\f378"; } @@ -529,6 +797,9 @@ readers do not read off random characters that represent icons */ .fa-blackberry:before { content: "\f37b"; } +.fa-blanket:before { + content: "\f498"; } + .fa-blender:before { content: "\f517"; } @@ -538,6 +809,15 @@ readers do not read off random characters that represent icons */ .fa-blind:before { content: "\f29d"; } +.fa-blinds:before { + content: "\f8fb"; } + +.fa-blinds-open:before { + content: "\f8fc"; } + +.fa-blinds-raised:before { + content: "\f8fd"; } + .fa-blog:before { content: "\f781"; } @@ -565,15 +845,24 @@ readers do not read off random characters that represent icons */ .fa-bone:before { content: "\f5d7"; } +.fa-bone-break:before { + content: "\f5d8"; } + .fa-bong:before { content: "\f55c"; } .fa-book:before { content: "\f02d"; } +.fa-book-alt:before { + content: "\f5d9"; } + .fa-book-dead:before { content: "\f6b7"; } +.fa-book-heart:before { + content: "\f499"; } + .fa-book-medical:before { content: "\f7e6"; } @@ -583,27 +872,135 @@ readers do not read off random characters that represent icons */ .fa-book-reader:before { content: "\f5da"; } +.fa-book-spells:before { + content: "\f6b8"; } + +.fa-book-user:before { + content: "\f7e7"; } + .fa-bookmark:before { content: "\f02e"; } +.fa-books:before { + content: "\f5db"; } + +.fa-books-medical:before { + content: "\f7e8"; } + +.fa-boombox:before { + content: "\f8a5"; } + +.fa-boot:before { + content: "\f782"; } + +.fa-booth-curtain:before { + content: "\f734"; } + +.fa-bootstrap:before { + content: "\f836"; } + +.fa-border-all:before { + content: "\f84c"; } + +.fa-border-bottom:before { + content: "\f84d"; } + +.fa-border-center-h:before { + content: "\f89c"; } + +.fa-border-center-v:before { + content: "\f89d"; } + +.fa-border-inner:before { + content: "\f84e"; } + +.fa-border-left:before { + content: "\f84f"; } + +.fa-border-none:before { + content: "\f850"; } + +.fa-border-outer:before { + content: "\f851"; } + +.fa-border-right:before { + content: "\f852"; } + +.fa-border-style:before { + content: "\f853"; } + +.fa-border-style-alt:before { + content: "\f854"; } + +.fa-border-top:before { + content: "\f855"; } + +.fa-bow-arrow:before { + content: "\f6b9"; } + .fa-bowling-ball:before { content: "\f436"; } +.fa-bowling-pins:before { + content: "\f437"; } + .fa-box:before { content: "\f466"; } +.fa-box-alt:before { + content: "\f49a"; } + +.fa-box-ballot:before { + content: "\f735"; } + +.fa-box-check:before { + content: "\f467"; } + +.fa-box-fragile:before { + content: "\f49b"; } + +.fa-box-full:before { + content: "\f49c"; } + +.fa-box-heart:before { + content: "\f49d"; } + .fa-box-open:before { content: "\f49e"; } +.fa-box-tissue:before { + content: "\e05b"; } + +.fa-box-up:before { + content: "\f49f"; } + +.fa-box-usd:before { + content: "\f4a0"; } + .fa-boxes:before { content: "\f468"; } +.fa-boxes-alt:before { + content: "\f4a1"; } + +.fa-boxing-glove:before { + content: "\f438"; } + +.fa-brackets:before { + content: "\f7e9"; } + +.fa-brackets-curly:before { + content: "\f7ea"; } + .fa-braille:before { content: "\f2a1"; } .fa-brain:before { content: "\f5dc"; } +.fa-bread-loaf:before { + content: "\f7eb"; } + .fa-bread-slice:before { content: "\f7ec"; } @@ -613,18 +1010,30 @@ readers do not read off random characters that represent icons */ .fa-briefcase-medical:before { content: "\f469"; } +.fa-bring-forward:before { + content: "\f856"; } + +.fa-bring-front:before { + content: "\f857"; } + .fa-broadcast-tower:before { content: "\f519"; } .fa-broom:before { content: "\f51a"; } +.fa-browser:before { + content: "\f37e"; } + .fa-brush:before { content: "\f55d"; } .fa-btc:before { content: "\f15a"; } +.fa-buffer:before { + content: "\f837"; } + .fa-bug:before { content: "\f188"; } @@ -637,27 +1046,54 @@ readers do not read off random characters that represent icons */ .fa-bullseye:before { content: "\f140"; } +.fa-bullseye-arrow:before { + content: "\f648"; } + +.fa-bullseye-pointer:before { + content: "\f649"; } + +.fa-burger-soda:before { + content: "\f858"; } + .fa-burn:before { content: "\f46a"; } .fa-buromobelexperte:before { content: "\f37f"; } +.fa-burrito:before { + content: "\f7ed"; } + .fa-bus:before { content: "\f207"; } .fa-bus-alt:before { content: "\f55e"; } +.fa-bus-school:before { + content: "\f5dd"; } + .fa-business-time:before { content: "\f64a"; } +.fa-buy-n-large:before { + content: "\f8a6"; } + .fa-buysellads:before { content: "\f20d"; } +.fa-cabinet-filing:before { + content: "\f64b"; } + +.fa-cactus:before { + content: "\f8a7"; } + .fa-calculator:before { content: "\f1ec"; } +.fa-calculator-alt:before { + content: "\f64c"; } + .fa-calendar:before { content: "\f133"; } @@ -670,33 +1106,66 @@ readers do not read off random characters that represent icons */ .fa-calendar-day:before { content: "\f783"; } +.fa-calendar-edit:before { + content: "\f333"; } + +.fa-calendar-exclamation:before { + content: "\f334"; } + .fa-calendar-minus:before { content: "\f272"; } .fa-calendar-plus:before { content: "\f271"; } +.fa-calendar-star:before { + content: "\f736"; } + .fa-calendar-times:before { content: "\f273"; } .fa-calendar-week:before { content: "\f784"; } +.fa-camcorder:before { + content: "\f8a8"; } + .fa-camera:before { content: "\f030"; } +.fa-camera-alt:before { + content: "\f332"; } + +.fa-camera-home:before { + content: "\f8fe"; } + +.fa-camera-movie:before { + content: "\f8a9"; } + +.fa-camera-polaroid:before { + content: "\f8aa"; } + .fa-camera-retro:before { content: "\f083"; } +.fa-campfire:before { + content: "\f6ba"; } + .fa-campground:before { content: "\f6bb"; } .fa-canadian-maple-leaf:before { content: "\f785"; } +.fa-candle-holder:before { + content: "\f6bc"; } + .fa-candy-cane:before { content: "\f786"; } +.fa-candy-corn:before { + content: "\f6bd"; } + .fa-cannabis:before { content: "\f55f"; } @@ -712,12 +1181,51 @@ readers do not read off random characters that represent icons */ .fa-car-battery:before { content: "\f5df"; } +.fa-car-building:before { + content: "\f859"; } + +.fa-car-bump:before { + content: "\f5e0"; } + +.fa-car-bus:before { + content: "\f85a"; } + .fa-car-crash:before { content: "\f5e1"; } +.fa-car-garage:before { + content: "\f5e2"; } + +.fa-car-mechanic:before { + content: "\f5e3"; } + .fa-car-side:before { content: "\f5e4"; } +.fa-car-tilt:before { + content: "\f5e5"; } + +.fa-car-wash:before { + content: "\f5e6"; } + +.fa-caravan:before { + content: "\f8ff"; } + +.fa-caravan-alt:before { + content: "\e000"; } + +.fa-caret-circle-down:before { + content: "\f32d"; } + +.fa-caret-circle-left:before { + content: "\f32e"; } + +.fa-caret-circle-right:before { + content: "\f330"; } + +.fa-caret-circle-up:before { + content: "\f331"; } + .fa-caret-down:before { content: "\f0d7"; } @@ -745,6 +1253,9 @@ readers do not read off random characters that represent icons */ .fa-carrot:before { content: "\f787"; } +.fa-cars:before { + content: "\f85b"; } + .fa-cart-arrow-down:before { content: "\f218"; } @@ -754,9 +1265,18 @@ readers do not read off random characters that represent icons */ .fa-cash-register:before { content: "\f788"; } +.fa-cassette-tape:before { + content: "\f8ab"; } + .fa-cat:before { content: "\f6be"; } +.fa-cat-space:before { + content: "\e001"; } + +.fa-cauldron:before { + content: "\f6bf"; } + .fa-cc-amazon-pay:before { content: "\f42d"; } @@ -787,6 +1307,9 @@ readers do not read off random characters that represent icons */ .fa-cc-visa:before { content: "\f1f0"; } +.fa-cctv:before { + content: "\f8ac"; } + .fa-centercode:before { content: "\f380"; } @@ -799,6 +1322,9 @@ readers do not read off random characters that represent icons */ .fa-chair:before { content: "\f6c0"; } +.fa-chair-office:before { + content: "\f6c1"; } + .fa-chalkboard:before { content: "\f51b"; } @@ -817,9 +1343,21 @@ readers do not read off random characters that represent icons */ .fa-chart-line:before { content: "\f201"; } +.fa-chart-line-down:before { + content: "\f64d"; } + +.fa-chart-network:before { + content: "\f78a"; } + .fa-chart-pie:before { content: "\f200"; } +.fa-chart-pie-alt:before { + content: "\f64e"; } + +.fa-chart-scatter:before { + content: "\f7ee"; } + .fa-check:before { content: "\f00c"; } @@ -835,30 +1373,60 @@ readers do not read off random characters that represent icons */ .fa-cheese:before { content: "\f7ef"; } +.fa-cheese-swiss:before { + content: "\f7f0"; } + +.fa-cheeseburger:before { + content: "\f7f1"; } + .fa-chess:before { content: "\f439"; } .fa-chess-bishop:before { content: "\f43a"; } +.fa-chess-bishop-alt:before { + content: "\f43b"; } + .fa-chess-board:before { content: "\f43c"; } +.fa-chess-clock:before { + content: "\f43d"; } + +.fa-chess-clock-alt:before { + content: "\f43e"; } + .fa-chess-king:before { content: "\f43f"; } +.fa-chess-king-alt:before { + content: "\f440"; } + .fa-chess-knight:before { content: "\f441"; } +.fa-chess-knight-alt:before { + content: "\f442"; } + .fa-chess-pawn:before { content: "\f443"; } +.fa-chess-pawn-alt:before { + content: "\f444"; } + .fa-chess-queen:before { content: "\f445"; } +.fa-chess-queen-alt:before { + content: "\f446"; } + .fa-chess-rook:before { content: "\f447"; } +.fa-chess-rook-alt:before { + content: "\f448"; } + .fa-chevron-circle-down:before { content: "\f13a"; } @@ -871,6 +1439,18 @@ readers do not read off random characters that represent icons */ .fa-chevron-circle-up:before { content: "\f139"; } +.fa-chevron-double-down:before { + content: "\f322"; } + +.fa-chevron-double-left:before { + content: "\f323"; } + +.fa-chevron-double-right:before { + content: "\f324"; } + +.fa-chevron-double-up:before { + content: "\f325"; } + .fa-chevron-down:before { content: "\f078"; } @@ -880,15 +1460,33 @@ readers do not read off random characters that represent icons */ .fa-chevron-right:before { content: "\f054"; } +.fa-chevron-square-down:before { + content: "\f329"; } + +.fa-chevron-square-left:before { + content: "\f32a"; } + +.fa-chevron-square-right:before { + content: "\f32b"; } + +.fa-chevron-square-up:before { + content: "\f32c"; } + .fa-chevron-up:before { content: "\f077"; } .fa-child:before { content: "\f1ae"; } +.fa-chimney:before { + content: "\f78b"; } + .fa-chrome:before { content: "\f268"; } +.fa-chromecast:before { + content: "\f838"; } + .fa-church:before { content: "\f51d"; } @@ -901,6 +1499,12 @@ readers do not read off random characters that represent icons */ .fa-city:before { content: "\f64f"; } +.fa-clarinet:before { + content: "\f8ad"; } + +.fa-claw-marks:before { + content: "\f6c2"; } + .fa-clinic-medical:before { content: "\f7f2"; } @@ -913,6 +1517,15 @@ readers do not read off random characters that represent icons */ .fa-clipboard-list:before { content: "\f46d"; } +.fa-clipboard-list-check:before { + content: "\f737"; } + +.fa-clipboard-prescription:before { + content: "\f5e8"; } + +.fa-clipboard-user:before { + content: "\f7f3"; } + .fa-clock:before { content: "\f017"; } @@ -925,9 +1538,21 @@ readers do not read off random characters that represent icons */ .fa-cloud:before { content: "\f0c2"; } +.fa-cloud-download:before { + content: "\f0ed"; } + .fa-cloud-download-alt:before { content: "\f381"; } +.fa-cloud-drizzle:before { + content: "\f738"; } + +.fa-cloud-hail:before { + content: "\f739"; } + +.fa-cloud-hail-mixed:before { + content: "\f73a"; } + .fa-cloud-meatball:before { content: "\f73b"; } @@ -937,21 +1562,51 @@ readers do not read off random characters that represent icons */ .fa-cloud-moon-rain:before { content: "\f73c"; } +.fa-cloud-music:before { + content: "\f8ae"; } + .fa-cloud-rain:before { content: "\f73d"; } +.fa-cloud-rainbow:before { + content: "\f73e"; } + +.fa-cloud-showers:before { + content: "\f73f"; } + .fa-cloud-showers-heavy:before { content: "\f740"; } +.fa-cloud-sleet:before { + content: "\f741"; } + +.fa-cloud-snow:before { + content: "\f742"; } + .fa-cloud-sun:before { content: "\f6c4"; } .fa-cloud-sun-rain:before { content: "\f743"; } +.fa-cloud-upload:before { + content: "\f0ee"; } + .fa-cloud-upload-alt:before { content: "\f382"; } +.fa-cloudflare:before { + content: "\e07d"; } + +.fa-clouds:before { + content: "\f744"; } + +.fa-clouds-moon:before { + content: "\f745"; } + +.fa-clouds-sun:before { + content: "\f746"; } + .fa-cloudscale:before { content: "\f383"; } @@ -961,6 +1616,9 @@ readers do not read off random characters that represent icons */ .fa-cloudversify:before { content: "\f385"; } +.fa-club:before { + content: "\f327"; } + .fa-cocktail:before { content: "\f561"; } @@ -970,6 +1628,12 @@ readers do not read off random characters that represent icons */ .fa-code-branch:before { content: "\f126"; } +.fa-code-commit:before { + content: "\f386"; } + +.fa-code-merge:before { + content: "\f387"; } + .fa-codepen:before { content: "\f1cb"; } @@ -979,39 +1643,129 @@ readers do not read off random characters that represent icons */ .fa-coffee:before { content: "\f0f4"; } +.fa-coffee-pot:before { + content: "\e002"; } + +.fa-coffee-togo:before { + content: "\f6c5"; } + +.fa-coffin:before { + content: "\f6c6"; } + +.fa-coffin-cross:before { + content: "\e051"; } + .fa-cog:before { content: "\f013"; } .fa-cogs:before { content: "\f085"; } +.fa-coin:before { + content: "\f85c"; } + .fa-coins:before { content: "\f51e"; } .fa-columns:before { content: "\f0db"; } +.fa-comet:before { + content: "\e003"; } + .fa-comment:before { content: "\f075"; } .fa-comment-alt:before { content: "\f27a"; } +.fa-comment-alt-check:before { + content: "\f4a2"; } + +.fa-comment-alt-dollar:before { + content: "\f650"; } + +.fa-comment-alt-dots:before { + content: "\f4a3"; } + +.fa-comment-alt-edit:before { + content: "\f4a4"; } + +.fa-comment-alt-exclamation:before { + content: "\f4a5"; } + +.fa-comment-alt-lines:before { + content: "\f4a6"; } + +.fa-comment-alt-medical:before { + content: "\f7f4"; } + +.fa-comment-alt-minus:before { + content: "\f4a7"; } + +.fa-comment-alt-music:before { + content: "\f8af"; } + +.fa-comment-alt-plus:before { + content: "\f4a8"; } + +.fa-comment-alt-slash:before { + content: "\f4a9"; } + +.fa-comment-alt-smile:before { + content: "\f4aa"; } + +.fa-comment-alt-times:before { + content: "\f4ab"; } + +.fa-comment-check:before { + content: "\f4ac"; } + .fa-comment-dollar:before { content: "\f651"; } .fa-comment-dots:before { content: "\f4ad"; } +.fa-comment-edit:before { + content: "\f4ae"; } + +.fa-comment-exclamation:before { + content: "\f4af"; } + +.fa-comment-lines:before { + content: "\f4b0"; } + .fa-comment-medical:before { content: "\f7f5"; } +.fa-comment-minus:before { + content: "\f4b1"; } + +.fa-comment-music:before { + content: "\f8b0"; } + +.fa-comment-plus:before { + content: "\f4b2"; } + .fa-comment-slash:before { content: "\f4b3"; } +.fa-comment-smile:before { + content: "\f4b4"; } + +.fa-comment-times:before { + content: "\f4b5"; } + .fa-comments:before { content: "\f086"; } +.fa-comments-alt:before { + content: "\f4b6"; } + +.fa-comments-alt-dollar:before { + content: "\f652"; } + .fa-comments-dollar:before { content: "\f653"; } @@ -1021,12 +1775,27 @@ readers do not read off random characters that represent icons */ .fa-compass:before { content: "\f14e"; } +.fa-compass-slash:before { + content: "\f5e9"; } + .fa-compress:before { content: "\f066"; } +.fa-compress-alt:before { + content: "\f422"; } + .fa-compress-arrows-alt:before { content: "\f78c"; } +.fa-compress-wide:before { + content: "\f326"; } + +.fa-computer-classic:before { + content: "\f8b1"; } + +.fa-computer-speaker:before { + content: "\f8b2"; } + .fa-concierge-bell:before { content: "\f562"; } @@ -1036,9 +1805,21 @@ readers do not read off random characters that represent icons */ .fa-connectdevelop:before { content: "\f20e"; } +.fa-construction:before { + content: "\f85d"; } + +.fa-container-storage:before { + content: "\f4b7"; } + .fa-contao:before { content: "\f26d"; } +.fa-conveyor-belt:before { + content: "\f46e"; } + +.fa-conveyor-belt-alt:before { + content: "\f46f"; } + .fa-cookie:before { content: "\f563"; } @@ -1051,9 +1832,24 @@ readers do not read off random characters that represent icons */ .fa-copyright:before { content: "\f1f9"; } +.fa-corn:before { + content: "\f6c7"; } + +.fa-cotton-bureau:before { + content: "\f89e"; } + .fa-couch:before { content: "\f4b8"; } +.fa-cow:before { + content: "\f6c8"; } + +.fa-cowbell:before { + content: "\f8b3"; } + +.fa-cowbell-more:before { + content: "\f8b4"; } + .fa-cpanel:before { content: "\f388"; } @@ -1102,9 +1898,21 @@ readers do not read off random characters that represent icons */ .fa-credit-card:before { content: "\f09d"; } +.fa-credit-card-blank:before { + content: "\f389"; } + +.fa-credit-card-front:before { + content: "\f38a"; } + +.fa-cricket:before { + content: "\f449"; } + .fa-critical-role:before { content: "\f6c9"; } +.fa-croissant:before { + content: "\f7f6"; } + .fa-crop:before { content: "\f125"; } @@ -1126,6 +1934,9 @@ readers do not read off random characters that represent icons */ .fa-crutch:before { content: "\f7f7"; } +.fa-crutches:before { + content: "\f7f8"; } + .fa-css3:before { content: "\f13c"; } @@ -1138,6 +1949,9 @@ readers do not read off random characters that represent icons */ .fa-cubes:before { content: "\f1b3"; } +.fa-curling:before { + content: "\f44a"; } + .fa-cut:before { content: "\f0c4"; } @@ -1150,6 +1964,12 @@ readers do not read off random characters that represent icons */ .fa-d-and-d-beyond:before { content: "\f6ca"; } +.fa-dagger:before { + content: "\f6cb"; } + +.fa-dailymotion:before { + content: "\e052"; } + .fa-dashcube:before { content: "\f210"; } @@ -1159,6 +1979,18 @@ readers do not read off random characters that represent icons */ .fa-deaf:before { content: "\f2a4"; } +.fa-debug:before { + content: "\f7f9"; } + +.fa-deer:before { + content: "\f78e"; } + +.fa-deer-rudolph:before { + content: "\f78f"; } + +.fa-deezer:before { + content: "\e077"; } + .fa-delicious:before { content: "\f1a5"; } @@ -1174,12 +2006,18 @@ readers do not read off random characters that represent icons */ .fa-desktop:before { content: "\f108"; } +.fa-desktop-alt:before { + content: "\f390"; } + .fa-dev:before { content: "\f6cc"; } .fa-deviantart:before { content: "\f1bd"; } +.fa-dewpoint:before { + content: "\f748"; } + .fa-dharmachakra:before { content: "\f655"; } @@ -1189,18 +2027,33 @@ readers do not read off random characters that represent icons */ .fa-diagnoses:before { content: "\f470"; } +.fa-diamond:before { + content: "\f219"; } + .fa-diaspora:before { content: "\f791"; } .fa-dice:before { content: "\f522"; } +.fa-dice-d10:before { + content: "\f6cd"; } + +.fa-dice-d12:before { + content: "\f6ce"; } + .fa-dice-d20:before { content: "\f6cf"; } +.fa-dice-d4:before { + content: "\f6d0"; } + .fa-dice-d6:before { content: "\f6d1"; } +.fa-dice-d8:before { + content: "\f6d2"; } + .fa-dice-five:before { content: "\f523"; } @@ -1222,21 +2075,33 @@ readers do not read off random characters that represent icons */ .fa-digg:before { content: "\f1a6"; } +.fa-digging:before { + content: "\f85e"; } + .fa-digital-ocean:before { content: "\f391"; } .fa-digital-tachograph:before { content: "\f566"; } +.fa-diploma:before { + content: "\f5ea"; } + .fa-directions:before { content: "\f5eb"; } +.fa-disc-drive:before { + content: "\f8b5"; } + .fa-discord:before { content: "\f392"; } .fa-discourse:before { content: "\f393"; } +.fa-disease:before { + content: "\f7fa"; } + .fa-divide:before { content: "\f529"; } @@ -1246,6 +2111,9 @@ readers do not read off random characters that represent icons */ .fa-dna:before { content: "\f471"; } +.fa-do-not-enter:before { + content: "\f5ec"; } + .fa-dochub:before { content: "\f394"; } @@ -1255,15 +2123,27 @@ readers do not read off random characters that represent icons */ .fa-dog:before { content: "\f6d3"; } +.fa-dog-leashed:before { + content: "\f6d4"; } + .fa-dollar-sign:before { content: "\f155"; } .fa-dolly:before { content: "\f472"; } +.fa-dolly-empty:before { + content: "\f473"; } + .fa-dolly-flatbed:before { content: "\f474"; } +.fa-dolly-flatbed-alt:before { + content: "\f475"; } + +.fa-dolly-flatbed-empty:before { + content: "\f476"; } + .fa-donate:before { content: "\f4b9"; } @@ -1291,15 +2171,30 @@ readers do not read off random characters that represent icons */ .fa-dragon:before { content: "\f6d5"; } +.fa-draw-circle:before { + content: "\f5ed"; } + .fa-draw-polygon:before { content: "\f5ee"; } +.fa-draw-square:before { + content: "\f5ef"; } + +.fa-dreidel:before { + content: "\f792"; } + .fa-dribbble:before { content: "\f17d"; } .fa-dribbble-square:before { content: "\f397"; } +.fa-drone:before { + content: "\f85f"; } + +.fa-drone-alt:before { + content: "\f860"; } + .fa-dropbox:before { content: "\f16b"; } @@ -1309,12 +2204,24 @@ readers do not read off random characters that represent icons */ .fa-drum-steelpan:before { content: "\f56a"; } +.fa-drumstick:before { + content: "\f6d6"; } + .fa-drumstick-bite:before { content: "\f6d7"; } .fa-drupal:before { content: "\f1a9"; } +.fa-dryer:before { + content: "\f861"; } + +.fa-dryer-alt:before { + content: "\f862"; } + +.fa-duck:before { + content: "\f6d8"; } + .fa-dumbbell:before { content: "\f44b"; } @@ -1330,33 +2237,60 @@ readers do not read off random characters that represent icons */ .fa-dyalog:before { content: "\f399"; } +.fa-ear:before { + content: "\f5f0"; } + +.fa-ear-muffs:before { + content: "\f795"; } + .fa-earlybirds:before { content: "\f39a"; } .fa-ebay:before { content: "\f4f4"; } +.fa-eclipse:before { + content: "\f749"; } + +.fa-eclipse-alt:before { + content: "\f74a"; } + .fa-edge:before { content: "\f282"; } +.fa-edge-legacy:before { + content: "\e078"; } + .fa-edit:before { content: "\f044"; } .fa-egg:before { content: "\f7fb"; } +.fa-egg-fried:before { + content: "\f7fc"; } + .fa-eject:before { content: "\f052"; } .fa-elementor:before { content: "\f430"; } +.fa-elephant:before { + content: "\f6da"; } + .fa-ellipsis-h:before { content: "\f141"; } +.fa-ellipsis-h-alt:before { + content: "\f39b"; } + .fa-ellipsis-v:before { content: "\f142"; } +.fa-ellipsis-v-alt:before { + content: "\f39c"; } + .fa-ello:before { content: "\f5f1"; } @@ -1366,12 +2300,21 @@ readers do not read off random characters that represent icons */ .fa-empire:before { content: "\f1d1"; } +.fa-empty-set:before { + content: "\f656"; } + +.fa-engine-warning:before { + content: "\f5f2"; } + .fa-envelope:before { content: "\f0e0"; } .fa-envelope-open:before { content: "\f2b6"; } +.fa-envelope-open-dollar:before { + content: "\f657"; } + .fa-envelope-open-text:before { content: "\f658"; } @@ -1402,6 +2345,12 @@ readers do not read off random characters that represent icons */ .fa-euro-sign:before { content: "\f153"; } +.fa-evernote:before { + content: "\f839"; } + +.fa-exchange:before { + content: "\f0ec"; } + .fa-exchange-alt:before { content: "\f362"; } @@ -1411,21 +2360,39 @@ readers do not read off random characters that represent icons */ .fa-exclamation-circle:before { content: "\f06a"; } +.fa-exclamation-square:before { + content: "\f321"; } + .fa-exclamation-triangle:before { content: "\f071"; } .fa-expand:before { content: "\f065"; } +.fa-expand-alt:before { + content: "\f424"; } + +.fa-expand-arrows:before { + content: "\f31d"; } + .fa-expand-arrows-alt:before { content: "\f31e"; } +.fa-expand-wide:before { + content: "\f320"; } + .fa-expeditedssl:before { content: "\f23e"; } +.fa-external-link:before { + content: "\f08e"; } + .fa-external-link-alt:before { content: "\f35d"; } +.fa-external-link-square:before { + content: "\f14c"; } + .fa-external-link-square-alt:before { content: "\f360"; } @@ -1435,6 +2402,9 @@ readers do not read off random characters that represent icons */ .fa-eye-dropper:before { content: "\f1fb"; } +.fa-eye-evil:before { + content: "\f6db"; } + .fa-eye-slash:before { content: "\f070"; } @@ -1450,15 +2420,30 @@ readers do not read off random characters that represent icons */ .fa-facebook-square:before { content: "\f082"; } +.fa-fan:before { + content: "\f863"; } + +.fa-fan-table:before { + content: "\e004"; } + .fa-fantasy-flight-games:before { content: "\f6dc"; } +.fa-farm:before { + content: "\f864"; } + .fa-fast-backward:before { content: "\f049"; } .fa-fast-forward:before { content: "\f050"; } +.fa-faucet:before { + content: "\e005"; } + +.fa-faucet-drip:before { + content: "\e006"; } + .fa-fax:before { content: "\f1ac"; } @@ -1477,6 +2462,9 @@ readers do not read off random characters that represent icons */ .fa-female:before { content: "\f182"; } +.fa-field-hockey:before { + content: "\f44c"; } + .fa-fighter-jet:before { content: "\f0fb"; } @@ -1495,6 +2483,18 @@ readers do not read off random characters that represent icons */ .fa-file-audio:before { content: "\f1c7"; } +.fa-file-certificate:before { + content: "\f5f3"; } + +.fa-file-chart-line:before { + content: "\f659"; } + +.fa-file-chart-pie:before { + content: "\f65a"; } + +.fa-file-check:before { + content: "\f316"; } + .fa-file-code:before { content: "\f1c9"; } @@ -1507,9 +2507,15 @@ readers do not read off random characters that represent icons */ .fa-file-download:before { content: "\f56d"; } +.fa-file-edit:before { + content: "\f31c"; } + .fa-file-excel:before { content: "\f1c3"; } +.fa-file-exclamation:before { + content: "\f31a"; } + .fa-file-export:before { content: "\f56e"; } @@ -1531,27 +2537,51 @@ readers do not read off random characters that represent icons */ .fa-file-medical-alt:before { content: "\f478"; } +.fa-file-minus:before { + content: "\f318"; } + +.fa-file-music:before { + content: "\f8b6"; } + .fa-file-pdf:before { content: "\f1c1"; } +.fa-file-plus:before { + content: "\f319"; } + .fa-file-powerpoint:before { content: "\f1c4"; } .fa-file-prescription:before { content: "\f572"; } +.fa-file-search:before { + content: "\f865"; } + .fa-file-signature:before { content: "\f573"; } +.fa-file-spreadsheet:before { + content: "\f65b"; } + +.fa-file-times:before { + content: "\f317"; } + .fa-file-upload:before { content: "\f574"; } +.fa-file-user:before { + content: "\f65c"; } + .fa-file-video:before { content: "\f1c8"; } .fa-file-word:before { content: "\f1c2"; } +.fa-files-medical:before { + content: "\f7fd"; } + .fa-fill:before { content: "\f575"; } @@ -1561,6 +2591,12 @@ readers do not read off random characters that represent icons */ .fa-film:before { content: "\f008"; } +.fa-film-alt:before { + content: "\f3a0"; } + +.fa-film-canister:before { + content: "\f8b7"; } + .fa-filter:before { content: "\f0b0"; } @@ -1576,9 +2612,18 @@ readers do not read off random characters that represent icons */ .fa-fire-extinguisher:before { content: "\f134"; } +.fa-fire-smoke:before { + content: "\f74b"; } + .fa-firefox:before { content: "\f269"; } +.fa-firefox-browser:before { + content: "\e007"; } + +.fa-fireplace:before { + content: "\f79a"; } + .fa-first-aid:before { content: "\f479"; } @@ -1594,36 +2639,75 @@ readers do not read off random characters that represent icons */ .fa-fish:before { content: "\f578"; } +.fa-fish-cooked:before { + content: "\f7fe"; } + .fa-fist-raised:before { content: "\f6de"; } .fa-flag:before { content: "\f024"; } +.fa-flag-alt:before { + content: "\f74c"; } + .fa-flag-checkered:before { content: "\f11e"; } .fa-flag-usa:before { content: "\f74d"; } +.fa-flame:before { + content: "\f6df"; } + +.fa-flashlight:before { + content: "\f8b8"; } + .fa-flask:before { content: "\f0c3"; } +.fa-flask-poison:before { + content: "\f6e0"; } + +.fa-flask-potion:before { + content: "\f6e1"; } + .fa-flickr:before { content: "\f16e"; } .fa-flipboard:before { content: "\f44d"; } +.fa-flower:before { + content: "\f7ff"; } + +.fa-flower-daffodil:before { + content: "\f800"; } + +.fa-flower-tulip:before { + content: "\f801"; } + .fa-flushed:before { content: "\f579"; } +.fa-flute:before { + content: "\f8b9"; } + +.fa-flux-capacitor:before { + content: "\f8ba"; } + .fa-fly:before { content: "\f417"; } +.fa-fog:before { + content: "\f74e"; } + .fa-folder:before { content: "\f07b"; } +.fa-folder-download:before { + content: "\e053"; } + .fa-folder-minus:before { content: "\f65d"; } @@ -1633,6 +2717,18 @@ readers do not read off random characters that represent icons */ .fa-folder-plus:before { content: "\f65e"; } +.fa-folder-times:before { + content: "\f65f"; } + +.fa-folder-tree:before { + content: "\f802"; } + +.fa-folder-upload:before { + content: "\e054"; } + +.fa-folders:before { + content: "\f660"; } + .fa-font:before { content: "\f031"; } @@ -1648,6 +2744,9 @@ readers do not read off random characters that represent icons */ .fa-font-awesome-logo-full:before { content: "\f4e6"; } +.fa-font-case:before { + content: "\f866"; } + .fa-fonticons:before { content: "\f280"; } @@ -1657,6 +2756,12 @@ readers do not read off random characters that represent icons */ .fa-football-ball:before { content: "\f44e"; } +.fa-football-helmet:before { + content: "\f44f"; } + +.fa-forklift:before { + content: "\f47a"; } + .fa-fort-awesome:before { content: "\f286"; } @@ -1672,15 +2777,24 @@ readers do not read off random characters that represent icons */ .fa-foursquare:before { content: "\f180"; } +.fa-fragile:before { + content: "\f4bb"; } + .fa-free-code-camp:before { content: "\f2c5"; } .fa-freebsd:before { content: "\f3a4"; } +.fa-french-fries:before { + content: "\f803"; } + .fa-frog:before { content: "\f52e"; } +.fa-frosty-head:before { + content: "\f79b"; } + .fa-frown:before { content: "\f119"; } @@ -1690,6 +2804,9 @@ readers do not read off random characters that represent icons */ .fa-fulcrum:before { content: "\f50b"; } +.fa-function:before { + content: "\f661"; } + .fa-funnel-dollar:before { content: "\f662"; } @@ -1702,12 +2819,39 @@ readers do not read off random characters that represent icons */ .fa-galactic-senate:before { content: "\f50d"; } +.fa-galaxy:before { + content: "\e008"; } + +.fa-game-board:before { + content: "\f867"; } + +.fa-game-board-alt:before { + content: "\f868"; } + +.fa-game-console-handheld:before { + content: "\f8bb"; } + .fa-gamepad:before { content: "\f11b"; } +.fa-gamepad-alt:before { + content: "\f8bc"; } + +.fa-garage:before { + content: "\e009"; } + +.fa-garage-car:before { + content: "\e00a"; } + +.fa-garage-open:before { + content: "\e00b"; } + .fa-gas-pump:before { content: "\f52f"; } +.fa-gas-pump-slash:before { + content: "\f5f4"; } + .fa-gavel:before { content: "\f0e3"; } @@ -1732,12 +2876,21 @@ readers do not read off random characters that represent icons */ .fa-gift:before { content: "\f06b"; } +.fa-gift-card:before { + content: "\f663"; } + .fa-gifts:before { content: "\f79c"; } +.fa-gingerbread-man:before { + content: "\f79d"; } + .fa-git:before { content: "\f1d3"; } +.fa-git-alt:before { + content: "\f841"; } + .fa-git-square:before { content: "\f1d2"; } @@ -1759,9 +2912,18 @@ readers do not read off random characters that represent icons */ .fa-gitter:before { content: "\f426"; } +.fa-glass:before { + content: "\f804"; } + +.fa-glass-champagne:before { + content: "\f79e"; } + .fa-glass-cheers:before { content: "\f79f"; } +.fa-glass-citrus:before { + content: "\f869"; } + .fa-glass-martini:before { content: "\f000"; } @@ -1771,9 +2933,15 @@ readers do not read off random characters that represent icons */ .fa-glass-whiskey:before { content: "\f7a0"; } +.fa-glass-whiskey-rocks:before { + content: "\f7a1"; } + .fa-glasses:before { content: "\f530"; } +.fa-glasses-alt:before { + content: "\f5f5"; } + .fa-glide:before { content: "\f2a5"; } @@ -1795,12 +2963,21 @@ readers do not read off random characters that represent icons */ .fa-globe-europe:before { content: "\f7a2"; } +.fa-globe-snow:before { + content: "\f7a3"; } + +.fa-globe-stand:before { + content: "\f5f6"; } + .fa-gofore:before { content: "\f3a7"; } .fa-golf-ball:before { content: "\f450"; } +.fa-golf-club:before { + content: "\f451"; } + .fa-goodreads:before { content: "\f3a8"; } @@ -1813,6 +2990,9 @@ readers do not read off random characters that represent icons */ .fa-google-drive:before { content: "\f3aa"; } +.fa-google-pay:before { + content: "\e079"; } + .fa-google-play:before { content: "\f3ab"; } @@ -1834,6 +3014,9 @@ readers do not read off random characters that represent icons */ .fa-graduation-cap:before { content: "\f19d"; } +.fa-gramophone:before { + content: "\f8bd"; } + .fa-gratipay:before { content: "\f184"; } @@ -1906,15 +3089,36 @@ readers do not read off random characters that represent icons */ .fa-grunt:before { content: "\f3ad"; } +.fa-guilded:before { + content: "\e07e"; } + .fa-guitar:before { content: "\f7a6"; } +.fa-guitar-electric:before { + content: "\f8be"; } + +.fa-guitars:before { + content: "\f8bf"; } + .fa-gulp:before { content: "\f3ae"; } .fa-h-square:before { content: "\f0fd"; } +.fa-h1:before { + content: "\f313"; } + +.fa-h2:before { + content: "\f314"; } + +.fa-h3:before { + content: "\f315"; } + +.fa-h4:before { + content: "\f86a"; } + .fa-hacker-news:before { content: "\f1d4"; } @@ -1930,18 +3134,39 @@ readers do not read off random characters that represent icons */ .fa-hammer:before { content: "\f6e3"; } +.fa-hammer-war:before { + content: "\f6e4"; } + .fa-hamsa:before { content: "\f665"; } +.fa-hand-heart:before { + content: "\f4bc"; } + .fa-hand-holding:before { content: "\f4bd"; } +.fa-hand-holding-box:before { + content: "\f47b"; } + .fa-hand-holding-heart:before { content: "\f4be"; } +.fa-hand-holding-magic:before { + content: "\f6e5"; } + +.fa-hand-holding-medical:before { + content: "\e05c"; } + +.fa-hand-holding-seedling:before { + content: "\f4bf"; } + .fa-hand-holding-usd:before { content: "\f4c0"; } +.fa-hand-holding-water:before { + content: "\f4c1"; } + .fa-hand-lizard:before { content: "\f258"; } @@ -1969,24 +3194,48 @@ readers do not read off random characters that represent icons */ .fa-hand-pointer:before { content: "\f25a"; } +.fa-hand-receiving:before { + content: "\f47c"; } + .fa-hand-rock:before { content: "\f255"; } .fa-hand-scissors:before { content: "\f257"; } +.fa-hand-sparkles:before { + content: "\e05d"; } + .fa-hand-spock:before { content: "\f259"; } .fa-hands:before { content: "\f4c2"; } +.fa-hands-heart:before { + content: "\f4c3"; } + .fa-hands-helping:before { content: "\f4c4"; } +.fa-hands-usd:before { + content: "\f4c5"; } + +.fa-hands-wash:before { + content: "\e05e"; } + .fa-handshake:before { content: "\f2b5"; } +.fa-handshake-alt:before { + content: "\f4c6"; } + +.fa-handshake-alt-slash:before { + content: "\e05f"; } + +.fa-handshake-slash:before { + content: "\e060"; } + .fa-hanukiah:before { content: "\f6e6"; } @@ -1996,15 +3245,57 @@ readers do not read off random characters that represent icons */ .fa-hashtag:before { content: "\f292"; } +.fa-hat-chef:before { + content: "\f86b"; } + +.fa-hat-cowboy:before { + content: "\f8c0"; } + +.fa-hat-cowboy-side:before { + content: "\f8c1"; } + +.fa-hat-santa:before { + content: "\f7a7"; } + +.fa-hat-winter:before { + content: "\f7a8"; } + +.fa-hat-witch:before { + content: "\f6e7"; } + .fa-hat-wizard:before { content: "\f6e8"; } -.fa-haykal:before { - content: "\f666"; } - .fa-hdd:before { content: "\f0a0"; } +.fa-head-side:before { + content: "\f6e9"; } + +.fa-head-side-brain:before { + content: "\f808"; } + +.fa-head-side-cough:before { + content: "\e061"; } + +.fa-head-side-cough-slash:before { + content: "\e062"; } + +.fa-head-side-headphones:before { + content: "\f8c2"; } + +.fa-head-side-mask:before { + content: "\e063"; } + +.fa-head-side-medical:before { + content: "\f809"; } + +.fa-head-side-virus:before { + content: "\e064"; } + +.fa-head-vr:before { + content: "\f6ea"; } + .fa-heading:before { content: "\f1dc"; } @@ -2023,12 +3314,30 @@ readers do not read off random characters that represent icons */ .fa-heart-broken:before { content: "\f7a9"; } +.fa-heart-circle:before { + content: "\f4c7"; } + +.fa-heart-rate:before { + content: "\f5f8"; } + +.fa-heart-square:before { + content: "\f4c8"; } + .fa-heartbeat:before { content: "\f21e"; } +.fa-heat:before { + content: "\e00c"; } + .fa-helicopter:before { content: "\f533"; } +.fa-helmet-battle:before { + content: "\f6eb"; } + +.fa-hexagon:before { + content: "\f312"; } + .fa-highlighter:before { content: "\f591"; } @@ -2047,18 +3356,45 @@ readers do not read off random characters that represent icons */ .fa-history:before { content: "\f1da"; } +.fa-hive:before { + content: "\e07f"; } + +.fa-hockey-mask:before { + content: "\f6ee"; } + .fa-hockey-puck:before { content: "\f453"; } +.fa-hockey-sticks:before { + content: "\f454"; } + .fa-holly-berry:before { content: "\f7aa"; } .fa-home:before { content: "\f015"; } +.fa-home-alt:before { + content: "\f80a"; } + +.fa-home-heart:before { + content: "\f4c9"; } + +.fa-home-lg:before { + content: "\f80b"; } + +.fa-home-lg-alt:before { + content: "\f80c"; } + +.fa-hood-cloak:before { + content: "\f6ef"; } + .fa-hooli:before { content: "\f427"; } +.fa-horizontal-rule:before { + content: "\f86c"; } + .fa-hornbill:before { content: "\f592"; } @@ -2068,6 +3404,9 @@ readers do not read off random characters that represent icons */ .fa-horse-head:before { content: "\f7ab"; } +.fa-horse-saddle:before { + content: "\f8c3"; } + .fa-hospital:before { content: "\f0f8"; } @@ -2077,6 +3416,12 @@ readers do not read off random characters that represent icons */ .fa-hospital-symbol:before { content: "\f47e"; } +.fa-hospital-user:before { + content: "\f80d"; } + +.fa-hospitals:before { + content: "\f80e"; } + .fa-hot-tub:before { content: "\f593"; } @@ -2101,9 +3446,33 @@ readers do not read off random characters that represent icons */ .fa-hourglass-start:before { content: "\f251"; } +.fa-house:before { + content: "\e00d"; } + .fa-house-damage:before { content: "\f6f1"; } +.fa-house-day:before { + content: "\e00e"; } + +.fa-house-flood:before { + content: "\f74f"; } + +.fa-house-leave:before { + content: "\e00f"; } + +.fa-house-night:before { + content: "\e010"; } + +.fa-house-return:before { + content: "\e011"; } + +.fa-house-signal:before { + content: "\e012"; } + +.fa-house-user:before { + content: "\e065"; } + .fa-houzz:before { content: "\f27c"; } @@ -2116,15 +3485,30 @@ readers do not read off random characters that represent icons */ .fa-hubspot:before { content: "\f3b2"; } +.fa-humidity:before { + content: "\f750"; } + +.fa-hurricane:before { + content: "\f751"; } + .fa-i-cursor:before { content: "\f246"; } .fa-ice-cream:before { content: "\f810"; } +.fa-ice-skate:before { + content: "\f7ac"; } + .fa-icicles:before { content: "\f7ad"; } +.fa-icons:before { + content: "\f86d"; } + +.fa-icons-alt:before { + content: "\f86e"; } + .fa-id-badge:before { content: "\f2c1"; } @@ -2134,12 +3518,18 @@ readers do not read off random characters that represent icons */ .fa-id-card-alt:before { content: "\f47f"; } +.fa-ideal:before { + content: "\e013"; } + .fa-igloo:before { content: "\f7ae"; } .fa-image:before { content: "\f03e"; } +.fa-image-polaroid:before { + content: "\f8c4"; } + .fa-images:before { content: "\f302"; } @@ -2149,12 +3539,21 @@ readers do not read off random characters that represent icons */ .fa-inbox:before { content: "\f01c"; } +.fa-inbox-in:before { + content: "\f310"; } + +.fa-inbox-out:before { + content: "\f311"; } + .fa-indent:before { content: "\f03c"; } .fa-industry:before { content: "\f275"; } +.fa-industry-alt:before { + content: "\f3b3"; } + .fa-infinity:before { content: "\f534"; } @@ -2164,30 +3563,63 @@ readers do not read off random characters that represent icons */ .fa-info-circle:before { content: "\f05a"; } +.fa-info-square:before { + content: "\f30f"; } + +.fa-inhaler:before { + content: "\f5f9"; } + +.fa-innosoft:before { + content: "\e080"; } + .fa-instagram:before { content: "\f16d"; } +.fa-instagram-square:before { + content: "\e055"; } + +.fa-instalod:before { + content: "\e081"; } + +.fa-integral:before { + content: "\f667"; } + .fa-intercom:before { content: "\f7af"; } .fa-internet-explorer:before { content: "\f26b"; } +.fa-intersection:before { + content: "\f668"; } + +.fa-inventory:before { + content: "\f480"; } + .fa-invision:before { content: "\f7b0"; } .fa-ioxhost:before { content: "\f208"; } +.fa-island-tropical:before { + content: "\f811"; } + .fa-italic:before { content: "\f033"; } +.fa-itch-io:before { + content: "\f83a"; } + .fa-itunes:before { content: "\f3b4"; } .fa-itunes-note:before { content: "\f3b5"; } +.fa-jack-o-lantern:before { + content: "\f30e"; } + .fa-java:before { content: "\f4e4"; } @@ -2215,6 +3647,9 @@ readers do not read off random characters that represent icons */ .fa-journal-whills:before { content: "\f66a"; } +.fa-joystick:before { + content: "\f8c5"; } + .fa-js:before { content: "\f3b8"; } @@ -2224,15 +3659,27 @@ readers do not read off random characters that represent icons */ .fa-jsfiddle:before { content: "\f1cc"; } +.fa-jug:before { + content: "\f8c6"; } + .fa-kaaba:before { content: "\f66b"; } .fa-kaggle:before { content: "\f5fa"; } +.fa-kazoo:before { + content: "\f8c7"; } + +.fa-kerning:before { + content: "\f86f"; } + .fa-key:before { content: "\f084"; } +.fa-key-skeleton:before { + content: "\f6f3"; } + .fa-keybase:before { content: "\f4f5"; } @@ -2242,6 +3689,9 @@ readers do not read off random characters that represent icons */ .fa-keycdn:before { content: "\f3ba"; } +.fa-keynote:before { + content: "\f66c"; } + .fa-khanda:before { content: "\f66d"; } @@ -2251,6 +3701,9 @@ readers do not read off random characters that represent icons */ .fa-kickstarter-k:before { content: "\f3bc"; } +.fa-kidneys:before { + content: "\f5fb"; } + .fa-kiss:before { content: "\f596"; } @@ -2260,15 +3713,36 @@ readers do not read off random characters that represent icons */ .fa-kiss-wink-heart:before { content: "\f598"; } +.fa-kite:before { + content: "\f6f4"; } + .fa-kiwi-bird:before { content: "\f535"; } +.fa-knife-kitchen:before { + content: "\f6f5"; } + .fa-korvue:before { content: "\f42f"; } +.fa-lambda:before { + content: "\f66e"; } + +.fa-lamp:before { + content: "\f4ca"; } + +.fa-lamp-desk:before { + content: "\e014"; } + +.fa-lamp-floor:before { + content: "\e015"; } + .fa-landmark:before { content: "\f66f"; } +.fa-landmark-alt:before { + content: "\f752"; } + .fa-language:before { content: "\f1ab"; } @@ -2278,12 +3752,18 @@ readers do not read off random characters that represent icons */ .fa-laptop-code:before { content: "\f5fc"; } +.fa-laptop-house:before { + content: "\e066"; } + .fa-laptop-medical:before { content: "\f812"; } .fa-laravel:before { content: "\f3bd"; } +.fa-lasso:before { + content: "\f8c8"; } + .fa-lastfm:before { content: "\f202"; } @@ -2305,9 +3785,24 @@ readers do not read off random characters that represent icons */ .fa-layer-group:before { content: "\f5fd"; } +.fa-layer-minus:before { + content: "\f5fe"; } + +.fa-layer-plus:before { + content: "\f5ff"; } + .fa-leaf:before { content: "\f06c"; } +.fa-leaf-heart:before { + content: "\f4cb"; } + +.fa-leaf-maple:before { + content: "\f6f6"; } + +.fa-leaf-oak:before { + content: "\f6f7"; } + .fa-leanpub:before { content: "\f212"; } @@ -2323,21 +3818,60 @@ readers do not read off random characters that represent icons */ .fa-less-than-equal:before { content: "\f537"; } +.fa-level-down:before { + content: "\f149"; } + .fa-level-down-alt:before { content: "\f3be"; } +.fa-level-up:before { + content: "\f148"; } + .fa-level-up-alt:before { content: "\f3bf"; } .fa-life-ring:before { content: "\f1cd"; } +.fa-light-ceiling:before { + content: "\e016"; } + +.fa-light-switch:before { + content: "\e017"; } + +.fa-light-switch-off:before { + content: "\e018"; } + +.fa-light-switch-on:before { + content: "\e019"; } + .fa-lightbulb:before { content: "\f0eb"; } +.fa-lightbulb-dollar:before { + content: "\f670"; } + +.fa-lightbulb-exclamation:before { + content: "\f671"; } + +.fa-lightbulb-on:before { + content: "\f672"; } + +.fa-lightbulb-slash:before { + content: "\f673"; } + +.fa-lights-holiday:before { + content: "\f7b2"; } + .fa-line:before { content: "\f3c0"; } +.fa-line-columns:before { + content: "\f870"; } + +.fa-line-height:before { + content: "\f871"; } + .fa-link:before { content: "\f0c1"; } @@ -2353,6 +3887,9 @@ readers do not read off random characters that represent icons */ .fa-linux:before { content: "\f17c"; } +.fa-lips:before { + content: "\f600"; } + .fa-lira-sign:before { content: "\f195"; } @@ -2362,21 +3899,39 @@ readers do not read off random characters that represent icons */ .fa-list-alt:before { content: "\f022"; } +.fa-list-music:before { + content: "\f8c9"; } + .fa-list-ol:before { content: "\f0cb"; } .fa-list-ul:before { content: "\f0ca"; } +.fa-location:before { + content: "\f601"; } + .fa-location-arrow:before { content: "\f124"; } +.fa-location-circle:before { + content: "\f602"; } + +.fa-location-slash:before { + content: "\f603"; } + .fa-lock:before { content: "\f023"; } +.fa-lock-alt:before { + content: "\f30d"; } + .fa-lock-open:before { content: "\f3c1"; } +.fa-lock-open-alt:before { + content: "\f3c2"; } + .fa-long-arrow-alt-down:before { content: "\f309"; } @@ -2389,15 +3944,42 @@ readers do not read off random characters that represent icons */ .fa-long-arrow-alt-up:before { content: "\f30c"; } +.fa-long-arrow-down:before { + content: "\f175"; } + +.fa-long-arrow-left:before { + content: "\f177"; } + +.fa-long-arrow-right:before { + content: "\f178"; } + +.fa-long-arrow-up:before { + content: "\f176"; } + +.fa-loveseat:before { + content: "\f4cc"; } + .fa-low-vision:before { content: "\f2a8"; } +.fa-luchador:before { + content: "\f455"; } + .fa-luggage-cart:before { content: "\f59d"; } +.fa-lungs:before { + content: "\f604"; } + +.fa-lungs-virus:before { + content: "\e067"; } + .fa-lyft:before { content: "\f3c3"; } +.fa-mace:before { + content: "\f6f8"; } + .fa-magento:before { content: "\f3c4"; } @@ -2410,6 +3992,9 @@ readers do not read off random characters that represent icons */ .fa-mail-bulk:before { content: "\f674"; } +.fa-mailbox:before { + content: "\f813"; } + .fa-mailchimp:before { content: "\f59e"; } @@ -2419,6 +4004,9 @@ readers do not read off random characters that represent icons */ .fa-mandalorian:before { content: "\f50f"; } +.fa-mandolin:before { + content: "\f6f9"; } + .fa-map:before { content: "\f279"; } @@ -2434,6 +4022,36 @@ readers do not read off random characters that represent icons */ .fa-map-marker-alt:before { content: "\f3c5"; } +.fa-map-marker-alt-slash:before { + content: "\f605"; } + +.fa-map-marker-check:before { + content: "\f606"; } + +.fa-map-marker-edit:before { + content: "\f607"; } + +.fa-map-marker-exclamation:before { + content: "\f608"; } + +.fa-map-marker-minus:before { + content: "\f609"; } + +.fa-map-marker-plus:before { + content: "\f60a"; } + +.fa-map-marker-question:before { + content: "\f60b"; } + +.fa-map-marker-slash:before { + content: "\f60c"; } + +.fa-map-marker-smile:before { + content: "\f60d"; } + +.fa-map-marker-times:before { + content: "\f60e"; } + .fa-map-pin:before { content: "\f276"; } @@ -2470,6 +4088,12 @@ readers do not read off random characters that represent icons */ .fa-maxcdn:before { content: "\f136"; } +.fa-mdb:before { + content: "\f8ca"; } + +.fa-meat:before { + content: "\f814"; } + .fa-medal:before { content: "\f5a2"; } @@ -2491,6 +4115,9 @@ readers do not read off random characters that represent icons */ .fa-meetup:before { content: "\f2e0"; } +.fa-megaphone:before { + content: "\f675"; } + .fa-megaport:before { content: "\f5a3"; } @@ -2518,6 +4145,9 @@ readers do not read off random characters that represent icons */ .fa-meteor:before { content: "\f753"; } +.fa-microblog:before { + content: "\e01a"; } + .fa-microchip:before { content: "\f2db"; } @@ -2533,21 +4163,39 @@ readers do not read off random characters that represent icons */ .fa-microphone-slash:before { content: "\f131"; } +.fa-microphone-stand:before { + content: "\f8cb"; } + .fa-microscope:before { content: "\f610"; } .fa-microsoft:before { content: "\f3ca"; } +.fa-microwave:before { + content: "\e01b"; } + +.fa-mind-share:before { + content: "\f677"; } + .fa-minus:before { content: "\f068"; } .fa-minus-circle:before { content: "\f056"; } +.fa-minus-hexagon:before { + content: "\f307"; } + +.fa-minus-octagon:before { + content: "\f308"; } + .fa-minus-square:before { content: "\f146"; } +.fa-mistletoe:before { + content: "\f7b4"; } + .fa-mitten:before { content: "\f7b5"; } @@ -2557,6 +4205,9 @@ readers do not read off random characters that represent icons */ .fa-mixcloud:before { content: "\f289"; } +.fa-mixer:before { + content: "\e056"; } + .fa-mizuni:before { content: "\f3cc"; } @@ -2566,6 +4217,12 @@ readers do not read off random characters that represent icons */ .fa-mobile-alt:before { content: "\f3cd"; } +.fa-mobile-android:before { + content: "\f3ce"; } + +.fa-mobile-android-alt:before { + content: "\f3cf"; } + .fa-modx:before { content: "\f285"; } @@ -2590,12 +4247,30 @@ readers do not read off random characters that represent icons */ .fa-money-check-alt:before { content: "\f53d"; } +.fa-money-check-edit:before { + content: "\f872"; } + +.fa-money-check-edit-alt:before { + content: "\f873"; } + +.fa-monitor-heart-rate:before { + content: "\f611"; } + +.fa-monkey:before { + content: "\f6fb"; } + .fa-monument:before { content: "\f5a6"; } .fa-moon:before { content: "\f186"; } +.fa-moon-cloud:before { + content: "\f754"; } + +.fa-moon-stars:before { + content: "\f755"; } + .fa-mortar-pestle:before { content: "\f5a7"; } @@ -2608,18 +4283,51 @@ readers do not read off random characters that represent icons */ .fa-mountain:before { content: "\f6fc"; } +.fa-mountains:before { + content: "\f6fd"; } + +.fa-mouse:before { + content: "\f8cc"; } + +.fa-mouse-alt:before { + content: "\f8cd"; } + .fa-mouse-pointer:before { content: "\f245"; } +.fa-mp3-player:before { + content: "\f8ce"; } + +.fa-mug:before { + content: "\f874"; } + .fa-mug-hot:before { content: "\f7b6"; } +.fa-mug-marshmallows:before { + content: "\f7b7"; } + +.fa-mug-tea:before { + content: "\f875"; } + .fa-music:before { content: "\f001"; } +.fa-music-alt:before { + content: "\f8cf"; } + +.fa-music-alt-slash:before { + content: "\f8d0"; } + +.fa-music-slash:before { + content: "\f8d1"; } + .fa-napster:before { content: "\f3d2"; } +.fa-narwhal:before { + content: "\f6fe"; } + .fa-neos:before { content: "\f612"; } @@ -2635,9 +4343,6 @@ readers do not read off random characters that represent icons */ .fa-nimblr:before { content: "\f5a8"; } -.fa-nintendo-switch:before { - content: "\f418"; } - .fa-node:before { content: "\f419"; } @@ -2665,6 +4370,12 @@ readers do not read off random characters that represent icons */ .fa-object-ungroup:before { content: "\f248"; } +.fa-octagon:before { + content: "\f306"; } + +.fa-octopus-deploy:before { + content: "\e082"; } + .fa-odnoklassniki:before { content: "\f263"; } @@ -2674,12 +4385,18 @@ readers do not read off random characters that represent icons */ .fa-oil-can:before { content: "\f613"; } +.fa-oil-temp:before { + content: "\f614"; } + .fa-old-republic:before { content: "\f510"; } .fa-om:before { content: "\f679"; } +.fa-omega:before { + content: "\f67a"; } + .fa-opencart:before { content: "\f23d"; } @@ -2692,6 +4409,12 @@ readers do not read off random characters that represent icons */ .fa-optin-monster:before { content: "\f23c"; } +.fa-orcid:before { + content: "\f8d2"; } + +.fa-ornament:before { + content: "\f7b8"; } + .fa-osi:before { content: "\f41a"; } @@ -2701,6 +4424,18 @@ readers do not read off random characters that represent icons */ .fa-outdent:before { content: "\f03b"; } +.fa-outlet:before { + content: "\e01c"; } + +.fa-oven:before { + content: "\e01d"; } + +.fa-overline:before { + content: "\f876"; } + +.fa-page-break:before { + content: "\f877"; } + .fa-page4:before { content: "\f3d7"; } @@ -2713,6 +4448,9 @@ readers do not read off random characters that represent icons */ .fa-paint-brush:before { content: "\f1fc"; } +.fa-paint-brush-alt:before { + content: "\f5a9"; } + .fa-paint-roller:before { content: "\f5aa"; } @@ -2725,6 +4463,9 @@ readers do not read off random characters that represent icons */ .fa-pallet:before { content: "\f482"; } +.fa-pallet-alt:before { + content: "\f483"; } + .fa-paper-plane:before { content: "\f1d8"; } @@ -2737,9 +4478,21 @@ readers do not read off random characters that represent icons */ .fa-paragraph:before { content: "\f1dd"; } +.fa-paragraph-rtl:before { + content: "\f878"; } + .fa-parking:before { content: "\f540"; } +.fa-parking-circle:before { + content: "\f615"; } + +.fa-parking-circle-slash:before { + content: "\f616"; } + +.fa-parking-slash:before { + content: "\f617"; } + .fa-passport:before { content: "\f5ab"; } @@ -2761,12 +4514,21 @@ readers do not read off random characters that represent icons */ .fa-paw:before { content: "\f1b0"; } +.fa-paw-alt:before { + content: "\f701"; } + +.fa-paw-claws:before { + content: "\f702"; } + .fa-paypal:before { content: "\f1ed"; } .fa-peace:before { content: "\f67c"; } +.fa-pegasus:before { + content: "\f703"; } + .fa-pen:before { content: "\f304"; } @@ -2782,21 +4544,36 @@ readers do not read off random characters that represent icons */ .fa-pen-square:before { content: "\f14b"; } +.fa-pencil:before { + content: "\f040"; } + .fa-pencil-alt:before { content: "\f303"; } +.fa-pencil-paintbrush:before { + content: "\f618"; } + .fa-pencil-ruler:before { content: "\f5ae"; } +.fa-pennant:before { + content: "\f456"; } + .fa-penny-arcade:before { content: "\f704"; } +.fa-people-arrows:before { + content: "\e068"; } + .fa-people-carry:before { content: "\f4ce"; } .fa-pepper-hot:before { content: "\f816"; } +.fa-perbyte:before { + content: "\e083"; } + .fa-percent:before { content: "\f295"; } @@ -2809,6 +4586,18 @@ readers do not read off random characters that represent icons */ .fa-person-booth:before { content: "\f756"; } +.fa-person-carry:before { + content: "\f4cf"; } + +.fa-person-dolly:before { + content: "\f4d0"; } + +.fa-person-dolly-empty:before { + content: "\f4d1"; } + +.fa-person-sign:before { + content: "\f757"; } + .fa-phabricator:before { content: "\f3db"; } @@ -2821,18 +4610,51 @@ readers do not read off random characters that represent icons */ .fa-phone:before { content: "\f095"; } +.fa-phone-alt:before { + content: "\f879"; } + +.fa-phone-laptop:before { + content: "\f87a"; } + +.fa-phone-office:before { + content: "\f67d"; } + +.fa-phone-plus:before { + content: "\f4d2"; } + +.fa-phone-rotary:before { + content: "\f8d3"; } + .fa-phone-slash:before { content: "\f3dd"; } .fa-phone-square:before { content: "\f098"; } +.fa-phone-square-alt:before { + content: "\f87b"; } + .fa-phone-volume:before { content: "\f2a0"; } +.fa-photo-video:before { + content: "\f87c"; } + .fa-php:before { content: "\f457"; } +.fa-pi:before { + content: "\f67e"; } + +.fa-piano:before { + content: "\f8d4"; } + +.fa-piano-keyboard:before { + content: "\f8d5"; } + +.fa-pie:before { + content: "\f705"; } + .fa-pied-piper:before { content: "\f2ae"; } @@ -2845,6 +4667,12 @@ readers do not read off random characters that represent icons */ .fa-pied-piper-pp:before { content: "\f1a7"; } +.fa-pied-piper-square:before { + content: "\e01e"; } + +.fa-pig:before { + content: "\f706"; } + .fa-piggy-bank:before { content: "\f4d3"; } @@ -2860,6 +4688,9 @@ readers do not read off random characters that represent icons */ .fa-pinterest-square:before { content: "\f0d3"; } +.fa-pizza:before { + content: "\f817"; } + .fa-pizza-slice:before { content: "\f818"; } @@ -2869,12 +4700,24 @@ readers do not read off random characters that represent icons */ .fa-plane:before { content: "\f072"; } +.fa-plane-alt:before { + content: "\f3de"; } + .fa-plane-arrival:before { content: "\f5af"; } .fa-plane-departure:before { content: "\f5b0"; } +.fa-plane-slash:before { + content: "\e069"; } + +.fa-planet-moon:before { + content: "\e01f"; } + +.fa-planet-ringed:before { + content: "\e020"; } + .fa-play:before { content: "\f04b"; } @@ -2893,18 +4736,36 @@ readers do not read off random characters that represent icons */ .fa-plus-circle:before { content: "\f055"; } +.fa-plus-hexagon:before { + content: "\f300"; } + +.fa-plus-octagon:before { + content: "\f301"; } + .fa-plus-square:before { content: "\f0fe"; } .fa-podcast:before { content: "\f2ce"; } +.fa-podium:before { + content: "\f680"; } + +.fa-podium-star:before { + content: "\f758"; } + +.fa-police-box:before { + content: "\e021"; } + .fa-poll:before { content: "\f681"; } .fa-poll-h:before { content: "\f682"; } +.fa-poll-people:before { + content: "\f759"; } + .fa-poo:before { content: "\f2fe"; } @@ -2914,6 +4775,15 @@ readers do not read off random characters that represent icons */ .fa-poop:before { content: "\f619"; } +.fa-popcorn:before { + content: "\f819"; } + +.fa-portal-enter:before { + content: "\e022"; } + +.fa-portal-exit:before { + content: "\e023"; } + .fa-portrait:before { content: "\f3e0"; } @@ -2938,9 +4808,18 @@ readers do not read off random characters that represent icons */ .fa-prescription-bottle-alt:before { content: "\f486"; } +.fa-presentation:before { + content: "\f685"; } + .fa-print:before { content: "\f02f"; } +.fa-print-search:before { + content: "\f81a"; } + +.fa-print-slash:before { + content: "\f686"; } + .fa-procedures:before { content: "\f487"; } @@ -2950,6 +4829,18 @@ readers do not read off random characters that represent icons */ .fa-project-diagram:before { content: "\f542"; } +.fa-projector:before { + content: "\f8d6"; } + +.fa-pump-medical:before { + content: "\e06a"; } + +.fa-pump-soap:before { + content: "\e06b"; } + +.fa-pumpkin:before { + content: "\f707"; } + .fa-pushed:before { content: "\f3e1"; } @@ -2971,6 +4862,9 @@ readers do not read off random characters that represent icons */ .fa-question-circle:before { content: "\f059"; } +.fa-question-square:before { + content: "\f2fd"; } + .fa-quidditch:before { content: "\f458"; } @@ -2992,15 +4886,42 @@ readers do not read off random characters that represent icons */ .fa-r-project:before { content: "\f4f7"; } +.fa-rabbit:before { + content: "\f708"; } + +.fa-rabbit-fast:before { + content: "\f709"; } + +.fa-racquet:before { + content: "\f45a"; } + +.fa-radar:before { + content: "\e024"; } + .fa-radiation:before { content: "\f7b9"; } .fa-radiation-alt:before { content: "\f7ba"; } +.fa-radio:before { + content: "\f8d7"; } + +.fa-radio-alt:before { + content: "\f8d8"; } + .fa-rainbow:before { content: "\f75b"; } +.fa-raindrops:before { + content: "\f75c"; } + +.fa-ram:before { + content: "\f70a"; } + +.fa-ramp-loading:before { + content: "\f4d4"; } + .fa-random:before { content: "\f074"; } @@ -3010,6 +4931,9 @@ readers do not read off random characters that represent icons */ .fa-ravelry:before { content: "\f2d9"; } +.fa-raygun:before { + content: "\e025"; } + .fa-react:before { content: "\f41b"; } @@ -3025,6 +4949,18 @@ readers do not read off random characters that represent icons */ .fa-receipt:before { content: "\f543"; } +.fa-record-vinyl:before { + content: "\f8d9"; } + +.fa-rectangle-landscape:before { + content: "\f2fa"; } + +.fa-rectangle-portrait:before { + content: "\f2fb"; } + +.fa-rectangle-wide:before { + content: "\f2fc"; } + .fa-recycle:before { content: "\f1b8"; } @@ -3049,12 +4985,30 @@ readers do not read off random characters that represent icons */ .fa-redo-alt:before { content: "\f2f9"; } +.fa-refrigerator:before { + content: "\e026"; } + .fa-registered:before { content: "\f25d"; } +.fa-remove-format:before { + content: "\f87d"; } + .fa-renren:before { content: "\f18b"; } +.fa-repeat:before { + content: "\f363"; } + +.fa-repeat-1:before { + content: "\f365"; } + +.fa-repeat-1-alt:before { + content: "\f366"; } + +.fa-repeat-alt:before { + content: "\f364"; } + .fa-reply:before { content: "\f3e5"; } @@ -3079,6 +5033,9 @@ readers do not read off random characters that represent icons */ .fa-retweet:before { content: "\f079"; } +.fa-retweet-alt:before { + content: "\f361"; } + .fa-rev:before { content: "\f5b2"; } @@ -3088,6 +5045,9 @@ readers do not read off random characters that represent icons */ .fa-ring:before { content: "\f70b"; } +.fa-rings-wedding:before { + content: "\f81b"; } + .fa-road:before { content: "\f018"; } @@ -3097,6 +5057,9 @@ readers do not read off random characters that represent icons */ .fa-rocket:before { content: "\f135"; } +.fa-rocket-launch:before { + content: "\e027"; } + .fa-rocketchat:before { content: "\f3e8"; } @@ -3106,6 +5069,15 @@ readers do not read off random characters that represent icons */ .fa-route:before { content: "\f4d7"; } +.fa-route-highway:before { + content: "\f61a"; } + +.fa-route-interstate:before { + content: "\f61b"; } + +.fa-router:before { + content: "\f8da"; } + .fa-rss:before { content: "\f09e"; } @@ -3124,6 +5096,9 @@ readers do not read off random characters that represent icons */ .fa-ruler-horizontal:before { content: "\f547"; } +.fa-ruler-triangle:before { + content: "\f61c"; } + .fa-ruler-vertical:before { content: "\f548"; } @@ -3133,6 +5108,18 @@ readers do not read off random characters that represent icons */ .fa-rupee-sign:before { content: "\f156"; } +.fa-rust:before { + content: "\e07a"; } + +.fa-rv:before { + content: "\f7be"; } + +.fa-sack:before { + content: "\f81c"; } + +.fa-sack-dollar:before { + content: "\f81d"; } + .fa-sad-cry:before { content: "\f5b3"; } @@ -3142,6 +5129,15 @@ readers do not read off random characters that represent icons */ .fa-safari:before { content: "\f267"; } +.fa-salad:before { + content: "\f81e"; } + +.fa-salesforce:before { + content: "\f83b"; } + +.fa-sandwich:before { + content: "\f81f"; } + .fa-sass:before { content: "\f41e"; } @@ -3151,9 +5147,42 @@ readers do not read off random characters that represent icons */ .fa-satellite-dish:before { content: "\f7c0"; } +.fa-sausage:before { + content: "\f820"; } + .fa-save:before { content: "\f0c7"; } +.fa-sax-hot:before { + content: "\f8db"; } + +.fa-saxophone:before { + content: "\f8dc"; } + +.fa-scalpel:before { + content: "\f61d"; } + +.fa-scalpel-path:before { + content: "\f61e"; } + +.fa-scanner:before { + content: "\f488"; } + +.fa-scanner-image:before { + content: "\f8f3"; } + +.fa-scanner-keyboard:before { + content: "\f489"; } + +.fa-scanner-touchscreen:before { + content: "\f48a"; } + +.fa-scarecrow:before { + content: "\f70d"; } + +.fa-scarf:before { + content: "\f7c1"; } + .fa-schlix:before { content: "\f3ea"; } @@ -3169,6 +5198,15 @@ readers do not read off random characters that represent icons */ .fa-scroll:before { content: "\f70e"; } +.fa-scroll-old:before { + content: "\f70f"; } + +.fa-scrubber:before { + content: "\f2f8"; } + +.fa-scythe:before { + content: "\f710"; } + .fa-sd-card:before { content: "\f7c2"; } @@ -3199,6 +5237,27 @@ readers do not read off random characters that represent icons */ .fa-sellsy:before { content: "\f213"; } +.fa-send-back:before { + content: "\f87e"; } + +.fa-send-backward:before { + content: "\f87f"; } + +.fa-sensor:before { + content: "\e028"; } + +.fa-sensor-alert:before { + content: "\e029"; } + +.fa-sensor-fire:before { + content: "\e02a"; } + +.fa-sensor-on:before { + content: "\e02b"; } + +.fa-sensor-smoke:before { + content: "\e02c"; } + .fa-server:before { content: "\f233"; } @@ -3211,6 +5270,9 @@ readers do not read off random characters that represent icons */ .fa-share:before { content: "\f064"; } +.fa-share-all:before { + content: "\f367"; } + .fa-share-alt:before { content: "\f1e0"; } @@ -3220,24 +5282,48 @@ readers do not read off random characters that represent icons */ .fa-share-square:before { content: "\f14d"; } +.fa-sheep:before { + content: "\f711"; } + .fa-shekel-sign:before { content: "\f20b"; } +.fa-shield:before { + content: "\f132"; } + .fa-shield-alt:before { content: "\f3ed"; } +.fa-shield-check:before { + content: "\f2f7"; } + +.fa-shield-cross:before { + content: "\f712"; } + +.fa-shield-virus:before { + content: "\e06c"; } + .fa-ship:before { content: "\f21a"; } .fa-shipping-fast:before { content: "\f48b"; } +.fa-shipping-timed:before { + content: "\f48c"; } + .fa-shirtsinbulk:before { content: "\f214"; } +.fa-shish-kebab:before { + content: "\f821"; } + .fa-shoe-prints:before { content: "\f54b"; } +.fa-shopify:before { + content: "\e057"; } + .fa-shopping-bag:before { content: "\f290"; } @@ -3250,27 +5336,84 @@ readers do not read off random characters that represent icons */ .fa-shopware:before { content: "\f5b5"; } +.fa-shovel:before { + content: "\f713"; } + +.fa-shovel-snow:before { + content: "\f7c3"; } + .fa-shower:before { content: "\f2cc"; } +.fa-shredder:before { + content: "\f68a"; } + .fa-shuttle-van:before { content: "\f5b6"; } +.fa-shuttlecock:before { + content: "\f45b"; } + +.fa-sickle:before { + content: "\f822"; } + +.fa-sigma:before { + content: "\f68b"; } + .fa-sign:before { content: "\f4d9"; } +.fa-sign-in:before { + content: "\f090"; } + .fa-sign-in-alt:before { content: "\f2f6"; } .fa-sign-language:before { content: "\f2a7"; } +.fa-sign-out:before { + content: "\f08b"; } + .fa-sign-out-alt:before { content: "\f2f5"; } .fa-signal:before { content: "\f012"; } +.fa-signal-1:before { + content: "\f68c"; } + +.fa-signal-2:before { + content: "\f68d"; } + +.fa-signal-3:before { + content: "\f68e"; } + +.fa-signal-4:before { + content: "\f68f"; } + +.fa-signal-alt:before { + content: "\f690"; } + +.fa-signal-alt-1:before { + content: "\f691"; } + +.fa-signal-alt-2:before { + content: "\f692"; } + +.fa-signal-alt-3:before { + content: "\f693"; } + +.fa-signal-alt-slash:before { + content: "\f694"; } + +.fa-signal-slash:before { + content: "\f695"; } + +.fa-signal-stream:before { + content: "\f8dd"; } + .fa-signature:before { content: "\f5b7"; } @@ -3280,6 +5423,15 @@ readers do not read off random characters that represent icons */ .fa-simplybuilt:before { content: "\f215"; } +.fa-sink:before { + content: "\e06d"; } + +.fa-siren:before { + content: "\e02d"; } + +.fa-siren-on:before { + content: "\e02e"; } + .fa-sistrix:before { content: "\f3ee"; } @@ -3292,9 +5444,18 @@ readers do not read off random characters that represent icons */ .fa-skating:before { content: "\f7c5"; } +.fa-skeleton:before { + content: "\f620"; } + .fa-sketch:before { content: "\f7c6"; } +.fa-ski-jump:before { + content: "\f7c7"; } + +.fa-ski-lift:before { + content: "\f7c8"; } + .fa-skiing:before { content: "\f7c9"; } @@ -3304,6 +5465,9 @@ readers do not read off random characters that represent icons */ .fa-skull:before { content: "\f54c"; } +.fa-skull-cow:before { + content: "\f8de"; } + .fa-skull-crossbones:before { content: "\f714"; } @@ -3322,12 +5486,24 @@ readers do not read off random characters that represent icons */ .fa-slash:before { content: "\f715"; } +.fa-sledding:before { + content: "\f7cb"; } + .fa-sleigh:before { content: "\f7cc"; } .fa-sliders-h:before { content: "\f1de"; } +.fa-sliders-h-square:before { + content: "\f3f0"; } + +.fa-sliders-v:before { + content: "\f3f1"; } + +.fa-sliders-v-square:before { + content: "\f3f2"; } + .fa-slideshare:before { content: "\f1e7"; } @@ -3337,12 +5513,18 @@ readers do not read off random characters that represent icons */ .fa-smile-beam:before { content: "\f5b8"; } +.fa-smile-plus:before { + content: "\f5b9"; } + .fa-smile-wink:before { content: "\f4da"; } .fa-smog:before { content: "\f75f"; } +.fa-smoke:before { + content: "\f760"; } + .fa-smoking:before { content: "\f48d"; } @@ -3352,6 +5534,9 @@ readers do not read off random characters that represent icons */ .fa-sms:before { content: "\f7cd"; } +.fa-snake:before { + content: "\f716"; } + .fa-snapchat:before { content: "\f2ab"; } @@ -3361,54 +5546,129 @@ readers do not read off random characters that represent icons */ .fa-snapchat-square:before { content: "\f2ad"; } +.fa-snooze:before { + content: "\f880"; } + +.fa-snow-blowing:before { + content: "\f761"; } + .fa-snowboarding:before { content: "\f7ce"; } .fa-snowflake:before { content: "\f2dc"; } +.fa-snowflakes:before { + content: "\f7cf"; } + .fa-snowman:before { content: "\f7d0"; } +.fa-snowmobile:before { + content: "\f7d1"; } + .fa-snowplow:before { content: "\f7d2"; } +.fa-soap:before { + content: "\e06e"; } + .fa-socks:before { content: "\f696"; } .fa-solar-panel:before { content: "\f5ba"; } +.fa-solar-system:before { + content: "\e02f"; } + .fa-sort:before { content: "\f0dc"; } .fa-sort-alpha-down:before { content: "\f15d"; } +.fa-sort-alpha-down-alt:before { + content: "\f881"; } + .fa-sort-alpha-up:before { content: "\f15e"; } +.fa-sort-alpha-up-alt:before { + content: "\f882"; } + +.fa-sort-alt:before { + content: "\f883"; } + .fa-sort-amount-down:before { content: "\f160"; } +.fa-sort-amount-down-alt:before { + content: "\f884"; } + .fa-sort-amount-up:before { content: "\f161"; } +.fa-sort-amount-up-alt:before { + content: "\f885"; } + +.fa-sort-circle:before { + content: "\e030"; } + +.fa-sort-circle-down:before { + content: "\e031"; } + +.fa-sort-circle-up:before { + content: "\e032"; } + .fa-sort-down:before { content: "\f0dd"; } .fa-sort-numeric-down:before { content: "\f162"; } +.fa-sort-numeric-down-alt:before { + content: "\f886"; } + .fa-sort-numeric-up:before { content: "\f163"; } +.fa-sort-numeric-up-alt:before { + content: "\f887"; } + +.fa-sort-shapes-down:before { + content: "\f888"; } + +.fa-sort-shapes-down-alt:before { + content: "\f889"; } + +.fa-sort-shapes-up:before { + content: "\f88a"; } + +.fa-sort-shapes-up-alt:before { + content: "\f88b"; } + +.fa-sort-size-down:before { + content: "\f88c"; } + +.fa-sort-size-down-alt:before { + content: "\f88d"; } + +.fa-sort-size-up:before { + content: "\f88e"; } + +.fa-sort-size-up-alt:before { + content: "\f88f"; } + .fa-sort-up:before { content: "\f0de"; } .fa-soundcloud:before { content: "\f1be"; } +.fa-soup:before { + content: "\f823"; } + .fa-sourcetree:before { content: "\f7d3"; } @@ -3418,15 +5678,48 @@ readers do not read off random characters that represent icons */ .fa-space-shuttle:before { content: "\f197"; } +.fa-space-station-moon:before { + content: "\e033"; } + +.fa-space-station-moon-alt:before { + content: "\e034"; } + +.fa-spade:before { + content: "\f2f4"; } + +.fa-sparkles:before { + content: "\f890"; } + .fa-speakap:before { content: "\f3f3"; } +.fa-speaker:before { + content: "\f8df"; } + +.fa-speaker-deck:before { + content: "\f83c"; } + +.fa-speakers:before { + content: "\f8e0"; } + +.fa-spell-check:before { + content: "\f891"; } + .fa-spider:before { content: "\f717"; } +.fa-spider-black-widow:before { + content: "\f718"; } + +.fa-spider-web:before { + content: "\f719"; } + .fa-spinner:before { content: "\f110"; } +.fa-spinner-third:before { + content: "\f3f4"; } + .fa-splotch:before { content: "\f5bc"; } @@ -3436,24 +5729,39 @@ readers do not read off random characters that represent icons */ .fa-spray-can:before { content: "\f5bd"; } +.fa-sprinkler:before { + content: "\e035"; } + .fa-square:before { content: "\f0c8"; } .fa-square-full:before { content: "\f45c"; } +.fa-square-root:before { + content: "\f697"; } + .fa-square-root-alt:before { content: "\f698"; } .fa-squarespace:before { content: "\f5be"; } +.fa-squirrel:before { + content: "\f71a"; } + .fa-stack-exchange:before { content: "\f18d"; } .fa-stack-overflow:before { content: "\f16c"; } +.fa-stackpath:before { + content: "\f842"; } + +.fa-staff:before { + content: "\f71b"; } + .fa-stamp:before { content: "\f5bf"; } @@ -3463,6 +5771,12 @@ readers do not read off random characters that represent icons */ .fa-star-and-crescent:before { content: "\f699"; } +.fa-star-christmas:before { + content: "\f7d4"; } + +.fa-star-exclamation:before { + content: "\f2f3"; } + .fa-star-half:before { content: "\f089"; } @@ -3475,9 +5789,30 @@ readers do not read off random characters that represent icons */ .fa-star-of-life:before { content: "\f621"; } +.fa-star-shooting:before { + content: "\e036"; } + +.fa-starfighter:before { + content: "\e037"; } + +.fa-starfighter-alt:before { + content: "\e038"; } + +.fa-stars:before { + content: "\f762"; } + +.fa-starship:before { + content: "\e039"; } + +.fa-starship-freighter:before { + content: "\e03a"; } + .fa-staylinked:before { content: "\f3f5"; } +.fa-steak:before { + content: "\f824"; } + .fa-steam:before { content: "\f1b6"; } @@ -3487,6 +5822,9 @@ readers do not read off random characters that represent icons */ .fa-steam-symbol:before { content: "\f3f6"; } +.fa-steering-wheel:before { + content: "\f622"; } + .fa-step-backward:before { content: "\f048"; } @@ -3502,6 +5840,12 @@ readers do not read off random characters that represent icons */ .fa-sticky-note:before { content: "\f249"; } +.fa-stocking:before { + content: "\f7d5"; } + +.fa-stomach:before { + content: "\f623"; } + .fa-stop:before { content: "\f04d"; } @@ -3511,12 +5855,21 @@ readers do not read off random characters that represent icons */ .fa-stopwatch:before { content: "\f2f2"; } +.fa-stopwatch-20:before { + content: "\e06f"; } + .fa-store:before { content: "\f54e"; } .fa-store-alt:before { content: "\f54f"; } +.fa-store-alt-slash:before { + content: "\e070"; } + +.fa-store-slash:before { + content: "\e071"; } + .fa-strava:before { content: "\f428"; } @@ -3526,6 +5879,9 @@ readers do not read off random characters that represent icons */ .fa-street-view:before { content: "\f21d"; } +.fa-stretcher:before { + content: "\f825"; } + .fa-strikethrough:before { content: "\f0cc"; } @@ -3562,6 +5918,24 @@ readers do not read off random characters that represent icons */ .fa-sun:before { content: "\f185"; } +.fa-sun-cloud:before { + content: "\f763"; } + +.fa-sun-dust:before { + content: "\f764"; } + +.fa-sun-haze:before { + content: "\f765"; } + +.fa-sunglasses:before { + content: "\f892"; } + +.fa-sunrise:before { + content: "\f766"; } + +.fa-sunset:before { + content: "\f767"; } + .fa-superpowers:before { content: "\f2dd"; } @@ -3580,12 +5954,33 @@ readers do not read off random characters that represent icons */ .fa-swatchbook:before { content: "\f5c3"; } +.fa-swift:before { + content: "\f8e1"; } + .fa-swimmer:before { content: "\f5c4"; } .fa-swimming-pool:before { content: "\f5c5"; } +.fa-sword:before { + content: "\f71c"; } + +.fa-sword-laser:before { + content: "\e03b"; } + +.fa-sword-laser-alt:before { + content: "\e03c"; } + +.fa-swords:before { + content: "\f71d"; } + +.fa-swords-laser:before { + content: "\e03d"; } + +.fa-symfony:before { + content: "\f83d"; } + .fa-synagogue:before { content: "\f69b"; } @@ -3610,24 +6005,78 @@ readers do not read off random characters that represent icons */ .fa-tablet-alt:before { content: "\f3fa"; } +.fa-tablet-android:before { + content: "\f3fb"; } + +.fa-tablet-android-alt:before { + content: "\f3fc"; } + +.fa-tablet-rugged:before { + content: "\f48f"; } + .fa-tablets:before { content: "\f490"; } +.fa-tachometer:before { + content: "\f0e4"; } + .fa-tachometer-alt:before { content: "\f3fd"; } +.fa-tachometer-alt-average:before { + content: "\f624"; } + +.fa-tachometer-alt-fast:before { + content: "\f625"; } + +.fa-tachometer-alt-fastest:before { + content: "\f626"; } + +.fa-tachometer-alt-slow:before { + content: "\f627"; } + +.fa-tachometer-alt-slowest:before { + content: "\f628"; } + +.fa-tachometer-average:before { + content: "\f629"; } + +.fa-tachometer-fast:before { + content: "\f62a"; } + +.fa-tachometer-fastest:before { + content: "\f62b"; } + +.fa-tachometer-slow:before { + content: "\f62c"; } + +.fa-tachometer-slowest:before { + content: "\f62d"; } + +.fa-taco:before { + content: "\f826"; } + .fa-tag:before { content: "\f02b"; } .fa-tags:before { content: "\f02c"; } +.fa-tally:before { + content: "\f69c"; } + +.fa-tanakh:before { + content: "\f827"; } + .fa-tape:before { content: "\f4db"; } .fa-tasks:before { content: "\f0ae"; } +.fa-tasks-alt:before { + content: "\f828"; } + .fa-taxi:before { content: "\f1ba"; } @@ -3646,24 +6095,48 @@ readers do not read off random characters that represent icons */ .fa-telegram-plane:before { content: "\f3fe"; } +.fa-telescope:before { + content: "\e03e"; } + +.fa-temperature-down:before { + content: "\e03f"; } + +.fa-temperature-frigid:before { + content: "\f768"; } + .fa-temperature-high:before { content: "\f769"; } +.fa-temperature-hot:before { + content: "\f76a"; } + .fa-temperature-low:before { content: "\f76b"; } +.fa-temperature-up:before { + content: "\e040"; } + .fa-tencent-weibo:before { content: "\f1d5"; } .fa-tenge:before { content: "\f7d7"; } +.fa-tennis-ball:before { + content: "\f45e"; } + .fa-terminal:before { content: "\f120"; } +.fa-text:before { + content: "\f893"; } + .fa-text-height:before { content: "\f034"; } +.fa-text-size:before { + content: "\f894"; } + .fa-text-width:before { content: "\f035"; } @@ -3706,6 +6179,9 @@ readers do not read off random characters that represent icons */ .fa-thermometer-three-quarters:before { content: "\f2c8"; } +.fa-theta:before { + content: "\f69e"; } + .fa-think-peaks:before { content: "\f731"; } @@ -3718,21 +6194,60 @@ readers do not read off random characters that represent icons */ .fa-thumbtack:before { content: "\f08d"; } +.fa-thunderstorm:before { + content: "\f76c"; } + +.fa-thunderstorm-moon:before { + content: "\f76d"; } + +.fa-thunderstorm-sun:before { + content: "\f76e"; } + +.fa-ticket:before { + content: "\f145"; } + .fa-ticket-alt:before { content: "\f3ff"; } +.fa-tiktok:before { + content: "\e07b"; } + +.fa-tilde:before { + content: "\f69f"; } + .fa-times:before { content: "\f00d"; } .fa-times-circle:before { content: "\f057"; } +.fa-times-hexagon:before { + content: "\f2ee"; } + +.fa-times-octagon:before { + content: "\f2f0"; } + +.fa-times-square:before { + content: "\f2d3"; } + .fa-tint:before { content: "\f043"; } .fa-tint-slash:before { content: "\f5c7"; } +.fa-tire:before { + content: "\f631"; } + +.fa-tire-flat:before { + content: "\f632"; } + +.fa-tire-pressure-warning:before { + content: "\f633"; } + +.fa-tire-rugged:before { + content: "\f634"; } + .fa-tired:before { content: "\f5c8"; } @@ -3748,6 +6263,18 @@ readers do not read off random characters that represent icons */ .fa-toilet-paper:before { content: "\f71e"; } +.fa-toilet-paper-alt:before { + content: "\f71f"; } + +.fa-toilet-paper-slash:before { + content: "\e072"; } + +.fa-tombstone:before { + content: "\f720"; } + +.fa-tombstone-alt:before { + content: "\f721"; } + .fa-toolbox:before { content: "\f552"; } @@ -3757,12 +6284,18 @@ readers do not read off random characters that represent icons */ .fa-tooth:before { content: "\f5c9"; } +.fa-toothbrush:before { + content: "\f635"; } + .fa-torah:before { content: "\f6a0"; } .fa-torii-gate:before { content: "\f6a1"; } +.fa-tornado:before { + content: "\f76f"; } + .fa-tractor:before { content: "\f722"; } @@ -3772,9 +6305,24 @@ readers do not read off random characters that represent icons */ .fa-trademark:before { content: "\f25c"; } +.fa-traffic-cone:before { + content: "\f636"; } + .fa-traffic-light:before { content: "\f637"; } +.fa-traffic-light-go:before { + content: "\f638"; } + +.fa-traffic-light-slow:before { + content: "\f639"; } + +.fa-traffic-light-stop:before { + content: "\f63a"; } + +.fa-trailer:before { + content: "\e041"; } + .fa-train:before { content: "\f238"; } @@ -3787,6 +6335,21 @@ readers do not read off random characters that represent icons */ .fa-transgender-alt:before { content: "\f225"; } +.fa-transporter:before { + content: "\e042"; } + +.fa-transporter-1:before { + content: "\e043"; } + +.fa-transporter-2:before { + content: "\e044"; } + +.fa-transporter-3:before { + content: "\e045"; } + +.fa-transporter-empty:before { + content: "\e046"; } + .fa-trash:before { content: "\f1f8"; } @@ -3799,21 +6362,60 @@ readers do not read off random characters that represent icons */ .fa-trash-restore-alt:before { content: "\f82a"; } +.fa-trash-undo:before { + content: "\f895"; } + +.fa-trash-undo-alt:before { + content: "\f896"; } + +.fa-treasure-chest:before { + content: "\f723"; } + .fa-tree:before { content: "\f1bb"; } +.fa-tree-alt:before { + content: "\f400"; } + +.fa-tree-christmas:before { + content: "\f7db"; } + +.fa-tree-decorated:before { + content: "\f7dc"; } + +.fa-tree-large:before { + content: "\f7dd"; } + +.fa-tree-palm:before { + content: "\f82b"; } + +.fa-trees:before { + content: "\f724"; } + .fa-trello:before { content: "\f181"; } -.fa-tripadvisor:before { - content: "\f262"; } +.fa-triangle:before { + content: "\f2ec"; } + +.fa-triangle-music:before { + content: "\f8e2"; } .fa-trophy:before { content: "\f091"; } +.fa-trophy-alt:before { + content: "\f2eb"; } + .fa-truck:before { content: "\f0d1"; } +.fa-truck-container:before { + content: "\f4dc"; } + +.fa-truck-couch:before { + content: "\f4dd"; } + .fa-truck-loading:before { content: "\f4de"; } @@ -3826,6 +6428,15 @@ readers do not read off random characters that represent icons */ .fa-truck-pickup:before { content: "\f63c"; } +.fa-truck-plow:before { + content: "\f7de"; } + +.fa-truck-ramp:before { + content: "\f4e0"; } + +.fa-trumpet:before { + content: "\f8e3"; } + .fa-tshirt:before { content: "\f553"; } @@ -3838,9 +6449,27 @@ readers do not read off random characters that represent icons */ .fa-tumblr-square:before { content: "\f174"; } +.fa-turkey:before { + content: "\f725"; } + +.fa-turntable:before { + content: "\f8e4"; } + +.fa-turtle:before { + content: "\f726"; } + .fa-tv:before { content: "\f26c"; } +.fa-tv-alt:before { + content: "\f8e5"; } + +.fa-tv-music:before { + content: "\f8e6"; } + +.fa-tv-retro:before { + content: "\f401"; } + .fa-twitch:before { content: "\f1e8"; } @@ -3850,6 +6479,9 @@ readers do not read off random characters that represent icons */ .fa-twitter-square:before { content: "\f081"; } +.fa-typewriter:before { + content: "\f8e7"; } + .fa-typo3:before { content: "\f42b"; } @@ -3859,15 +6491,27 @@ readers do not read off random characters that represent icons */ .fa-ubuntu:before { content: "\f7df"; } +.fa-ufo:before { + content: "\e047"; } + +.fa-ufo-beam:before { + content: "\e048"; } + .fa-uikit:before { content: "\f403"; } +.fa-umbraco:before { + content: "\f8e8"; } + .fa-umbrella:before { content: "\f0e9"; } .fa-umbrella-beach:before { content: "\f5ca"; } +.fa-uncharted:before { + content: "\e084"; } + .fa-underline:before { content: "\f0cd"; } @@ -3877,9 +6521,18 @@ readers do not read off random characters that represent icons */ .fa-undo-alt:before { content: "\f2ea"; } +.fa-unicorn:before { + content: "\f727"; } + +.fa-union:before { + content: "\f6a2"; } + .fa-uniregistry:before { content: "\f404"; } +.fa-unity:before { + content: "\e049"; } + .fa-universal-access:before { content: "\f29a"; } @@ -3895,6 +6548,9 @@ readers do not read off random characters that represent icons */ .fa-unlock-alt:before { content: "\f13e"; } +.fa-unsplash:before { + content: "\e07c"; } + .fa-untappd:before { content: "\f405"; } @@ -3907,9 +6563,21 @@ readers do not read off random characters that represent icons */ .fa-usb:before { content: "\f287"; } +.fa-usb-drive:before { + content: "\f8e9"; } + +.fa-usd-circle:before { + content: "\f2e8"; } + +.fa-usd-square:before { + content: "\f2e9"; } + .fa-user:before { content: "\f007"; } +.fa-user-alien:before { + content: "\e04a"; } + .fa-user-alt:before { content: "\f406"; } @@ -3919,6 +6587,9 @@ readers do not read off random characters that represent icons */ .fa-user-astronaut:before { content: "\f4fb"; } +.fa-user-chart:before { + content: "\f6a3"; } + .fa-user-check:before { content: "\f4fc"; } @@ -3931,6 +6602,12 @@ readers do not read off random characters that represent icons */ .fa-user-cog:before { content: "\f4fe"; } +.fa-user-cowboy:before { + content: "\f8ea"; } + +.fa-user-crown:before { + content: "\f6a4"; } + .fa-user-edit:before { content: "\f4ff"; } @@ -3940,6 +6617,12 @@ readers do not read off random characters that represent icons */ .fa-user-graduate:before { content: "\f501"; } +.fa-user-hard-hat:before { + content: "\f82c"; } + +.fa-user-headset:before { + content: "\f82d"; } + .fa-user-injured:before { content: "\f728"; } @@ -3949,9 +6632,15 @@ readers do not read off random characters that represent icons */ .fa-user-md:before { content: "\f0f0"; } +.fa-user-md-chat:before { + content: "\f82e"; } + .fa-user-minus:before { content: "\f503"; } +.fa-user-music:before { + content: "\f8eb"; } + .fa-user-ninja:before { content: "\f504"; } @@ -3961,6 +6650,9 @@ readers do not read off random characters that represent icons */ .fa-user-plus:before { content: "\f234"; } +.fa-user-robot:before { + content: "\e04b"; } + .fa-user-secret:before { content: "\f21b"; } @@ -3979,27 +6671,63 @@ readers do not read off random characters that represent icons */ .fa-user-times:before { content: "\f235"; } +.fa-user-unlock:before { + content: "\e058"; } + +.fa-user-visor:before { + content: "\e04c"; } + .fa-users:before { content: "\f0c0"; } +.fa-users-class:before { + content: "\f63d"; } + .fa-users-cog:before { content: "\f509"; } +.fa-users-crown:before { + content: "\f6a5"; } + +.fa-users-medical:before { + content: "\f830"; } + +.fa-users-slash:before { + content: "\e073"; } + .fa-usps:before { content: "\f7e1"; } .fa-ussunnah:before { content: "\f407"; } +.fa-utensil-fork:before { + content: "\f2e3"; } + +.fa-utensil-knife:before { + content: "\f2e4"; } + .fa-utensil-spoon:before { content: "\f2e5"; } .fa-utensils:before { content: "\f2e7"; } +.fa-utensils-alt:before { + content: "\f2e6"; } + .fa-vaadin:before { content: "\f408"; } +.fa-vacuum:before { + content: "\e04d"; } + +.fa-vacuum-robot:before { + content: "\e04e"; } + +.fa-value-absolute:before { + content: "\f6a6"; } + .fa-vector-square:before { content: "\f5cb"; } @@ -4012,6 +6740,15 @@ readers do not read off random characters that represent icons */ .fa-venus-mars:before { content: "\f228"; } +.fa-vest:before { + content: "\e085"; } + +.fa-vest-patches:before { + content: "\e086"; } + +.fa-vhs:before { + content: "\f8ec"; } + .fa-viacoin:before { content: "\f237"; } @@ -4033,6 +6770,9 @@ readers do not read off random characters that represent icons */ .fa-video:before { content: "\f03d"; } +.fa-video-plus:before { + content: "\f4e1"; } + .fa-video-slash:before { content: "\f4e2"; } @@ -4051,15 +6791,36 @@ readers do not read off random characters that represent icons */ .fa-vine:before { content: "\f1ca"; } +.fa-violin:before { + content: "\f8ed"; } + +.fa-virus:before { + content: "\e074"; } + +.fa-virus-slash:before { + content: "\e075"; } + +.fa-viruses:before { + content: "\e076"; } + .fa-vk:before { content: "\f189"; } .fa-vnv:before { content: "\f40b"; } +.fa-voicemail:before { + content: "\f897"; } + +.fa-volcano:before { + content: "\f770"; } + .fa-volleyball-ball:before { content: "\f45f"; } +.fa-volume:before { + content: "\f6a8"; } + .fa-volume-down:before { content: "\f027"; } @@ -4069,9 +6830,15 @@ readers do not read off random characters that represent icons */ .fa-volume-off:before { content: "\f026"; } +.fa-volume-slash:before { + content: "\f2e2"; } + .fa-volume-up:before { content: "\f028"; } +.fa-vote-nay:before { + content: "\f771"; } + .fa-vote-yea:before { content: "\f772"; } @@ -4081,18 +6848,81 @@ readers do not read off random characters that represent icons */ .fa-vuejs:before { content: "\f41f"; } +.fa-wagon-covered:before { + content: "\f8ee"; } + +.fa-walker:before { + content: "\f831"; } + +.fa-walkie-talkie:before { + content: "\f8ef"; } + .fa-walking:before { content: "\f554"; } .fa-wallet:before { content: "\f555"; } +.fa-wand:before { + content: "\f72a"; } + +.fa-wand-magic:before { + content: "\f72b"; } + .fa-warehouse:before { content: "\f494"; } +.fa-warehouse-alt:before { + content: "\f495"; } + +.fa-washer:before { + content: "\f898"; } + +.fa-watch:before { + content: "\f2e1"; } + +.fa-watch-calculator:before { + content: "\f8f0"; } + +.fa-watch-fitness:before { + content: "\f63e"; } + +.fa-watchman-monitoring:before { + content: "\e087"; } + .fa-water:before { content: "\f773"; } +.fa-water-lower:before { + content: "\f774"; } + +.fa-water-rise:before { + content: "\f775"; } + +.fa-wave-sine:before { + content: "\f899"; } + +.fa-wave-square:before { + content: "\f83e"; } + +.fa-wave-triangle:before { + content: "\f89a"; } + +.fa-waveform:before { + content: "\f8f1"; } + +.fa-waveform-path:before { + content: "\f8f2"; } + +.fa-waze:before { + content: "\f83f"; } + +.fa-webcam:before { + content: "\f832"; } + +.fa-webcam-slash:before { + content: "\f833"; } + .fa-weebly:before { content: "\f5cc"; } @@ -4108,30 +6938,66 @@ readers do not read off random characters that represent icons */ .fa-weixin:before { content: "\f1d7"; } +.fa-whale:before { + content: "\f72c"; } + .fa-whatsapp:before { content: "\f232"; } .fa-whatsapp-square:before { content: "\f40c"; } +.fa-wheat:before { + content: "\f72d"; } + .fa-wheelchair:before { content: "\f193"; } +.fa-whistle:before { + content: "\f460"; } + .fa-whmcs:before { content: "\f40d"; } .fa-wifi:before { content: "\f1eb"; } +.fa-wifi-1:before { + content: "\f6aa"; } + +.fa-wifi-2:before { + content: "\f6ab"; } + +.fa-wifi-slash:before { + content: "\f6ac"; } + .fa-wikipedia-w:before { content: "\f266"; } .fa-wind:before { content: "\f72e"; } +.fa-wind-turbine:before { + content: "\f89b"; } + +.fa-wind-warning:before { + content: "\f776"; } + +.fa-window:before { + content: "\f40e"; } + +.fa-window-alt:before { + content: "\f40f"; } + .fa-window-close:before { content: "\f410"; } +.fa-window-frame:before { + content: "\e04f"; } + +.fa-window-frame-open:before { + content: "\e050"; } + .fa-window-maximize:before { content: "\f2d0"; } @@ -4144,6 +7010,9 @@ readers do not read off random characters that represent icons */ .fa-windows:before { content: "\f17a"; } +.fa-windsock:before { + content: "\f777"; } + .fa-wine-bottle:before { content: "\f72f"; } @@ -4159,6 +7028,9 @@ readers do not read off random characters that represent icons */ .fa-wizards-of-the-coast:before { content: "\f730"; } +.fa-wodu:before { + content: "\e088"; } + .fa-wolf-pack-battalion:before { content: "\f514"; } @@ -4183,6 +7055,9 @@ readers do not read off random characters that represent icons */ .fa-wpressr:before { content: "\f3e4"; } +.fa-wreath:before { + content: "\f7e2"; } + .fa-wrench:before { content: "\f0ad"; } @@ -4204,6 +7079,9 @@ readers do not read off random characters that represent icons */ .fa-yahoo:before { content: "\f19e"; } +.fa-yammer:before { + content: "\f840"; } + .fa-yandex:before { content: "\f413"; } @@ -4254,18 +7132,5635 @@ readers do not read off random characters that represent icons */ @font-face { font-family: 'Font Awesome 5 Brands'; font-style: normal; - font-weight: normal; - font-display: swap; + font-weight: 400; + font-display: block; src: url("/fonts/fa-brands-400.eot"); src: url("/fonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("/fonts/fa-brands-400.woff2") format("woff2"), url("/fonts/fa-brands-400.woff") format("woff"), url("/fonts/fa-brands-400.ttf") format("truetype"), url("/fonts/fa-brands-400.svg#fontawesome") format("svg"); } .fab { - font-family: 'Font Awesome 5 Brands'; } + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } +@font-face { + font-family: 'Font Awesome 5 Duotone'; + font-style: normal; + font-weight: 900; + font-display: block; + src: url("/fonts/fa-duotone-900.eot"); + src: url("/fonts/fa-duotone-900.eot?#iefix") format("embedded-opentype"), url("/fonts/fa-duotone-900.woff2") format("woff2"), url("/fonts/fa-duotone-900.woff") format("woff"), url("/fonts/fa-duotone-900.ttf") format("truetype"), url("/fonts/fa-duotone-900.svg#fontawesome") format("svg"); } + +.fad { + position: relative; + font-family: 'Font Awesome 5 Duotone'; + font-weight: 900; } + +.fad:before { + position: absolute; + color: var(--fa-primary-color, inherit); + opacity: 1; + opacity: var(--fa-primary-opacity, 1); } + +.fad:after { + color: var(--fa-secondary-color, inherit); + opacity: 0.4; + opacity: var(--fa-secondary-opacity, 0.4); } + +.fa-swap-opacity .fad:before, +.fad.fa-swap-opacity:before { + opacity: 0.4; + opacity: var(--fa-secondary-opacity, 0.4); } + +.fa-swap-opacity .fad:after, +.fad.fa-swap-opacity:after { + opacity: 1; + opacity: var(--fa-primary-opacity, 1); } + +.fad.fa-inverse { + color: #fff; } + +.fad.fa-stack-1x, .fad.fa-stack-2x { + position: absolute; } + +.fad.fa-stack-1x:before, +.fad.fa-stack-2x:before, +.fad.fa-fw:before { + left: 50%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); } + +.fad.fa-abacus:after { + content: "\10f640"; } + +.fad.fa-acorn:after { + content: "\10f6ae"; } + +.fad.fa-ad:after { + content: "\10f641"; } + +.fad.fa-address-book:after { + content: "\10f2b9"; } + +.fad.fa-address-card:after { + content: "\10f2bb"; } + +.fad.fa-adjust:after { + content: "\10f042"; } + +.fad.fa-air-conditioner:after { + content: "\10f8f4"; } + +.fad.fa-air-freshener:after { + content: "\10f5d0"; } + +.fad.fa-alarm-clock:after { + content: "\10f34e"; } + +.fad.fa-alarm-exclamation:after { + content: "\10f843"; } + +.fad.fa-alarm-plus:after { + content: "\10f844"; } + +.fad.fa-alarm-snooze:after { + content: "\10f845"; } + +.fad.fa-album:after { + content: "\10f89f"; } + +.fad.fa-album-collection:after { + content: "\10f8a0"; } + +.fad.fa-alicorn:after { + content: "\10f6b0"; } + +.fad.fa-alien:after { + content: "\10f8f5"; } + +.fad.fa-alien-monster:after { + content: "\10f8f6"; } + +.fad.fa-align-center:after { + content: "\10f037"; } + +.fad.fa-align-justify:after { + content: "\10f039"; } + +.fad.fa-align-left:after { + content: "\10f036"; } + +.fad.fa-align-right:after { + content: "\10f038"; } + +.fad.fa-align-slash:after { + content: "\10f846"; } + +.fad.fa-allergies:after { + content: "\10f461"; } + +.fad.fa-ambulance:after { + content: "\10f0f9"; } + +.fad.fa-american-sign-language-interpreting:after { + content: "\10f2a3"; } + +.fad.fa-amp-guitar:after { + content: "\10f8a1"; } + +.fad.fa-analytics:after { + content: "\10f643"; } + +.fad.fa-anchor:after { + content: "\10f13d"; } + +.fad.fa-angel:after { + content: "\10f779"; } + +.fad.fa-angle-double-down:after { + content: "\10f103"; } + +.fad.fa-angle-double-left:after { + content: "\10f100"; } + +.fad.fa-angle-double-right:after { + content: "\10f101"; } + +.fad.fa-angle-double-up:after { + content: "\10f102"; } + +.fad.fa-angle-down:after { + content: "\10f107"; } + +.fad.fa-angle-left:after { + content: "\10f104"; } + +.fad.fa-angle-right:after { + content: "\10f105"; } + +.fad.fa-angle-up:after { + content: "\10f106"; } + +.fad.fa-angry:after { + content: "\10f556"; } + +.fad.fa-ankh:after { + content: "\10f644"; } + +.fad.fa-apple-alt:after { + content: "\10f5d1"; } + +.fad.fa-apple-crate:after { + content: "\10f6b1"; } + +.fad.fa-archive:after { + content: "\10f187"; } + +.fad.fa-archway:after { + content: "\10f557"; } + +.fad.fa-arrow-alt-circle-down:after { + content: "\10f358"; } + +.fad.fa-arrow-alt-circle-left:after { + content: "\10f359"; } + +.fad.fa-arrow-alt-circle-right:after { + content: "\10f35a"; } + +.fad.fa-arrow-alt-circle-up:after { + content: "\10f35b"; } + +.fad.fa-arrow-alt-down:after { + content: "\10f354"; } + +.fad.fa-arrow-alt-from-bottom:after { + content: "\10f346"; } + +.fad.fa-arrow-alt-from-left:after { + content: "\10f347"; } + +.fad.fa-arrow-alt-from-right:after { + content: "\10f348"; } + +.fad.fa-arrow-alt-from-top:after { + content: "\10f349"; } + +.fad.fa-arrow-alt-left:after { + content: "\10f355"; } + +.fad.fa-arrow-alt-right:after { + content: "\10f356"; } + +.fad.fa-arrow-alt-square-down:after { + content: "\10f350"; } + +.fad.fa-arrow-alt-square-left:after { + content: "\10f351"; } + +.fad.fa-arrow-alt-square-right:after { + content: "\10f352"; } + +.fad.fa-arrow-alt-square-up:after { + content: "\10f353"; } + +.fad.fa-arrow-alt-to-bottom:after { + content: "\10f34a"; } + +.fad.fa-arrow-alt-to-left:after { + content: "\10f34b"; } + +.fad.fa-arrow-alt-to-right:after { + content: "\10f34c"; } + +.fad.fa-arrow-alt-to-top:after { + content: "\10f34d"; } + +.fad.fa-arrow-alt-up:after { + content: "\10f357"; } + +.fad.fa-arrow-circle-down:after { + content: "\10f0ab"; } + +.fad.fa-arrow-circle-left:after { + content: "\10f0a8"; } + +.fad.fa-arrow-circle-right:after { + content: "\10f0a9"; } + +.fad.fa-arrow-circle-up:after { + content: "\10f0aa"; } + +.fad.fa-arrow-down:after { + content: "\10f063"; } + +.fad.fa-arrow-from-bottom:after { + content: "\10f342"; } + +.fad.fa-arrow-from-left:after { + content: "\10f343"; } + +.fad.fa-arrow-from-right:after { + content: "\10f344"; } + +.fad.fa-arrow-from-top:after { + content: "\10f345"; } + +.fad.fa-arrow-left:after { + content: "\10f060"; } + +.fad.fa-arrow-right:after { + content: "\10f061"; } + +.fad.fa-arrow-square-down:after { + content: "\10f339"; } + +.fad.fa-arrow-square-left:after { + content: "\10f33a"; } + +.fad.fa-arrow-square-right:after { + content: "\10f33b"; } + +.fad.fa-arrow-square-up:after { + content: "\10f33c"; } + +.fad.fa-arrow-to-bottom:after { + content: "\10f33d"; } + +.fad.fa-arrow-to-left:after { + content: "\10f33e"; } + +.fad.fa-arrow-to-right:after { + content: "\10f340"; } + +.fad.fa-arrow-to-top:after { + content: "\10f341"; } + +.fad.fa-arrow-up:after { + content: "\10f062"; } + +.fad.fa-arrows:after { + content: "\10f047"; } + +.fad.fa-arrows-alt:after { + content: "\10f0b2"; } + +.fad.fa-arrows-alt-h:after { + content: "\10f337"; } + +.fad.fa-arrows-alt-v:after { + content: "\10f338"; } + +.fad.fa-arrows-h:after { + content: "\10f07e"; } + +.fad.fa-arrows-v:after { + content: "\10f07d"; } + +.fad.fa-assistive-listening-systems:after { + content: "\10f2a2"; } + +.fad.fa-asterisk:after { + content: "\10f069"; } + +.fad.fa-at:after { + content: "\10f1fa"; } + +.fad.fa-atlas:after { + content: "\10f558"; } + +.fad.fa-atom:after { + content: "\10f5d2"; } + +.fad.fa-atom-alt:after { + content: "\10f5d3"; } + +.fad.fa-audio-description:after { + content: "\10f29e"; } + +.fad.fa-award:after { + content: "\10f559"; } + +.fad.fa-axe:after { + content: "\10f6b2"; } + +.fad.fa-axe-battle:after { + content: "\10f6b3"; } + +.fad.fa-baby:after { + content: "\10f77c"; } + +.fad.fa-baby-carriage:after { + content: "\10f77d"; } + +.fad.fa-backpack:after { + content: "\10f5d4"; } + +.fad.fa-backspace:after { + content: "\10f55a"; } + +.fad.fa-backward:after { + content: "\10f04a"; } + +.fad.fa-bacon:after { + content: "\10f7e5"; } + +.fad.fa-bacteria:after { + content: "\10e059"; } + +.fad.fa-bacterium:after { + content: "\10e05a"; } + +.fad.fa-badge:after { + content: "\10f335"; } + +.fad.fa-badge-check:after { + content: "\10f336"; } + +.fad.fa-badge-dollar:after { + content: "\10f645"; } + +.fad.fa-badge-percent:after { + content: "\10f646"; } + +.fad.fa-badge-sheriff:after { + content: "\10f8a2"; } + +.fad.fa-badger-honey:after { + content: "\10f6b4"; } + +.fad.fa-bags-shopping:after { + content: "\10f847"; } + +.fad.fa-bahai:after { + content: "\10f666"; } + +.fad.fa-balance-scale:after { + content: "\10f24e"; } + +.fad.fa-balance-scale-left:after { + content: "\10f515"; } + +.fad.fa-balance-scale-right:after { + content: "\10f516"; } + +.fad.fa-ball-pile:after { + content: "\10f77e"; } + +.fad.fa-ballot:after { + content: "\10f732"; } + +.fad.fa-ballot-check:after { + content: "\10f733"; } + +.fad.fa-ban:after { + content: "\10f05e"; } + +.fad.fa-band-aid:after { + content: "\10f462"; } + +.fad.fa-banjo:after { + content: "\10f8a3"; } + +.fad.fa-barcode:after { + content: "\10f02a"; } + +.fad.fa-barcode-alt:after { + content: "\10f463"; } + +.fad.fa-barcode-read:after { + content: "\10f464"; } + +.fad.fa-barcode-scan:after { + content: "\10f465"; } + +.fad.fa-bars:after { + content: "\10f0c9"; } + +.fad.fa-baseball:after { + content: "\10f432"; } + +.fad.fa-baseball-ball:after { + content: "\10f433"; } + +.fad.fa-basketball-ball:after { + content: "\10f434"; } + +.fad.fa-basketball-hoop:after { + content: "\10f435"; } + +.fad.fa-bat:after { + content: "\10f6b5"; } + +.fad.fa-bath:after { + content: "\10f2cd"; } + +.fad.fa-battery-bolt:after { + content: "\10f376"; } + +.fad.fa-battery-empty:after { + content: "\10f244"; } + +.fad.fa-battery-full:after { + content: "\10f240"; } + +.fad.fa-battery-half:after { + content: "\10f242"; } + +.fad.fa-battery-quarter:after { + content: "\10f243"; } + +.fad.fa-battery-slash:after { + content: "\10f377"; } + +.fad.fa-battery-three-quarters:after { + content: "\10f241"; } + +.fad.fa-bed:after { + content: "\10f236"; } + +.fad.fa-bed-alt:after { + content: "\10f8f7"; } + +.fad.fa-bed-bunk:after { + content: "\10f8f8"; } + +.fad.fa-bed-empty:after { + content: "\10f8f9"; } + +.fad.fa-beer:after { + content: "\10f0fc"; } + +.fad.fa-bell:after { + content: "\10f0f3"; } + +.fad.fa-bell-exclamation:after { + content: "\10f848"; } + +.fad.fa-bell-on:after { + content: "\10f8fa"; } + +.fad.fa-bell-plus:after { + content: "\10f849"; } + +.fad.fa-bell-school:after { + content: "\10f5d5"; } + +.fad.fa-bell-school-slash:after { + content: "\10f5d6"; } + +.fad.fa-bell-slash:after { + content: "\10f1f6"; } + +.fad.fa-bells:after { + content: "\10f77f"; } + +.fad.fa-betamax:after { + content: "\10f8a4"; } + +.fad.fa-bezier-curve:after { + content: "\10f55b"; } + +.fad.fa-bible:after { + content: "\10f647"; } + +.fad.fa-bicycle:after { + content: "\10f206"; } + +.fad.fa-biking:after { + content: "\10f84a"; } + +.fad.fa-biking-mountain:after { + content: "\10f84b"; } + +.fad.fa-binoculars:after { + content: "\10f1e5"; } + +.fad.fa-biohazard:after { + content: "\10f780"; } + +.fad.fa-birthday-cake:after { + content: "\10f1fd"; } + +.fad.fa-blanket:after { + content: "\10f498"; } + +.fad.fa-blender:after { + content: "\10f517"; } + +.fad.fa-blender-phone:after { + content: "\10f6b6"; } + +.fad.fa-blind:after { + content: "\10f29d"; } + +.fad.fa-blinds:after { + content: "\10f8fb"; } + +.fad.fa-blinds-open:after { + content: "\10f8fc"; } + +.fad.fa-blinds-raised:after { + content: "\10f8fd"; } + +.fad.fa-blog:after { + content: "\10f781"; } + +.fad.fa-bold:after { + content: "\10f032"; } + +.fad.fa-bolt:after { + content: "\10f0e7"; } + +.fad.fa-bomb:after { + content: "\10f1e2"; } + +.fad.fa-bone:after { + content: "\10f5d7"; } + +.fad.fa-bone-break:after { + content: "\10f5d8"; } + +.fad.fa-bong:after { + content: "\10f55c"; } + +.fad.fa-book:after { + content: "\10f02d"; } + +.fad.fa-book-alt:after { + content: "\10f5d9"; } + +.fad.fa-book-dead:after { + content: "\10f6b7"; } + +.fad.fa-book-heart:after { + content: "\10f499"; } + +.fad.fa-book-medical:after { + content: "\10f7e6"; } + +.fad.fa-book-open:after { + content: "\10f518"; } + +.fad.fa-book-reader:after { + content: "\10f5da"; } + +.fad.fa-book-spells:after { + content: "\10f6b8"; } + +.fad.fa-book-user:after { + content: "\10f7e7"; } + +.fad.fa-bookmark:after { + content: "\10f02e"; } + +.fad.fa-books:after { + content: "\10f5db"; } + +.fad.fa-books-medical:after { + content: "\10f7e8"; } + +.fad.fa-boombox:after { + content: "\10f8a5"; } + +.fad.fa-boot:after { + content: "\10f782"; } + +.fad.fa-booth-curtain:after { + content: "\10f734"; } + +.fad.fa-border-all:after { + content: "\10f84c"; } + +.fad.fa-border-bottom:after { + content: "\10f84d"; } + +.fad.fa-border-center-h:after { + content: "\10f89c"; } + +.fad.fa-border-center-v:after { + content: "\10f89d"; } + +.fad.fa-border-inner:after { + content: "\10f84e"; } + +.fad.fa-border-left:after { + content: "\10f84f"; } + +.fad.fa-border-none:after { + content: "\10f850"; } + +.fad.fa-border-outer:after { + content: "\10f851"; } + +.fad.fa-border-right:after { + content: "\10f852"; } + +.fad.fa-border-style:after { + content: "\10f853"; } + +.fad.fa-border-style-alt:after { + content: "\10f854"; } + +.fad.fa-border-top:after { + content: "\10f855"; } + +.fad.fa-bow-arrow:after { + content: "\10f6b9"; } + +.fad.fa-bowling-ball:after { + content: "\10f436"; } + +.fad.fa-bowling-pins:after { + content: "\10f437"; } + +.fad.fa-box:after { + content: "\10f466"; } + +.fad.fa-box-alt:after { + content: "\10f49a"; } + +.fad.fa-box-ballot:after { + content: "\10f735"; } + +.fad.fa-box-check:after { + content: "\10f467"; } + +.fad.fa-box-fragile:after { + content: "\10f49b"; } + +.fad.fa-box-full:after { + content: "\10f49c"; } + +.fad.fa-box-heart:after { + content: "\10f49d"; } + +.fad.fa-box-open:after { + content: "\10f49e"; } + +.fad.fa-box-tissue:after { + content: "\10e05b"; } + +.fad.fa-box-up:after { + content: "\10f49f"; } + +.fad.fa-box-usd:after { + content: "\10f4a0"; } + +.fad.fa-boxes:after { + content: "\10f468"; } + +.fad.fa-boxes-alt:after { + content: "\10f4a1"; } + +.fad.fa-boxing-glove:after { + content: "\10f438"; } + +.fad.fa-brackets:after { + content: "\10f7e9"; } + +.fad.fa-brackets-curly:after { + content: "\10f7ea"; } + +.fad.fa-braille:after { + content: "\10f2a1"; } + +.fad.fa-brain:after { + content: "\10f5dc"; } + +.fad.fa-bread-loaf:after { + content: "\10f7eb"; } + +.fad.fa-bread-slice:after { + content: "\10f7ec"; } + +.fad.fa-briefcase:after { + content: "\10f0b1"; } + +.fad.fa-briefcase-medical:after { + content: "\10f469"; } + +.fad.fa-bring-forward:after { + content: "\10f856"; } + +.fad.fa-bring-front:after { + content: "\10f857"; } + +.fad.fa-broadcast-tower:after { + content: "\10f519"; } + +.fad.fa-broom:after { + content: "\10f51a"; } + +.fad.fa-browser:after { + content: "\10f37e"; } + +.fad.fa-brush:after { + content: "\10f55d"; } + +.fad.fa-bug:after { + content: "\10f188"; } + +.fad.fa-building:after { + content: "\10f1ad"; } + +.fad.fa-bullhorn:after { + content: "\10f0a1"; } + +.fad.fa-bullseye:after { + content: "\10f140"; } + +.fad.fa-bullseye-arrow:after { + content: "\10f648"; } + +.fad.fa-bullseye-pointer:after { + content: "\10f649"; } + +.fad.fa-burger-soda:after { + content: "\10f858"; } + +.fad.fa-burn:after { + content: "\10f46a"; } + +.fad.fa-burrito:after { + content: "\10f7ed"; } + +.fad.fa-bus:after { + content: "\10f207"; } + +.fad.fa-bus-alt:after { + content: "\10f55e"; } + +.fad.fa-bus-school:after { + content: "\10f5dd"; } + +.fad.fa-business-time:after { + content: "\10f64a"; } + +.fad.fa-cabinet-filing:after { + content: "\10f64b"; } + +.fad.fa-cactus:after { + content: "\10f8a7"; } + +.fad.fa-calculator:after { + content: "\10f1ec"; } + +.fad.fa-calculator-alt:after { + content: "\10f64c"; } + +.fad.fa-calendar:after { + content: "\10f133"; } + +.fad.fa-calendar-alt:after { + content: "\10f073"; } + +.fad.fa-calendar-check:after { + content: "\10f274"; } + +.fad.fa-calendar-day:after { + content: "\10f783"; } + +.fad.fa-calendar-edit:after { + content: "\10f333"; } + +.fad.fa-calendar-exclamation:after { + content: "\10f334"; } + +.fad.fa-calendar-minus:after { + content: "\10f272"; } + +.fad.fa-calendar-plus:after { + content: "\10f271"; } + +.fad.fa-calendar-star:after { + content: "\10f736"; } + +.fad.fa-calendar-times:after { + content: "\10f273"; } + +.fad.fa-calendar-week:after { + content: "\10f784"; } + +.fad.fa-camcorder:after { + content: "\10f8a8"; } + +.fad.fa-camera:after { + content: "\10f030"; } + +.fad.fa-camera-alt:after { + content: "\10f332"; } + +.fad.fa-camera-home:after { + content: "\10f8fe"; } + +.fad.fa-camera-movie:after { + content: "\10f8a9"; } + +.fad.fa-camera-polaroid:after { + content: "\10f8aa"; } + +.fad.fa-camera-retro:after { + content: "\10f083"; } + +.fad.fa-campfire:after { + content: "\10f6ba"; } + +.fad.fa-campground:after { + content: "\10f6bb"; } + +.fad.fa-candle-holder:after { + content: "\10f6bc"; } + +.fad.fa-candy-cane:after { + content: "\10f786"; } + +.fad.fa-candy-corn:after { + content: "\10f6bd"; } + +.fad.fa-cannabis:after { + content: "\10f55f"; } + +.fad.fa-capsules:after { + content: "\10f46b"; } + +.fad.fa-car:after { + content: "\10f1b9"; } + +.fad.fa-car-alt:after { + content: "\10f5de"; } + +.fad.fa-car-battery:after { + content: "\10f5df"; } + +.fad.fa-car-building:after { + content: "\10f859"; } + +.fad.fa-car-bump:after { + content: "\10f5e0"; } + +.fad.fa-car-bus:after { + content: "\10f85a"; } + +.fad.fa-car-crash:after { + content: "\10f5e1"; } + +.fad.fa-car-garage:after { + content: "\10f5e2"; } + +.fad.fa-car-mechanic:after { + content: "\10f5e3"; } + +.fad.fa-car-side:after { + content: "\10f5e4"; } + +.fad.fa-car-tilt:after { + content: "\10f5e5"; } + +.fad.fa-car-wash:after { + content: "\10f5e6"; } + +.fad.fa-caravan:after { + content: "\10f8ff"; } + +.fad.fa-caravan-alt:after { + content: "\10e000"; } + +.fad.fa-caret-circle-down:after { + content: "\10f32d"; } + +.fad.fa-caret-circle-left:after { + content: "\10f32e"; } + +.fad.fa-caret-circle-right:after { + content: "\10f330"; } + +.fad.fa-caret-circle-up:after { + content: "\10f331"; } + +.fad.fa-caret-down:after { + content: "\10f0d7"; } + +.fad.fa-caret-left:after { + content: "\10f0d9"; } + +.fad.fa-caret-right:after { + content: "\10f0da"; } + +.fad.fa-caret-square-down:after { + content: "\10f150"; } + +.fad.fa-caret-square-left:after { + content: "\10f191"; } + +.fad.fa-caret-square-right:after { + content: "\10f152"; } + +.fad.fa-caret-square-up:after { + content: "\10f151"; } + +.fad.fa-caret-up:after { + content: "\10f0d8"; } + +.fad.fa-carrot:after { + content: "\10f787"; } + +.fad.fa-cars:after { + content: "\10f85b"; } + +.fad.fa-cart-arrow-down:after { + content: "\10f218"; } + +.fad.fa-cart-plus:after { + content: "\10f217"; } + +.fad.fa-cash-register:after { + content: "\10f788"; } + +.fad.fa-cassette-tape:after { + content: "\10f8ab"; } + +.fad.fa-cat:after { + content: "\10f6be"; } + +.fad.fa-cat-space:after { + content: "\10e001"; } + +.fad.fa-cauldron:after { + content: "\10f6bf"; } + +.fad.fa-cctv:after { + content: "\10f8ac"; } + +.fad.fa-certificate:after { + content: "\10f0a3"; } + +.fad.fa-chair:after { + content: "\10f6c0"; } + +.fad.fa-chair-office:after { + content: "\10f6c1"; } + +.fad.fa-chalkboard:after { + content: "\10f51b"; } + +.fad.fa-chalkboard-teacher:after { + content: "\10f51c"; } + +.fad.fa-charging-station:after { + content: "\10f5e7"; } + +.fad.fa-chart-area:after { + content: "\10f1fe"; } + +.fad.fa-chart-bar:after { + content: "\10f080"; } + +.fad.fa-chart-line:after { + content: "\10f201"; } + +.fad.fa-chart-line-down:after { + content: "\10f64d"; } + +.fad.fa-chart-network:after { + content: "\10f78a"; } + +.fad.fa-chart-pie:after { + content: "\10f200"; } + +.fad.fa-chart-pie-alt:after { + content: "\10f64e"; } + +.fad.fa-chart-scatter:after { + content: "\10f7ee"; } + +.fad.fa-check:after { + content: "\10f00c"; } + +.fad.fa-check-circle:after { + content: "\10f058"; } + +.fad.fa-check-double:after { + content: "\10f560"; } + +.fad.fa-check-square:after { + content: "\10f14a"; } + +.fad.fa-cheese:after { + content: "\10f7ef"; } + +.fad.fa-cheese-swiss:after { + content: "\10f7f0"; } + +.fad.fa-cheeseburger:after { + content: "\10f7f1"; } + +.fad.fa-chess:after { + content: "\10f439"; } + +.fad.fa-chess-bishop:after { + content: "\10f43a"; } + +.fad.fa-chess-bishop-alt:after { + content: "\10f43b"; } + +.fad.fa-chess-board:after { + content: "\10f43c"; } + +.fad.fa-chess-clock:after { + content: "\10f43d"; } + +.fad.fa-chess-clock-alt:after { + content: "\10f43e"; } + +.fad.fa-chess-king:after { + content: "\10f43f"; } + +.fad.fa-chess-king-alt:after { + content: "\10f440"; } + +.fad.fa-chess-knight:after { + content: "\10f441"; } + +.fad.fa-chess-knight-alt:after { + content: "\10f442"; } + +.fad.fa-chess-pawn:after { + content: "\10f443"; } + +.fad.fa-chess-pawn-alt:after { + content: "\10f444"; } + +.fad.fa-chess-queen:after { + content: "\10f445"; } + +.fad.fa-chess-queen-alt:after { + content: "\10f446"; } + +.fad.fa-chess-rook:after { + content: "\10f447"; } + +.fad.fa-chess-rook-alt:after { + content: "\10f448"; } + +.fad.fa-chevron-circle-down:after { + content: "\10f13a"; } + +.fad.fa-chevron-circle-left:after { + content: "\10f137"; } + +.fad.fa-chevron-circle-right:after { + content: "\10f138"; } + +.fad.fa-chevron-circle-up:after { + content: "\10f139"; } + +.fad.fa-chevron-double-down:after { + content: "\10f322"; } + +.fad.fa-chevron-double-left:after { + content: "\10f323"; } + +.fad.fa-chevron-double-right:after { + content: "\10f324"; } + +.fad.fa-chevron-double-up:after { + content: "\10f325"; } + +.fad.fa-chevron-down:after { + content: "\10f078"; } + +.fad.fa-chevron-left:after { + content: "\10f053"; } + +.fad.fa-chevron-right:after { + content: "\10f054"; } + +.fad.fa-chevron-square-down:after { + content: "\10f329"; } + +.fad.fa-chevron-square-left:after { + content: "\10f32a"; } + +.fad.fa-chevron-square-right:after { + content: "\10f32b"; } + +.fad.fa-chevron-square-up:after { + content: "\10f32c"; } + +.fad.fa-chevron-up:after { + content: "\10f077"; } + +.fad.fa-child:after { + content: "\10f1ae"; } + +.fad.fa-chimney:after { + content: "\10f78b"; } + +.fad.fa-church:after { + content: "\10f51d"; } + +.fad.fa-circle:after { + content: "\10f111"; } + +.fad.fa-circle-notch:after { + content: "\10f1ce"; } + +.fad.fa-city:after { + content: "\10f64f"; } + +.fad.fa-clarinet:after { + content: "\10f8ad"; } + +.fad.fa-claw-marks:after { + content: "\10f6c2"; } + +.fad.fa-clinic-medical:after { + content: "\10f7f2"; } + +.fad.fa-clipboard:after { + content: "\10f328"; } + +.fad.fa-clipboard-check:after { + content: "\10f46c"; } + +.fad.fa-clipboard-list:after { + content: "\10f46d"; } + +.fad.fa-clipboard-list-check:after { + content: "\10f737"; } + +.fad.fa-clipboard-prescription:after { + content: "\10f5e8"; } + +.fad.fa-clipboard-user:after { + content: "\10f7f3"; } + +.fad.fa-clock:after { + content: "\10f017"; } + +.fad.fa-clone:after { + content: "\10f24d"; } + +.fad.fa-closed-captioning:after { + content: "\10f20a"; } + +.fad.fa-cloud:after { + content: "\10f0c2"; } + +.fad.fa-cloud-download:after { + content: "\10f0ed"; } + +.fad.fa-cloud-download-alt:after { + content: "\10f381"; } + +.fad.fa-cloud-drizzle:after { + content: "\10f738"; } + +.fad.fa-cloud-hail:after { + content: "\10f739"; } + +.fad.fa-cloud-hail-mixed:after { + content: "\10f73a"; } + +.fad.fa-cloud-meatball:after { + content: "\10f73b"; } + +.fad.fa-cloud-moon:after { + content: "\10f6c3"; } + +.fad.fa-cloud-moon-rain:after { + content: "\10f73c"; } + +.fad.fa-cloud-music:after { + content: "\10f8ae"; } + +.fad.fa-cloud-rain:after { + content: "\10f73d"; } + +.fad.fa-cloud-rainbow:after { + content: "\10f73e"; } + +.fad.fa-cloud-showers:after { + content: "\10f73f"; } + +.fad.fa-cloud-showers-heavy:after { + content: "\10f740"; } + +.fad.fa-cloud-sleet:after { + content: "\10f741"; } + +.fad.fa-cloud-snow:after { + content: "\10f742"; } + +.fad.fa-cloud-sun:after { + content: "\10f6c4"; } + +.fad.fa-cloud-sun-rain:after { + content: "\10f743"; } + +.fad.fa-cloud-upload:after { + content: "\10f0ee"; } + +.fad.fa-cloud-upload-alt:after { + content: "\10f382"; } + +.fad.fa-clouds:after { + content: "\10f744"; } + +.fad.fa-clouds-moon:after { + content: "\10f745"; } + +.fad.fa-clouds-sun:after { + content: "\10f746"; } + +.fad.fa-club:after { + content: "\10f327"; } + +.fad.fa-cocktail:after { + content: "\10f561"; } + +.fad.fa-code:after { + content: "\10f121"; } + +.fad.fa-code-branch:after { + content: "\10f126"; } + +.fad.fa-code-commit:after { + content: "\10f386"; } + +.fad.fa-code-merge:after { + content: "\10f387"; } + +.fad.fa-coffee:after { + content: "\10f0f4"; } + +.fad.fa-coffee-pot:after { + content: "\10e002"; } + +.fad.fa-coffee-togo:after { + content: "\10f6c5"; } + +.fad.fa-coffin:after { + content: "\10f6c6"; } + +.fad.fa-coffin-cross:after { + content: "\10e051"; } + +.fad.fa-cog:after { + content: "\10f013"; } + +.fad.fa-cogs:after { + content: "\10f085"; } + +.fad.fa-coin:after { + content: "\10f85c"; } + +.fad.fa-coins:after { + content: "\10f51e"; } + +.fad.fa-columns:after { + content: "\10f0db"; } + +.fad.fa-comet:after { + content: "\10e003"; } + +.fad.fa-comment:after { + content: "\10f075"; } + +.fad.fa-comment-alt:after { + content: "\10f27a"; } + +.fad.fa-comment-alt-check:after { + content: "\10f4a2"; } + +.fad.fa-comment-alt-dollar:after { + content: "\10f650"; } + +.fad.fa-comment-alt-dots:after { + content: "\10f4a3"; } + +.fad.fa-comment-alt-edit:after { + content: "\10f4a4"; } + +.fad.fa-comment-alt-exclamation:after { + content: "\10f4a5"; } + +.fad.fa-comment-alt-lines:after { + content: "\10f4a6"; } + +.fad.fa-comment-alt-medical:after { + content: "\10f7f4"; } + +.fad.fa-comment-alt-minus:after { + content: "\10f4a7"; } + +.fad.fa-comment-alt-music:after { + content: "\10f8af"; } + +.fad.fa-comment-alt-plus:after { + content: "\10f4a8"; } + +.fad.fa-comment-alt-slash:after { + content: "\10f4a9"; } + +.fad.fa-comment-alt-smile:after { + content: "\10f4aa"; } + +.fad.fa-comment-alt-times:after { + content: "\10f4ab"; } + +.fad.fa-comment-check:after { + content: "\10f4ac"; } + +.fad.fa-comment-dollar:after { + content: "\10f651"; } + +.fad.fa-comment-dots:after { + content: "\10f4ad"; } + +.fad.fa-comment-edit:after { + content: "\10f4ae"; } + +.fad.fa-comment-exclamation:after { + content: "\10f4af"; } + +.fad.fa-comment-lines:after { + content: "\10f4b0"; } + +.fad.fa-comment-medical:after { + content: "\10f7f5"; } + +.fad.fa-comment-minus:after { + content: "\10f4b1"; } + +.fad.fa-comment-music:after { + content: "\10f8b0"; } + +.fad.fa-comment-plus:after { + content: "\10f4b2"; } + +.fad.fa-comment-slash:after { + content: "\10f4b3"; } + +.fad.fa-comment-smile:after { + content: "\10f4b4"; } + +.fad.fa-comment-times:after { + content: "\10f4b5"; } + +.fad.fa-comments:after { + content: "\10f086"; } + +.fad.fa-comments-alt:after { + content: "\10f4b6"; } + +.fad.fa-comments-alt-dollar:after { + content: "\10f652"; } + +.fad.fa-comments-dollar:after { + content: "\10f653"; } + +.fad.fa-compact-disc:after { + content: "\10f51f"; } + +.fad.fa-compass:after { + content: "\10f14e"; } + +.fad.fa-compass-slash:after { + content: "\10f5e9"; } + +.fad.fa-compress:after { + content: "\10f066"; } + +.fad.fa-compress-alt:after { + content: "\10f422"; } + +.fad.fa-compress-arrows-alt:after { + content: "\10f78c"; } + +.fad.fa-compress-wide:after { + content: "\10f326"; } + +.fad.fa-computer-classic:after { + content: "\10f8b1"; } + +.fad.fa-computer-speaker:after { + content: "\10f8b2"; } + +.fad.fa-concierge-bell:after { + content: "\10f562"; } + +.fad.fa-construction:after { + content: "\10f85d"; } + +.fad.fa-container-storage:after { + content: "\10f4b7"; } + +.fad.fa-conveyor-belt:after { + content: "\10f46e"; } + +.fad.fa-conveyor-belt-alt:after { + content: "\10f46f"; } + +.fad.fa-cookie:after { + content: "\10f563"; } + +.fad.fa-cookie-bite:after { + content: "\10f564"; } + +.fad.fa-copy:after { + content: "\10f0c5"; } + +.fad.fa-copyright:after { + content: "\10f1f9"; } + +.fad.fa-corn:after { + content: "\10f6c7"; } + +.fad.fa-couch:after { + content: "\10f4b8"; } + +.fad.fa-cow:after { + content: "\10f6c8"; } + +.fad.fa-cowbell:after { + content: "\10f8b3"; } + +.fad.fa-cowbell-more:after { + content: "\10f8b4"; } + +.fad.fa-credit-card:after { + content: "\10f09d"; } + +.fad.fa-credit-card-blank:after { + content: "\10f389"; } + +.fad.fa-credit-card-front:after { + content: "\10f38a"; } + +.fad.fa-cricket:after { + content: "\10f449"; } + +.fad.fa-croissant:after { + content: "\10f7f6"; } + +.fad.fa-crop:after { + content: "\10f125"; } + +.fad.fa-crop-alt:after { + content: "\10f565"; } + +.fad.fa-cross:after { + content: "\10f654"; } + +.fad.fa-crosshairs:after { + content: "\10f05b"; } + +.fad.fa-crow:after { + content: "\10f520"; } + +.fad.fa-crown:after { + content: "\10f521"; } + +.fad.fa-crutch:after { + content: "\10f7f7"; } + +.fad.fa-crutches:after { + content: "\10f7f8"; } + +.fad.fa-cube:after { + content: "\10f1b2"; } + +.fad.fa-cubes:after { + content: "\10f1b3"; } + +.fad.fa-curling:after { + content: "\10f44a"; } + +.fad.fa-cut:after { + content: "\10f0c4"; } + +.fad.fa-dagger:after { + content: "\10f6cb"; } + +.fad.fa-database:after { + content: "\10f1c0"; } + +.fad.fa-deaf:after { + content: "\10f2a4"; } + +.fad.fa-debug:after { + content: "\10f7f9"; } + +.fad.fa-deer:after { + content: "\10f78e"; } + +.fad.fa-deer-rudolph:after { + content: "\10f78f"; } + +.fad.fa-democrat:after { + content: "\10f747"; } + +.fad.fa-desktop:after { + content: "\10f108"; } + +.fad.fa-desktop-alt:after { + content: "\10f390"; } + +.fad.fa-dewpoint:after { + content: "\10f748"; } + +.fad.fa-dharmachakra:after { + content: "\10f655"; } + +.fad.fa-diagnoses:after { + content: "\10f470"; } + +.fad.fa-diamond:after { + content: "\10f219"; } + +.fad.fa-dice:after { + content: "\10f522"; } + +.fad.fa-dice-d10:after { + content: "\10f6cd"; } + +.fad.fa-dice-d12:after { + content: "\10f6ce"; } + +.fad.fa-dice-d20:after { + content: "\10f6cf"; } + +.fad.fa-dice-d4:after { + content: "\10f6d0"; } + +.fad.fa-dice-d6:after { + content: "\10f6d1"; } + +.fad.fa-dice-d8:after { + content: "\10f6d2"; } + +.fad.fa-dice-five:after { + content: "\10f523"; } + +.fad.fa-dice-four:after { + content: "\10f524"; } + +.fad.fa-dice-one:after { + content: "\10f525"; } + +.fad.fa-dice-six:after { + content: "\10f526"; } + +.fad.fa-dice-three:after { + content: "\10f527"; } + +.fad.fa-dice-two:after { + content: "\10f528"; } + +.fad.fa-digging:after { + content: "\10f85e"; } + +.fad.fa-digital-tachograph:after { + content: "\10f566"; } + +.fad.fa-diploma:after { + content: "\10f5ea"; } + +.fad.fa-directions:after { + content: "\10f5eb"; } + +.fad.fa-disc-drive:after { + content: "\10f8b5"; } + +.fad.fa-disease:after { + content: "\10f7fa"; } + +.fad.fa-divide:after { + content: "\10f529"; } + +.fad.fa-dizzy:after { + content: "\10f567"; } + +.fad.fa-dna:after { + content: "\10f471"; } + +.fad.fa-do-not-enter:after { + content: "\10f5ec"; } + +.fad.fa-dog:after { + content: "\10f6d3"; } + +.fad.fa-dog-leashed:after { + content: "\10f6d4"; } + +.fad.fa-dollar-sign:after { + content: "\10f155"; } + +.fad.fa-dolly:after { + content: "\10f472"; } + +.fad.fa-dolly-empty:after { + content: "\10f473"; } + +.fad.fa-dolly-flatbed:after { + content: "\10f474"; } + +.fad.fa-dolly-flatbed-alt:after { + content: "\10f475"; } + +.fad.fa-dolly-flatbed-empty:after { + content: "\10f476"; } + +.fad.fa-donate:after { + content: "\10f4b9"; } + +.fad.fa-door-closed:after { + content: "\10f52a"; } + +.fad.fa-door-open:after { + content: "\10f52b"; } + +.fad.fa-dot-circle:after { + content: "\10f192"; } + +.fad.fa-dove:after { + content: "\10f4ba"; } + +.fad.fa-download:after { + content: "\10f019"; } + +.fad.fa-drafting-compass:after { + content: "\10f568"; } + +.fad.fa-dragon:after { + content: "\10f6d5"; } + +.fad.fa-draw-circle:after { + content: "\10f5ed"; } + +.fad.fa-draw-polygon:after { + content: "\10f5ee"; } + +.fad.fa-draw-square:after { + content: "\10f5ef"; } + +.fad.fa-dreidel:after { + content: "\10f792"; } + +.fad.fa-drone:after { + content: "\10f85f"; } + +.fad.fa-drone-alt:after { + content: "\10f860"; } + +.fad.fa-drum:after { + content: "\10f569"; } + +.fad.fa-drum-steelpan:after { + content: "\10f56a"; } + +.fad.fa-drumstick:after { + content: "\10f6d6"; } + +.fad.fa-drumstick-bite:after { + content: "\10f6d7"; } + +.fad.fa-dryer:after { + content: "\10f861"; } + +.fad.fa-dryer-alt:after { + content: "\10f862"; } + +.fad.fa-duck:after { + content: "\10f6d8"; } + +.fad.fa-dumbbell:after { + content: "\10f44b"; } + +.fad.fa-dumpster:after { + content: "\10f793"; } + +.fad.fa-dumpster-fire:after { + content: "\10f794"; } + +.fad.fa-dungeon:after { + content: "\10f6d9"; } + +.fad.fa-ear:after { + content: "\10f5f0"; } + +.fad.fa-ear-muffs:after { + content: "\10f795"; } + +.fad.fa-eclipse:after { + content: "\10f749"; } + +.fad.fa-eclipse-alt:after { + content: "\10f74a"; } + +.fad.fa-edit:after { + content: "\10f044"; } + +.fad.fa-egg:after { + content: "\10f7fb"; } + +.fad.fa-egg-fried:after { + content: "\10f7fc"; } + +.fad.fa-eject:after { + content: "\10f052"; } + +.fad.fa-elephant:after { + content: "\10f6da"; } + +.fad.fa-ellipsis-h:after { + content: "\10f141"; } + +.fad.fa-ellipsis-h-alt:after { + content: "\10f39b"; } + +.fad.fa-ellipsis-v:after { + content: "\10f142"; } + +.fad.fa-ellipsis-v-alt:after { + content: "\10f39c"; } + +.fad.fa-empty-set:after { + content: "\10f656"; } + +.fad.fa-engine-warning:after { + content: "\10f5f2"; } + +.fad.fa-envelope:after { + content: "\10f0e0"; } + +.fad.fa-envelope-open:after { + content: "\10f2b6"; } + +.fad.fa-envelope-open-dollar:after { + content: "\10f657"; } + +.fad.fa-envelope-open-text:after { + content: "\10f658"; } + +.fad.fa-envelope-square:after { + content: "\10f199"; } + +.fad.fa-equals:after { + content: "\10f52c"; } + +.fad.fa-eraser:after { + content: "\10f12d"; } + +.fad.fa-ethernet:after { + content: "\10f796"; } + +.fad.fa-euro-sign:after { + content: "\10f153"; } + +.fad.fa-exchange:after { + content: "\10f0ec"; } + +.fad.fa-exchange-alt:after { + content: "\10f362"; } + +.fad.fa-exclamation:after { + content: "\10f12a"; } + +.fad.fa-exclamation-circle:after { + content: "\10f06a"; } + +.fad.fa-exclamation-square:after { + content: "\10f321"; } + +.fad.fa-exclamation-triangle:after { + content: "\10f071"; } + +.fad.fa-expand:after { + content: "\10f065"; } + +.fad.fa-expand-alt:after { + content: "\10f424"; } + +.fad.fa-expand-arrows:after { + content: "\10f31d"; } + +.fad.fa-expand-arrows-alt:after { + content: "\10f31e"; } + +.fad.fa-expand-wide:after { + content: "\10f320"; } + +.fad.fa-external-link:after { + content: "\10f08e"; } + +.fad.fa-external-link-alt:after { + content: "\10f35d"; } + +.fad.fa-external-link-square:after { + content: "\10f14c"; } + +.fad.fa-external-link-square-alt:after { + content: "\10f360"; } + +.fad.fa-eye:after { + content: "\10f06e"; } + +.fad.fa-eye-dropper:after { + content: "\10f1fb"; } + +.fad.fa-eye-evil:after { + content: "\10f6db"; } + +.fad.fa-eye-slash:after { + content: "\10f070"; } + +.fad.fa-fan:after { + content: "\10f863"; } + +.fad.fa-fan-table:after { + content: "\10e004"; } + +.fad.fa-farm:after { + content: "\10f864"; } + +.fad.fa-fast-backward:after { + content: "\10f049"; } + +.fad.fa-fast-forward:after { + content: "\10f050"; } + +.fad.fa-faucet:after { + content: "\10e005"; } + +.fad.fa-faucet-drip:after { + content: "\10e006"; } + +.fad.fa-fax:after { + content: "\10f1ac"; } + +.fad.fa-feather:after { + content: "\10f52d"; } + +.fad.fa-feather-alt:after { + content: "\10f56b"; } + +.fad.fa-female:after { + content: "\10f182"; } + +.fad.fa-field-hockey:after { + content: "\10f44c"; } + +.fad.fa-fighter-jet:after { + content: "\10f0fb"; } + +.fad.fa-file:after { + content: "\10f15b"; } + +.fad.fa-file-alt:after { + content: "\10f15c"; } + +.fad.fa-file-archive:after { + content: "\10f1c6"; } + +.fad.fa-file-audio:after { + content: "\10f1c7"; } + +.fad.fa-file-certificate:after { + content: "\10f5f3"; } + +.fad.fa-file-chart-line:after { + content: "\10f659"; } + +.fad.fa-file-chart-pie:after { + content: "\10f65a"; } + +.fad.fa-file-check:after { + content: "\10f316"; } + +.fad.fa-file-code:after { + content: "\10f1c9"; } + +.fad.fa-file-contract:after { + content: "\10f56c"; } + +.fad.fa-file-csv:after { + content: "\10f6dd"; } + +.fad.fa-file-download:after { + content: "\10f56d"; } + +.fad.fa-file-edit:after { + content: "\10f31c"; } + +.fad.fa-file-excel:after { + content: "\10f1c3"; } + +.fad.fa-file-exclamation:after { + content: "\10f31a"; } + +.fad.fa-file-export:after { + content: "\10f56e"; } + +.fad.fa-file-image:after { + content: "\10f1c5"; } + +.fad.fa-file-import:after { + content: "\10f56f"; } + +.fad.fa-file-invoice:after { + content: "\10f570"; } + +.fad.fa-file-invoice-dollar:after { + content: "\10f571"; } + +.fad.fa-file-medical:after { + content: "\10f477"; } + +.fad.fa-file-medical-alt:after { + content: "\10f478"; } + +.fad.fa-file-minus:after { + content: "\10f318"; } + +.fad.fa-file-music:after { + content: "\10f8b6"; } + +.fad.fa-file-pdf:after { + content: "\10f1c1"; } + +.fad.fa-file-plus:after { + content: "\10f319"; } + +.fad.fa-file-powerpoint:after { + content: "\10f1c4"; } + +.fad.fa-file-prescription:after { + content: "\10f572"; } + +.fad.fa-file-search:after { + content: "\10f865"; } + +.fad.fa-file-signature:after { + content: "\10f573"; } + +.fad.fa-file-spreadsheet:after { + content: "\10f65b"; } + +.fad.fa-file-times:after { + content: "\10f317"; } + +.fad.fa-file-upload:after { + content: "\10f574"; } + +.fad.fa-file-user:after { + content: "\10f65c"; } + +.fad.fa-file-video:after { + content: "\10f1c8"; } + +.fad.fa-file-word:after { + content: "\10f1c2"; } + +.fad.fa-files-medical:after { + content: "\10f7fd"; } + +.fad.fa-fill:after { + content: "\10f575"; } + +.fad.fa-fill-drip:after { + content: "\10f576"; } + +.fad.fa-film:after { + content: "\10f008"; } + +.fad.fa-film-alt:after { + content: "\10f3a0"; } + +.fad.fa-film-canister:after { + content: "\10f8b7"; } + +.fad.fa-filter:after { + content: "\10f0b0"; } + +.fad.fa-fingerprint:after { + content: "\10f577"; } + +.fad.fa-fire:after { + content: "\10f06d"; } + +.fad.fa-fire-alt:after { + content: "\10f7e4"; } + +.fad.fa-fire-extinguisher:after { + content: "\10f134"; } + +.fad.fa-fire-smoke:after { + content: "\10f74b"; } + +.fad.fa-fireplace:after { + content: "\10f79a"; } + +.fad.fa-first-aid:after { + content: "\10f479"; } + +.fad.fa-fish:after { + content: "\10f578"; } + +.fad.fa-fish-cooked:after { + content: "\10f7fe"; } + +.fad.fa-fist-raised:after { + content: "\10f6de"; } + +.fad.fa-flag:after { + content: "\10f024"; } + +.fad.fa-flag-alt:after { + content: "\10f74c"; } + +.fad.fa-flag-checkered:after { + content: "\10f11e"; } + +.fad.fa-flag-usa:after { + content: "\10f74d"; } + +.fad.fa-flame:after { + content: "\10f6df"; } + +.fad.fa-flashlight:after { + content: "\10f8b8"; } + +.fad.fa-flask:after { + content: "\10f0c3"; } + +.fad.fa-flask-poison:after { + content: "\10f6e0"; } + +.fad.fa-flask-potion:after { + content: "\10f6e1"; } + +.fad.fa-flower:after { + content: "\10f7ff"; } + +.fad.fa-flower-daffodil:after { + content: "\10f800"; } + +.fad.fa-flower-tulip:after { + content: "\10f801"; } + +.fad.fa-flushed:after { + content: "\10f579"; } + +.fad.fa-flute:after { + content: "\10f8b9"; } + +.fad.fa-flux-capacitor:after { + content: "\10f8ba"; } + +.fad.fa-fog:after { + content: "\10f74e"; } + +.fad.fa-folder:after { + content: "\10f07b"; } + +.fad.fa-folder-download:after { + content: "\10e053"; } + +.fad.fa-folder-minus:after { + content: "\10f65d"; } + +.fad.fa-folder-open:after { + content: "\10f07c"; } + +.fad.fa-folder-plus:after { + content: "\10f65e"; } + +.fad.fa-folder-times:after { + content: "\10f65f"; } + +.fad.fa-folder-tree:after { + content: "\10f802"; } + +.fad.fa-folder-upload:after { + content: "\10e054"; } + +.fad.fa-folders:after { + content: "\10f660"; } + +.fad.fa-font:after { + content: "\10f031"; } + +.fad.fa-font-awesome-logo-full:after { + content: "\10f4e6"; } + +.fad.fa-font-case:after { + content: "\10f866"; } + +.fad.fa-football-ball:after { + content: "\10f44e"; } + +.fad.fa-football-helmet:after { + content: "\10f44f"; } + +.fad.fa-forklift:after { + content: "\10f47a"; } + +.fad.fa-forward:after { + content: "\10f04e"; } + +.fad.fa-fragile:after { + content: "\10f4bb"; } + +.fad.fa-french-fries:after { + content: "\10f803"; } + +.fad.fa-frog:after { + content: "\10f52e"; } + +.fad.fa-frosty-head:after { + content: "\10f79b"; } + +.fad.fa-frown:after { + content: "\10f119"; } + +.fad.fa-frown-open:after { + content: "\10f57a"; } + +.fad.fa-function:after { + content: "\10f661"; } + +.fad.fa-funnel-dollar:after { + content: "\10f662"; } + +.fad.fa-futbol:after { + content: "\10f1e3"; } + +.fad.fa-galaxy:after { + content: "\10e008"; } + +.fad.fa-game-board:after { + content: "\10f867"; } + +.fad.fa-game-board-alt:after { + content: "\10f868"; } + +.fad.fa-game-console-handheld:after { + content: "\10f8bb"; } + +.fad.fa-gamepad:after { + content: "\10f11b"; } + +.fad.fa-gamepad-alt:after { + content: "\10f8bc"; } + +.fad.fa-garage:after { + content: "\10e009"; } + +.fad.fa-garage-car:after { + content: "\10e00a"; } + +.fad.fa-garage-open:after { + content: "\10e00b"; } + +.fad.fa-gas-pump:after { + content: "\10f52f"; } + +.fad.fa-gas-pump-slash:after { + content: "\10f5f4"; } + +.fad.fa-gavel:after { + content: "\10f0e3"; } + +.fad.fa-gem:after { + content: "\10f3a5"; } + +.fad.fa-genderless:after { + content: "\10f22d"; } + +.fad.fa-ghost:after { + content: "\10f6e2"; } + +.fad.fa-gift:after { + content: "\10f06b"; } + +.fad.fa-gift-card:after { + content: "\10f663"; } + +.fad.fa-gifts:after { + content: "\10f79c"; } + +.fad.fa-gingerbread-man:after { + content: "\10f79d"; } + +.fad.fa-glass:after { + content: "\10f804"; } + +.fad.fa-glass-champagne:after { + content: "\10f79e"; } + +.fad.fa-glass-cheers:after { + content: "\10f79f"; } + +.fad.fa-glass-citrus:after { + content: "\10f869"; } + +.fad.fa-glass-martini:after { + content: "\10f000"; } + +.fad.fa-glass-martini-alt:after { + content: "\10f57b"; } + +.fad.fa-glass-whiskey:after { + content: "\10f7a0"; } + +.fad.fa-glass-whiskey-rocks:after { + content: "\10f7a1"; } + +.fad.fa-glasses:after { + content: "\10f530"; } + +.fad.fa-glasses-alt:after { + content: "\10f5f5"; } + +.fad.fa-globe:after { + content: "\10f0ac"; } + +.fad.fa-globe-africa:after { + content: "\10f57c"; } + +.fad.fa-globe-americas:after { + content: "\10f57d"; } + +.fad.fa-globe-asia:after { + content: "\10f57e"; } + +.fad.fa-globe-europe:after { + content: "\10f7a2"; } + +.fad.fa-globe-snow:after { + content: "\10f7a3"; } + +.fad.fa-globe-stand:after { + content: "\10f5f6"; } + +.fad.fa-golf-ball:after { + content: "\10f450"; } + +.fad.fa-golf-club:after { + content: "\10f451"; } + +.fad.fa-gopuram:after { + content: "\10f664"; } + +.fad.fa-graduation-cap:after { + content: "\10f19d"; } + +.fad.fa-gramophone:after { + content: "\10f8bd"; } + +.fad.fa-greater-than:after { + content: "\10f531"; } + +.fad.fa-greater-than-equal:after { + content: "\10f532"; } + +.fad.fa-grimace:after { + content: "\10f57f"; } + +.fad.fa-grin:after { + content: "\10f580"; } + +.fad.fa-grin-alt:after { + content: "\10f581"; } + +.fad.fa-grin-beam:after { + content: "\10f582"; } + +.fad.fa-grin-beam-sweat:after { + content: "\10f583"; } + +.fad.fa-grin-hearts:after { + content: "\10f584"; } + +.fad.fa-grin-squint:after { + content: "\10f585"; } + +.fad.fa-grin-squint-tears:after { + content: "\10f586"; } + +.fad.fa-grin-stars:after { + content: "\10f587"; } + +.fad.fa-grin-tears:after { + content: "\10f588"; } + +.fad.fa-grin-tongue:after { + content: "\10f589"; } + +.fad.fa-grin-tongue-squint:after { + content: "\10f58a"; } + +.fad.fa-grin-tongue-wink:after { + content: "\10f58b"; } + +.fad.fa-grin-wink:after { + content: "\10f58c"; } + +.fad.fa-grip-horizontal:after { + content: "\10f58d"; } + +.fad.fa-grip-lines:after { + content: "\10f7a4"; } + +.fad.fa-grip-lines-vertical:after { + content: "\10f7a5"; } + +.fad.fa-grip-vertical:after { + content: "\10f58e"; } + +.fad.fa-guitar:after { + content: "\10f7a6"; } + +.fad.fa-guitar-electric:after { + content: "\10f8be"; } + +.fad.fa-guitars:after { + content: "\10f8bf"; } + +.fad.fa-h-square:after { + content: "\10f0fd"; } + +.fad.fa-h1:after { + content: "\10f313"; } + +.fad.fa-h2:after { + content: "\10f314"; } + +.fad.fa-h3:after { + content: "\10f315"; } + +.fad.fa-h4:after { + content: "\10f86a"; } + +.fad.fa-hamburger:after { + content: "\10f805"; } + +.fad.fa-hammer:after { + content: "\10f6e3"; } + +.fad.fa-hammer-war:after { + content: "\10f6e4"; } + +.fad.fa-hamsa:after { + content: "\10f665"; } + +.fad.fa-hand-heart:after { + content: "\10f4bc"; } + +.fad.fa-hand-holding:after { + content: "\10f4bd"; } + +.fad.fa-hand-holding-box:after { + content: "\10f47b"; } + +.fad.fa-hand-holding-heart:after { + content: "\10f4be"; } + +.fad.fa-hand-holding-magic:after { + content: "\10f6e5"; } + +.fad.fa-hand-holding-medical:after { + content: "\10e05c"; } + +.fad.fa-hand-holding-seedling:after { + content: "\10f4bf"; } + +.fad.fa-hand-holding-usd:after { + content: "\10f4c0"; } + +.fad.fa-hand-holding-water:after { + content: "\10f4c1"; } + +.fad.fa-hand-lizard:after { + content: "\10f258"; } + +.fad.fa-hand-middle-finger:after { + content: "\10f806"; } + +.fad.fa-hand-paper:after { + content: "\10f256"; } + +.fad.fa-hand-peace:after { + content: "\10f25b"; } + +.fad.fa-hand-point-down:after { + content: "\10f0a7"; } + +.fad.fa-hand-point-left:after { + content: "\10f0a5"; } + +.fad.fa-hand-point-right:after { + content: "\10f0a4"; } + +.fad.fa-hand-point-up:after { + content: "\10f0a6"; } + +.fad.fa-hand-pointer:after { + content: "\10f25a"; } + +.fad.fa-hand-receiving:after { + content: "\10f47c"; } + +.fad.fa-hand-rock:after { + content: "\10f255"; } + +.fad.fa-hand-scissors:after { + content: "\10f257"; } + +.fad.fa-hand-sparkles:after { + content: "\10e05d"; } + +.fad.fa-hand-spock:after { + content: "\10f259"; } + +.fad.fa-hands:after { + content: "\10f4c2"; } + +.fad.fa-hands-heart:after { + content: "\10f4c3"; } + +.fad.fa-hands-helping:after { + content: "\10f4c4"; } + +.fad.fa-hands-usd:after { + content: "\10f4c5"; } + +.fad.fa-hands-wash:after { + content: "\10e05e"; } + +.fad.fa-handshake:after { + content: "\10f2b5"; } + +.fad.fa-handshake-alt:after { + content: "\10f4c6"; } + +.fad.fa-handshake-alt-slash:after { + content: "\10e05f"; } + +.fad.fa-handshake-slash:after { + content: "\10e060"; } + +.fad.fa-hanukiah:after { + content: "\10f6e6"; } + +.fad.fa-hard-hat:after { + content: "\10f807"; } + +.fad.fa-hashtag:after { + content: "\10f292"; } + +.fad.fa-hat-chef:after { + content: "\10f86b"; } + +.fad.fa-hat-cowboy:after { + content: "\10f8c0"; } + +.fad.fa-hat-cowboy-side:after { + content: "\10f8c1"; } + +.fad.fa-hat-santa:after { + content: "\10f7a7"; } + +.fad.fa-hat-winter:after { + content: "\10f7a8"; } + +.fad.fa-hat-witch:after { + content: "\10f6e7"; } + +.fad.fa-hat-wizard:after { + content: "\10f6e8"; } + +.fad.fa-hdd:after { + content: "\10f0a0"; } + +.fad.fa-head-side:after { + content: "\10f6e9"; } + +.fad.fa-head-side-brain:after { + content: "\10f808"; } + +.fad.fa-head-side-cough:after { + content: "\10e061"; } + +.fad.fa-head-side-cough-slash:after { + content: "\10e062"; } + +.fad.fa-head-side-headphones:after { + content: "\10f8c2"; } + +.fad.fa-head-side-mask:after { + content: "\10e063"; } + +.fad.fa-head-side-medical:after { + content: "\10f809"; } + +.fad.fa-head-side-virus:after { + content: "\10e064"; } + +.fad.fa-head-vr:after { + content: "\10f6ea"; } + +.fad.fa-heading:after { + content: "\10f1dc"; } + +.fad.fa-headphones:after { + content: "\10f025"; } + +.fad.fa-headphones-alt:after { + content: "\10f58f"; } + +.fad.fa-headset:after { + content: "\10f590"; } + +.fad.fa-heart:after { + content: "\10f004"; } + +.fad.fa-heart-broken:after { + content: "\10f7a9"; } + +.fad.fa-heart-circle:after { + content: "\10f4c7"; } + +.fad.fa-heart-rate:after { + content: "\10f5f8"; } + +.fad.fa-heart-square:after { + content: "\10f4c8"; } + +.fad.fa-heartbeat:after { + content: "\10f21e"; } + +.fad.fa-heat:after { + content: "\10e00c"; } + +.fad.fa-helicopter:after { + content: "\10f533"; } + +.fad.fa-helmet-battle:after { + content: "\10f6eb"; } + +.fad.fa-hexagon:after { + content: "\10f312"; } + +.fad.fa-highlighter:after { + content: "\10f591"; } + +.fad.fa-hiking:after { + content: "\10f6ec"; } + +.fad.fa-hippo:after { + content: "\10f6ed"; } + +.fad.fa-history:after { + content: "\10f1da"; } + +.fad.fa-hockey-mask:after { + content: "\10f6ee"; } + +.fad.fa-hockey-puck:after { + content: "\10f453"; } + +.fad.fa-hockey-sticks:after { + content: "\10f454"; } + +.fad.fa-holly-berry:after { + content: "\10f7aa"; } + +.fad.fa-home:after { + content: "\10f015"; } + +.fad.fa-home-alt:after { + content: "\10f80a"; } + +.fad.fa-home-heart:after { + content: "\10f4c9"; } + +.fad.fa-home-lg:after { + content: "\10f80b"; } + +.fad.fa-home-lg-alt:after { + content: "\10f80c"; } + +.fad.fa-hood-cloak:after { + content: "\10f6ef"; } + +.fad.fa-horizontal-rule:after { + content: "\10f86c"; } + +.fad.fa-horse:after { + content: "\10f6f0"; } + +.fad.fa-horse-head:after { + content: "\10f7ab"; } + +.fad.fa-horse-saddle:after { + content: "\10f8c3"; } + +.fad.fa-hospital:after { + content: "\10f0f8"; } + +.fad.fa-hospital-alt:after { + content: "\10f47d"; } + +.fad.fa-hospital-symbol:after { + content: "\10f47e"; } + +.fad.fa-hospital-user:after { + content: "\10f80d"; } + +.fad.fa-hospitals:after { + content: "\10f80e"; } + +.fad.fa-hot-tub:after { + content: "\10f593"; } + +.fad.fa-hotdog:after { + content: "\10f80f"; } + +.fad.fa-hotel:after { + content: "\10f594"; } + +.fad.fa-hourglass:after { + content: "\10f254"; } + +.fad.fa-hourglass-end:after { + content: "\10f253"; } + +.fad.fa-hourglass-half:after { + content: "\10f252"; } + +.fad.fa-hourglass-start:after { + content: "\10f251"; } + +.fad.fa-house:after { + content: "\10e00d"; } + +.fad.fa-house-damage:after { + content: "\10f6f1"; } + +.fad.fa-house-day:after { + content: "\10e00e"; } + +.fad.fa-house-flood:after { + content: "\10f74f"; } + +.fad.fa-house-leave:after { + content: "\10e00f"; } + +.fad.fa-house-night:after { + content: "\10e010"; } + +.fad.fa-house-return:after { + content: "\10e011"; } + +.fad.fa-house-signal:after { + content: "\10e012"; } + +.fad.fa-house-user:after { + content: "\10e065"; } + +.fad.fa-hryvnia:after { + content: "\10f6f2"; } + +.fad.fa-humidity:after { + content: "\10f750"; } + +.fad.fa-hurricane:after { + content: "\10f751"; } + +.fad.fa-i-cursor:after { + content: "\10f246"; } + +.fad.fa-ice-cream:after { + content: "\10f810"; } + +.fad.fa-ice-skate:after { + content: "\10f7ac"; } + +.fad.fa-icicles:after { + content: "\10f7ad"; } + +.fad.fa-icons:after { + content: "\10f86d"; } + +.fad.fa-icons-alt:after { + content: "\10f86e"; } + +.fad.fa-id-badge:after { + content: "\10f2c1"; } + +.fad.fa-id-card:after { + content: "\10f2c2"; } + +.fad.fa-id-card-alt:after { + content: "\10f47f"; } + +.fad.fa-igloo:after { + content: "\10f7ae"; } + +.fad.fa-image:after { + content: "\10f03e"; } + +.fad.fa-image-polaroid:after { + content: "\10f8c4"; } + +.fad.fa-images:after { + content: "\10f302"; } + +.fad.fa-inbox:after { + content: "\10f01c"; } + +.fad.fa-inbox-in:after { + content: "\10f310"; } + +.fad.fa-inbox-out:after { + content: "\10f311"; } + +.fad.fa-indent:after { + content: "\10f03c"; } + +.fad.fa-industry:after { + content: "\10f275"; } + +.fad.fa-industry-alt:after { + content: "\10f3b3"; } + +.fad.fa-infinity:after { + content: "\10f534"; } + +.fad.fa-info:after { + content: "\10f129"; } + +.fad.fa-info-circle:after { + content: "\10f05a"; } + +.fad.fa-info-square:after { + content: "\10f30f"; } + +.fad.fa-inhaler:after { + content: "\10f5f9"; } + +.fad.fa-integral:after { + content: "\10f667"; } + +.fad.fa-intersection:after { + content: "\10f668"; } + +.fad.fa-inventory:after { + content: "\10f480"; } + +.fad.fa-island-tropical:after { + content: "\10f811"; } + +.fad.fa-italic:after { + content: "\10f033"; } + +.fad.fa-jack-o-lantern:after { + content: "\10f30e"; } + +.fad.fa-jedi:after { + content: "\10f669"; } + +.fad.fa-joint:after { + content: "\10f595"; } + +.fad.fa-journal-whills:after { + content: "\10f66a"; } + +.fad.fa-joystick:after { + content: "\10f8c5"; } + +.fad.fa-jug:after { + content: "\10f8c6"; } + +.fad.fa-kaaba:after { + content: "\10f66b"; } + +.fad.fa-kazoo:after { + content: "\10f8c7"; } + +.fad.fa-kerning:after { + content: "\10f86f"; } + +.fad.fa-key:after { + content: "\10f084"; } + +.fad.fa-key-skeleton:after { + content: "\10f6f3"; } + +.fad.fa-keyboard:after { + content: "\10f11c"; } + +.fad.fa-keynote:after { + content: "\10f66c"; } + +.fad.fa-khanda:after { + content: "\10f66d"; } + +.fad.fa-kidneys:after { + content: "\10f5fb"; } + +.fad.fa-kiss:after { + content: "\10f596"; } + +.fad.fa-kiss-beam:after { + content: "\10f597"; } + +.fad.fa-kiss-wink-heart:after { + content: "\10f598"; } + +.fad.fa-kite:after { + content: "\10f6f4"; } + +.fad.fa-kiwi-bird:after { + content: "\10f535"; } + +.fad.fa-knife-kitchen:after { + content: "\10f6f5"; } + +.fad.fa-lambda:after { + content: "\10f66e"; } + +.fad.fa-lamp:after { + content: "\10f4ca"; } + +.fad.fa-lamp-desk:after { + content: "\10e014"; } + +.fad.fa-lamp-floor:after { + content: "\10e015"; } + +.fad.fa-landmark:after { + content: "\10f66f"; } + +.fad.fa-landmark-alt:after { + content: "\10f752"; } + +.fad.fa-language:after { + content: "\10f1ab"; } + +.fad.fa-laptop:after { + content: "\10f109"; } + +.fad.fa-laptop-code:after { + content: "\10f5fc"; } + +.fad.fa-laptop-house:after { + content: "\10e066"; } + +.fad.fa-laptop-medical:after { + content: "\10f812"; } + +.fad.fa-lasso:after { + content: "\10f8c8"; } + +.fad.fa-laugh:after { + content: "\10f599"; } + +.fad.fa-laugh-beam:after { + content: "\10f59a"; } + +.fad.fa-laugh-squint:after { + content: "\10f59b"; } + +.fad.fa-laugh-wink:after { + content: "\10f59c"; } + +.fad.fa-layer-group:after { + content: "\10f5fd"; } + +.fad.fa-layer-minus:after { + content: "\10f5fe"; } + +.fad.fa-layer-plus:after { + content: "\10f5ff"; } + +.fad.fa-leaf:after { + content: "\10f06c"; } + +.fad.fa-leaf-heart:after { + content: "\10f4cb"; } + +.fad.fa-leaf-maple:after { + content: "\10f6f6"; } + +.fad.fa-leaf-oak:after { + content: "\10f6f7"; } + +.fad.fa-lemon:after { + content: "\10f094"; } + +.fad.fa-less-than:after { + content: "\10f536"; } + +.fad.fa-less-than-equal:after { + content: "\10f537"; } + +.fad.fa-level-down:after { + content: "\10f149"; } + +.fad.fa-level-down-alt:after { + content: "\10f3be"; } + +.fad.fa-level-up:after { + content: "\10f148"; } + +.fad.fa-level-up-alt:after { + content: "\10f3bf"; } + +.fad.fa-life-ring:after { + content: "\10f1cd"; } + +.fad.fa-light-ceiling:after { + content: "\10e016"; } + +.fad.fa-light-switch:after { + content: "\10e017"; } + +.fad.fa-light-switch-off:after { + content: "\10e018"; } + +.fad.fa-light-switch-on:after { + content: "\10e019"; } + +.fad.fa-lightbulb:after { + content: "\10f0eb"; } + +.fad.fa-lightbulb-dollar:after { + content: "\10f670"; } + +.fad.fa-lightbulb-exclamation:after { + content: "\10f671"; } + +.fad.fa-lightbulb-on:after { + content: "\10f672"; } + +.fad.fa-lightbulb-slash:after { + content: "\10f673"; } + +.fad.fa-lights-holiday:after { + content: "\10f7b2"; } + +.fad.fa-line-columns:after { + content: "\10f870"; } + +.fad.fa-line-height:after { + content: "\10f871"; } + +.fad.fa-link:after { + content: "\10f0c1"; } + +.fad.fa-lips:after { + content: "\10f600"; } + +.fad.fa-lira-sign:after { + content: "\10f195"; } + +.fad.fa-list:after { + content: "\10f03a"; } + +.fad.fa-list-alt:after { + content: "\10f022"; } + +.fad.fa-list-music:after { + content: "\10f8c9"; } + +.fad.fa-list-ol:after { + content: "\10f0cb"; } + +.fad.fa-list-ul:after { + content: "\10f0ca"; } + +.fad.fa-location:after { + content: "\10f601"; } + +.fad.fa-location-arrow:after { + content: "\10f124"; } + +.fad.fa-location-circle:after { + content: "\10f602"; } + +.fad.fa-location-slash:after { + content: "\10f603"; } + +.fad.fa-lock:after { + content: "\10f023"; } + +.fad.fa-lock-alt:after { + content: "\10f30d"; } + +.fad.fa-lock-open:after { + content: "\10f3c1"; } + +.fad.fa-lock-open-alt:after { + content: "\10f3c2"; } + +.fad.fa-long-arrow-alt-down:after { + content: "\10f309"; } + +.fad.fa-long-arrow-alt-left:after { + content: "\10f30a"; } + +.fad.fa-long-arrow-alt-right:after { + content: "\10f30b"; } + +.fad.fa-long-arrow-alt-up:after { + content: "\10f30c"; } + +.fad.fa-long-arrow-down:after { + content: "\10f175"; } + +.fad.fa-long-arrow-left:after { + content: "\10f177"; } + +.fad.fa-long-arrow-right:after { + content: "\10f178"; } + +.fad.fa-long-arrow-up:after { + content: "\10f176"; } + +.fad.fa-loveseat:after { + content: "\10f4cc"; } + +.fad.fa-low-vision:after { + content: "\10f2a8"; } + +.fad.fa-luchador:after { + content: "\10f455"; } + +.fad.fa-luggage-cart:after { + content: "\10f59d"; } + +.fad.fa-lungs:after { + content: "\10f604"; } + +.fad.fa-lungs-virus:after { + content: "\10e067"; } + +.fad.fa-mace:after { + content: "\10f6f8"; } + +.fad.fa-magic:after { + content: "\10f0d0"; } + +.fad.fa-magnet:after { + content: "\10f076"; } + +.fad.fa-mail-bulk:after { + content: "\10f674"; } + +.fad.fa-mailbox:after { + content: "\10f813"; } + +.fad.fa-male:after { + content: "\10f183"; } + +.fad.fa-mandolin:after { + content: "\10f6f9"; } + +.fad.fa-map:after { + content: "\10f279"; } + +.fad.fa-map-marked:after { + content: "\10f59f"; } + +.fad.fa-map-marked-alt:after { + content: "\10f5a0"; } + +.fad.fa-map-marker:after { + content: "\10f041"; } + +.fad.fa-map-marker-alt:after { + content: "\10f3c5"; } + +.fad.fa-map-marker-alt-slash:after { + content: "\10f605"; } + +.fad.fa-map-marker-check:after { + content: "\10f606"; } + +.fad.fa-map-marker-edit:after { + content: "\10f607"; } + +.fad.fa-map-marker-exclamation:after { + content: "\10f608"; } + +.fad.fa-map-marker-minus:after { + content: "\10f609"; } + +.fad.fa-map-marker-plus:after { + content: "\10f60a"; } + +.fad.fa-map-marker-question:after { + content: "\10f60b"; } + +.fad.fa-map-marker-slash:after { + content: "\10f60c"; } + +.fad.fa-map-marker-smile:after { + content: "\10f60d"; } + +.fad.fa-map-marker-times:after { + content: "\10f60e"; } + +.fad.fa-map-pin:after { + content: "\10f276"; } + +.fad.fa-map-signs:after { + content: "\10f277"; } + +.fad.fa-marker:after { + content: "\10f5a1"; } + +.fad.fa-mars:after { + content: "\10f222"; } + +.fad.fa-mars-double:after { + content: "\10f227"; } + +.fad.fa-mars-stroke:after { + content: "\10f229"; } + +.fad.fa-mars-stroke-h:after { + content: "\10f22b"; } + +.fad.fa-mars-stroke-v:after { + content: "\10f22a"; } + +.fad.fa-mask:after { + content: "\10f6fa"; } + +.fad.fa-meat:after { + content: "\10f814"; } + +.fad.fa-medal:after { + content: "\10f5a2"; } + +.fad.fa-medkit:after { + content: "\10f0fa"; } + +.fad.fa-megaphone:after { + content: "\10f675"; } + +.fad.fa-meh:after { + content: "\10f11a"; } + +.fad.fa-meh-blank:after { + content: "\10f5a4"; } + +.fad.fa-meh-rolling-eyes:after { + content: "\10f5a5"; } + +.fad.fa-memory:after { + content: "\10f538"; } + +.fad.fa-menorah:after { + content: "\10f676"; } + +.fad.fa-mercury:after { + content: "\10f223"; } + +.fad.fa-meteor:after { + content: "\10f753"; } + +.fad.fa-microchip:after { + content: "\10f2db"; } + +.fad.fa-microphone:after { + content: "\10f130"; } + +.fad.fa-microphone-alt:after { + content: "\10f3c9"; } + +.fad.fa-microphone-alt-slash:after { + content: "\10f539"; } + +.fad.fa-microphone-slash:after { + content: "\10f131"; } + +.fad.fa-microphone-stand:after { + content: "\10f8cb"; } + +.fad.fa-microscope:after { + content: "\10f610"; } + +.fad.fa-microwave:after { + content: "\10e01b"; } + +.fad.fa-mind-share:after { + content: "\10f677"; } + +.fad.fa-minus:after { + content: "\10f068"; } + +.fad.fa-minus-circle:after { + content: "\10f056"; } + +.fad.fa-minus-hexagon:after { + content: "\10f307"; } + +.fad.fa-minus-octagon:after { + content: "\10f308"; } + +.fad.fa-minus-square:after { + content: "\10f146"; } + +.fad.fa-mistletoe:after { + content: "\10f7b4"; } + +.fad.fa-mitten:after { + content: "\10f7b5"; } + +.fad.fa-mobile:after { + content: "\10f10b"; } + +.fad.fa-mobile-alt:after { + content: "\10f3cd"; } + +.fad.fa-mobile-android:after { + content: "\10f3ce"; } + +.fad.fa-mobile-android-alt:after { + content: "\10f3cf"; } + +.fad.fa-money-bill:after { + content: "\10f0d6"; } + +.fad.fa-money-bill-alt:after { + content: "\10f3d1"; } + +.fad.fa-money-bill-wave:after { + content: "\10f53a"; } + +.fad.fa-money-bill-wave-alt:after { + content: "\10f53b"; } + +.fad.fa-money-check:after { + content: "\10f53c"; } + +.fad.fa-money-check-alt:after { + content: "\10f53d"; } + +.fad.fa-money-check-edit:after { + content: "\10f872"; } + +.fad.fa-money-check-edit-alt:after { + content: "\10f873"; } + +.fad.fa-monitor-heart-rate:after { + content: "\10f611"; } + +.fad.fa-monkey:after { + content: "\10f6fb"; } + +.fad.fa-monument:after { + content: "\10f5a6"; } + +.fad.fa-moon:after { + content: "\10f186"; } + +.fad.fa-moon-cloud:after { + content: "\10f754"; } + +.fad.fa-moon-stars:after { + content: "\10f755"; } + +.fad.fa-mortar-pestle:after { + content: "\10f5a7"; } + +.fad.fa-mosque:after { + content: "\10f678"; } + +.fad.fa-motorcycle:after { + content: "\10f21c"; } + +.fad.fa-mountain:after { + content: "\10f6fc"; } + +.fad.fa-mountains:after { + content: "\10f6fd"; } + +.fad.fa-mouse:after { + content: "\10f8cc"; } + +.fad.fa-mouse-alt:after { + content: "\10f8cd"; } + +.fad.fa-mouse-pointer:after { + content: "\10f245"; } + +.fad.fa-mp3-player:after { + content: "\10f8ce"; } + +.fad.fa-mug:after { + content: "\10f874"; } + +.fad.fa-mug-hot:after { + content: "\10f7b6"; } + +.fad.fa-mug-marshmallows:after { + content: "\10f7b7"; } + +.fad.fa-mug-tea:after { + content: "\10f875"; } + +.fad.fa-music:after { + content: "\10f001"; } + +.fad.fa-music-alt:after { + content: "\10f8cf"; } + +.fad.fa-music-alt-slash:after { + content: "\10f8d0"; } + +.fad.fa-music-slash:after { + content: "\10f8d1"; } + +.fad.fa-narwhal:after { + content: "\10f6fe"; } + +.fad.fa-network-wired:after { + content: "\10f6ff"; } + +.fad.fa-neuter:after { + content: "\10f22c"; } + +.fad.fa-newspaper:after { + content: "\10f1ea"; } + +.fad.fa-not-equal:after { + content: "\10f53e"; } + +.fad.fa-notes-medical:after { + content: "\10f481"; } + +.fad.fa-object-group:after { + content: "\10f247"; } + +.fad.fa-object-ungroup:after { + content: "\10f248"; } + +.fad.fa-octagon:after { + content: "\10f306"; } + +.fad.fa-oil-can:after { + content: "\10f613"; } + +.fad.fa-oil-temp:after { + content: "\10f614"; } + +.fad.fa-om:after { + content: "\10f679"; } + +.fad.fa-omega:after { + content: "\10f67a"; } + +.fad.fa-ornament:after { + content: "\10f7b8"; } + +.fad.fa-otter:after { + content: "\10f700"; } + +.fad.fa-outdent:after { + content: "\10f03b"; } + +.fad.fa-outlet:after { + content: "\10e01c"; } + +.fad.fa-oven:after { + content: "\10e01d"; } + +.fad.fa-overline:after { + content: "\10f876"; } + +.fad.fa-page-break:after { + content: "\10f877"; } + +.fad.fa-pager:after { + content: "\10f815"; } + +.fad.fa-paint-brush:after { + content: "\10f1fc"; } + +.fad.fa-paint-brush-alt:after { + content: "\10f5a9"; } + +.fad.fa-paint-roller:after { + content: "\10f5aa"; } + +.fad.fa-palette:after { + content: "\10f53f"; } + +.fad.fa-pallet:after { + content: "\10f482"; } + +.fad.fa-pallet-alt:after { + content: "\10f483"; } + +.fad.fa-paper-plane:after { + content: "\10f1d8"; } + +.fad.fa-paperclip:after { + content: "\10f0c6"; } + +.fad.fa-parachute-box:after { + content: "\10f4cd"; } + +.fad.fa-paragraph:after { + content: "\10f1dd"; } + +.fad.fa-paragraph-rtl:after { + content: "\10f878"; } + +.fad.fa-parking:after { + content: "\10f540"; } + +.fad.fa-parking-circle:after { + content: "\10f615"; } + +.fad.fa-parking-circle-slash:after { + content: "\10f616"; } + +.fad.fa-parking-slash:after { + content: "\10f617"; } + +.fad.fa-passport:after { + content: "\10f5ab"; } + +.fad.fa-pastafarianism:after { + content: "\10f67b"; } + +.fad.fa-paste:after { + content: "\10f0ea"; } + +.fad.fa-pause:after { + content: "\10f04c"; } + +.fad.fa-pause-circle:after { + content: "\10f28b"; } + +.fad.fa-paw:after { + content: "\10f1b0"; } + +.fad.fa-paw-alt:after { + content: "\10f701"; } + +.fad.fa-paw-claws:after { + content: "\10f702"; } + +.fad.fa-peace:after { + content: "\10f67c"; } + +.fad.fa-pegasus:after { + content: "\10f703"; } + +.fad.fa-pen:after { + content: "\10f304"; } + +.fad.fa-pen-alt:after { + content: "\10f305"; } + +.fad.fa-pen-fancy:after { + content: "\10f5ac"; } + +.fad.fa-pen-nib:after { + content: "\10f5ad"; } + +.fad.fa-pen-square:after { + content: "\10f14b"; } + +.fad.fa-pencil:after { + content: "\10f040"; } + +.fad.fa-pencil-alt:after { + content: "\10f303"; } + +.fad.fa-pencil-paintbrush:after { + content: "\10f618"; } + +.fad.fa-pencil-ruler:after { + content: "\10f5ae"; } + +.fad.fa-pennant:after { + content: "\10f456"; } + +.fad.fa-people-arrows:after { + content: "\10e068"; } + +.fad.fa-people-carry:after { + content: "\10f4ce"; } + +.fad.fa-pepper-hot:after { + content: "\10f816"; } + +.fad.fa-percent:after { + content: "\10f295"; } + +.fad.fa-percentage:after { + content: "\10f541"; } + +.fad.fa-person-booth:after { + content: "\10f756"; } + +.fad.fa-person-carry:after { + content: "\10f4cf"; } + +.fad.fa-person-dolly:after { + content: "\10f4d0"; } + +.fad.fa-person-dolly-empty:after { + content: "\10f4d1"; } + +.fad.fa-person-sign:after { + content: "\10f757"; } + +.fad.fa-phone:after { + content: "\10f095"; } + +.fad.fa-phone-alt:after { + content: "\10f879"; } + +.fad.fa-phone-laptop:after { + content: "\10f87a"; } + +.fad.fa-phone-office:after { + content: "\10f67d"; } + +.fad.fa-phone-plus:after { + content: "\10f4d2"; } + +.fad.fa-phone-rotary:after { + content: "\10f8d3"; } + +.fad.fa-phone-slash:after { + content: "\10f3dd"; } + +.fad.fa-phone-square:after { + content: "\10f098"; } + +.fad.fa-phone-square-alt:after { + content: "\10f87b"; } + +.fad.fa-phone-volume:after { + content: "\10f2a0"; } + +.fad.fa-photo-video:after { + content: "\10f87c"; } + +.fad.fa-pi:after { + content: "\10f67e"; } + +.fad.fa-piano:after { + content: "\10f8d4"; } + +.fad.fa-piano-keyboard:after { + content: "\10f8d5"; } + +.fad.fa-pie:after { + content: "\10f705"; } + +.fad.fa-pig:after { + content: "\10f706"; } + +.fad.fa-piggy-bank:after { + content: "\10f4d3"; } + +.fad.fa-pills:after { + content: "\10f484"; } + +.fad.fa-pizza:after { + content: "\10f817"; } + +.fad.fa-pizza-slice:after { + content: "\10f818"; } + +.fad.fa-place-of-worship:after { + content: "\10f67f"; } + +.fad.fa-plane:after { + content: "\10f072"; } + +.fad.fa-plane-alt:after { + content: "\10f3de"; } + +.fad.fa-plane-arrival:after { + content: "\10f5af"; } + +.fad.fa-plane-departure:after { + content: "\10f5b0"; } + +.fad.fa-plane-slash:after { + content: "\10e069"; } + +.fad.fa-planet-moon:after { + content: "\10e01f"; } + +.fad.fa-planet-ringed:after { + content: "\10e020"; } + +.fad.fa-play:after { + content: "\10f04b"; } + +.fad.fa-play-circle:after { + content: "\10f144"; } + +.fad.fa-plug:after { + content: "\10f1e6"; } + +.fad.fa-plus:after { + content: "\10f067"; } + +.fad.fa-plus-circle:after { + content: "\10f055"; } + +.fad.fa-plus-hexagon:after { + content: "\10f300"; } + +.fad.fa-plus-octagon:after { + content: "\10f301"; } + +.fad.fa-plus-square:after { + content: "\10f0fe"; } + +.fad.fa-podcast:after { + content: "\10f2ce"; } + +.fad.fa-podium:after { + content: "\10f680"; } + +.fad.fa-podium-star:after { + content: "\10f758"; } + +.fad.fa-police-box:after { + content: "\10e021"; } + +.fad.fa-poll:after { + content: "\10f681"; } + +.fad.fa-poll-h:after { + content: "\10f682"; } + +.fad.fa-poll-people:after { + content: "\10f759"; } + +.fad.fa-poo:after { + content: "\10f2fe"; } + +.fad.fa-poo-storm:after { + content: "\10f75a"; } + +.fad.fa-poop:after { + content: "\10f619"; } + +.fad.fa-popcorn:after { + content: "\10f819"; } + +.fad.fa-portal-enter:after { + content: "\10e022"; } + +.fad.fa-portal-exit:after { + content: "\10e023"; } + +.fad.fa-portrait:after { + content: "\10f3e0"; } + +.fad.fa-pound-sign:after { + content: "\10f154"; } + +.fad.fa-power-off:after { + content: "\10f011"; } + +.fad.fa-pray:after { + content: "\10f683"; } + +.fad.fa-praying-hands:after { + content: "\10f684"; } + +.fad.fa-prescription:after { + content: "\10f5b1"; } + +.fad.fa-prescription-bottle:after { + content: "\10f485"; } + +.fad.fa-prescription-bottle-alt:after { + content: "\10f486"; } + +.fad.fa-presentation:after { + content: "\10f685"; } + +.fad.fa-print:after { + content: "\10f02f"; } + +.fad.fa-print-search:after { + content: "\10f81a"; } + +.fad.fa-print-slash:after { + content: "\10f686"; } + +.fad.fa-procedures:after { + content: "\10f487"; } + +.fad.fa-project-diagram:after { + content: "\10f542"; } + +.fad.fa-projector:after { + content: "\10f8d6"; } + +.fad.fa-pump-medical:after { + content: "\10e06a"; } + +.fad.fa-pump-soap:after { + content: "\10e06b"; } + +.fad.fa-pumpkin:after { + content: "\10f707"; } + +.fad.fa-puzzle-piece:after { + content: "\10f12e"; } + +.fad.fa-qrcode:after { + content: "\10f029"; } + +.fad.fa-question:after { + content: "\10f128"; } + +.fad.fa-question-circle:after { + content: "\10f059"; } + +.fad.fa-question-square:after { + content: "\10f2fd"; } + +.fad.fa-quidditch:after { + content: "\10f458"; } + +.fad.fa-quote-left:after { + content: "\10f10d"; } + +.fad.fa-quote-right:after { + content: "\10f10e"; } + +.fad.fa-quran:after { + content: "\10f687"; } + +.fad.fa-rabbit:after { + content: "\10f708"; } + +.fad.fa-rabbit-fast:after { + content: "\10f709"; } + +.fad.fa-racquet:after { + content: "\10f45a"; } + +.fad.fa-radar:after { + content: "\10e024"; } + +.fad.fa-radiation:after { + content: "\10f7b9"; } + +.fad.fa-radiation-alt:after { + content: "\10f7ba"; } + +.fad.fa-radio:after { + content: "\10f8d7"; } + +.fad.fa-radio-alt:after { + content: "\10f8d8"; } + +.fad.fa-rainbow:after { + content: "\10f75b"; } + +.fad.fa-raindrops:after { + content: "\10f75c"; } + +.fad.fa-ram:after { + content: "\10f70a"; } + +.fad.fa-ramp-loading:after { + content: "\10f4d4"; } + +.fad.fa-random:after { + content: "\10f074"; } + +.fad.fa-raygun:after { + content: "\10e025"; } + +.fad.fa-receipt:after { + content: "\10f543"; } + +.fad.fa-record-vinyl:after { + content: "\10f8d9"; } + +.fad.fa-rectangle-landscape:after { + content: "\10f2fa"; } + +.fad.fa-rectangle-portrait:after { + content: "\10f2fb"; } + +.fad.fa-rectangle-wide:after { + content: "\10f2fc"; } + +.fad.fa-recycle:after { + content: "\10f1b8"; } + +.fad.fa-redo:after { + content: "\10f01e"; } + +.fad.fa-redo-alt:after { + content: "\10f2f9"; } + +.fad.fa-refrigerator:after { + content: "\10e026"; } + +.fad.fa-registered:after { + content: "\10f25d"; } + +.fad.fa-remove-format:after { + content: "\10f87d"; } + +.fad.fa-repeat:after { + content: "\10f363"; } + +.fad.fa-repeat-1:after { + content: "\10f365"; } + +.fad.fa-repeat-1-alt:after { + content: "\10f366"; } + +.fad.fa-repeat-alt:after { + content: "\10f364"; } + +.fad.fa-reply:after { + content: "\10f3e5"; } + +.fad.fa-reply-all:after { + content: "\10f122"; } + +.fad.fa-republican:after { + content: "\10f75e"; } + +.fad.fa-restroom:after { + content: "\10f7bd"; } + +.fad.fa-retweet:after { + content: "\10f079"; } + +.fad.fa-retweet-alt:after { + content: "\10f361"; } + +.fad.fa-ribbon:after { + content: "\10f4d6"; } + +.fad.fa-ring:after { + content: "\10f70b"; } + +.fad.fa-rings-wedding:after { + content: "\10f81b"; } + +.fad.fa-road:after { + content: "\10f018"; } + +.fad.fa-robot:after { + content: "\10f544"; } + +.fad.fa-rocket:after { + content: "\10f135"; } + +.fad.fa-rocket-launch:after { + content: "\10e027"; } + +.fad.fa-route:after { + content: "\10f4d7"; } + +.fad.fa-route-highway:after { + content: "\10f61a"; } + +.fad.fa-route-interstate:after { + content: "\10f61b"; } + +.fad.fa-router:after { + content: "\10f8da"; } + +.fad.fa-rss:after { + content: "\10f09e"; } + +.fad.fa-rss-square:after { + content: "\10f143"; } + +.fad.fa-ruble-sign:after { + content: "\10f158"; } + +.fad.fa-ruler:after { + content: "\10f545"; } + +.fad.fa-ruler-combined:after { + content: "\10f546"; } + +.fad.fa-ruler-horizontal:after { + content: "\10f547"; } + +.fad.fa-ruler-triangle:after { + content: "\10f61c"; } + +.fad.fa-ruler-vertical:after { + content: "\10f548"; } + +.fad.fa-running:after { + content: "\10f70c"; } + +.fad.fa-rupee-sign:after { + content: "\10f156"; } + +.fad.fa-rv:after { + content: "\10f7be"; } + +.fad.fa-sack:after { + content: "\10f81c"; } + +.fad.fa-sack-dollar:after { + content: "\10f81d"; } + +.fad.fa-sad-cry:after { + content: "\10f5b3"; } + +.fad.fa-sad-tear:after { + content: "\10f5b4"; } + +.fad.fa-salad:after { + content: "\10f81e"; } + +.fad.fa-sandwich:after { + content: "\10f81f"; } + +.fad.fa-satellite:after { + content: "\10f7bf"; } + +.fad.fa-satellite-dish:after { + content: "\10f7c0"; } + +.fad.fa-sausage:after { + content: "\10f820"; } + +.fad.fa-save:after { + content: "\10f0c7"; } + +.fad.fa-sax-hot:after { + content: "\10f8db"; } + +.fad.fa-saxophone:after { + content: "\10f8dc"; } + +.fad.fa-scalpel:after { + content: "\10f61d"; } + +.fad.fa-scalpel-path:after { + content: "\10f61e"; } + +.fad.fa-scanner:after { + content: "\10f488"; } + +.fad.fa-scanner-image:after { + content: "\10f8f3"; } + +.fad.fa-scanner-keyboard:after { + content: "\10f489"; } + +.fad.fa-scanner-touchscreen:after { + content: "\10f48a"; } + +.fad.fa-scarecrow:after { + content: "\10f70d"; } + +.fad.fa-scarf:after { + content: "\10f7c1"; } + +.fad.fa-school:after { + content: "\10f549"; } + +.fad.fa-screwdriver:after { + content: "\10f54a"; } + +.fad.fa-scroll:after { + content: "\10f70e"; } + +.fad.fa-scroll-old:after { + content: "\10f70f"; } + +.fad.fa-scrubber:after { + content: "\10f2f8"; } + +.fad.fa-scythe:after { + content: "\10f710"; } + +.fad.fa-sd-card:after { + content: "\10f7c2"; } + +.fad.fa-search:after { + content: "\10f002"; } + +.fad.fa-search-dollar:after { + content: "\10f688"; } + +.fad.fa-search-location:after { + content: "\10f689"; } + +.fad.fa-search-minus:after { + content: "\10f010"; } + +.fad.fa-search-plus:after { + content: "\10f00e"; } + +.fad.fa-seedling:after { + content: "\10f4d8"; } + +.fad.fa-send-back:after { + content: "\10f87e"; } + +.fad.fa-send-backward:after { + content: "\10f87f"; } + +.fad.fa-sensor:after { + content: "\10e028"; } + +.fad.fa-sensor-alert:after { + content: "\10e029"; } + +.fad.fa-sensor-fire:after { + content: "\10e02a"; } + +.fad.fa-sensor-on:after { + content: "\10e02b"; } + +.fad.fa-sensor-smoke:after { + content: "\10e02c"; } + +.fad.fa-server:after { + content: "\10f233"; } + +.fad.fa-shapes:after { + content: "\10f61f"; } + +.fad.fa-share:after { + content: "\10f064"; } + +.fad.fa-share-all:after { + content: "\10f367"; } + +.fad.fa-share-alt:after { + content: "\10f1e0"; } + +.fad.fa-share-alt-square:after { + content: "\10f1e1"; } + +.fad.fa-share-square:after { + content: "\10f14d"; } + +.fad.fa-sheep:after { + content: "\10f711"; } + +.fad.fa-shekel-sign:after { + content: "\10f20b"; } + +.fad.fa-shield:after { + content: "\10f132"; } + +.fad.fa-shield-alt:after { + content: "\10f3ed"; } + +.fad.fa-shield-check:after { + content: "\10f2f7"; } + +.fad.fa-shield-cross:after { + content: "\10f712"; } + +.fad.fa-shield-virus:after { + content: "\10e06c"; } + +.fad.fa-ship:after { + content: "\10f21a"; } + +.fad.fa-shipping-fast:after { + content: "\10f48b"; } + +.fad.fa-shipping-timed:after { + content: "\10f48c"; } + +.fad.fa-shish-kebab:after { + content: "\10f821"; } + +.fad.fa-shoe-prints:after { + content: "\10f54b"; } + +.fad.fa-shopping-bag:after { + content: "\10f290"; } + +.fad.fa-shopping-basket:after { + content: "\10f291"; } + +.fad.fa-shopping-cart:after { + content: "\10f07a"; } + +.fad.fa-shovel:after { + content: "\10f713"; } + +.fad.fa-shovel-snow:after { + content: "\10f7c3"; } + +.fad.fa-shower:after { + content: "\10f2cc"; } + +.fad.fa-shredder:after { + content: "\10f68a"; } + +.fad.fa-shuttle-van:after { + content: "\10f5b6"; } + +.fad.fa-shuttlecock:after { + content: "\10f45b"; } + +.fad.fa-sickle:after { + content: "\10f822"; } + +.fad.fa-sigma:after { + content: "\10f68b"; } + +.fad.fa-sign:after { + content: "\10f4d9"; } + +.fad.fa-sign-in:after { + content: "\10f090"; } + +.fad.fa-sign-in-alt:after { + content: "\10f2f6"; } + +.fad.fa-sign-language:after { + content: "\10f2a7"; } + +.fad.fa-sign-out:after { + content: "\10f08b"; } + +.fad.fa-sign-out-alt:after { + content: "\10f2f5"; } + +.fad.fa-signal:after { + content: "\10f012"; } + +.fad.fa-signal-1:after { + content: "\10f68c"; } + +.fad.fa-signal-2:after { + content: "\10f68d"; } + +.fad.fa-signal-3:after { + content: "\10f68e"; } + +.fad.fa-signal-4:after { + content: "\10f68f"; } + +.fad.fa-signal-alt:after { + content: "\10f690"; } + +.fad.fa-signal-alt-1:after { + content: "\10f691"; } + +.fad.fa-signal-alt-2:after { + content: "\10f692"; } + +.fad.fa-signal-alt-3:after { + content: "\10f693"; } + +.fad.fa-signal-alt-slash:after { + content: "\10f694"; } + +.fad.fa-signal-slash:after { + content: "\10f695"; } + +.fad.fa-signal-stream:after { + content: "\10f8dd"; } + +.fad.fa-signature:after { + content: "\10f5b7"; } + +.fad.fa-sim-card:after { + content: "\10f7c4"; } + +.fad.fa-sink:after { + content: "\10e06d"; } + +.fad.fa-siren:after { + content: "\10e02d"; } + +.fad.fa-siren-on:after { + content: "\10e02e"; } + +.fad.fa-sitemap:after { + content: "\10f0e8"; } + +.fad.fa-skating:after { + content: "\10f7c5"; } + +.fad.fa-skeleton:after { + content: "\10f620"; } + +.fad.fa-ski-jump:after { + content: "\10f7c7"; } + +.fad.fa-ski-lift:after { + content: "\10f7c8"; } + +.fad.fa-skiing:after { + content: "\10f7c9"; } + +.fad.fa-skiing-nordic:after { + content: "\10f7ca"; } + +.fad.fa-skull:after { + content: "\10f54c"; } + +.fad.fa-skull-cow:after { + content: "\10f8de"; } + +.fad.fa-skull-crossbones:after { + content: "\10f714"; } + +.fad.fa-slash:after { + content: "\10f715"; } + +.fad.fa-sledding:after { + content: "\10f7cb"; } + +.fad.fa-sleigh:after { + content: "\10f7cc"; } + +.fad.fa-sliders-h:after { + content: "\10f1de"; } + +.fad.fa-sliders-h-square:after { + content: "\10f3f0"; } + +.fad.fa-sliders-v:after { + content: "\10f3f1"; } + +.fad.fa-sliders-v-square:after { + content: "\10f3f2"; } + +.fad.fa-smile:after { + content: "\10f118"; } + +.fad.fa-smile-beam:after { + content: "\10f5b8"; } + +.fad.fa-smile-plus:after { + content: "\10f5b9"; } + +.fad.fa-smile-wink:after { + content: "\10f4da"; } + +.fad.fa-smog:after { + content: "\10f75f"; } + +.fad.fa-smoke:after { + content: "\10f760"; } + +.fad.fa-smoking:after { + content: "\10f48d"; } + +.fad.fa-smoking-ban:after { + content: "\10f54d"; } + +.fad.fa-sms:after { + content: "\10f7cd"; } + +.fad.fa-snake:after { + content: "\10f716"; } + +.fad.fa-snooze:after { + content: "\10f880"; } + +.fad.fa-snow-blowing:after { + content: "\10f761"; } + +.fad.fa-snowboarding:after { + content: "\10f7ce"; } + +.fad.fa-snowflake:after { + content: "\10f2dc"; } + +.fad.fa-snowflakes:after { + content: "\10f7cf"; } + +.fad.fa-snowman:after { + content: "\10f7d0"; } + +.fad.fa-snowmobile:after { + content: "\10f7d1"; } + +.fad.fa-snowplow:after { + content: "\10f7d2"; } + +.fad.fa-soap:after { + content: "\10e06e"; } + +.fad.fa-socks:after { + content: "\10f696"; } + +.fad.fa-solar-panel:after { + content: "\10f5ba"; } + +.fad.fa-solar-system:after { + content: "\10e02f"; } + +.fad.fa-sort:after { + content: "\10f0dc"; } + +.fad.fa-sort-alpha-down:after { + content: "\10f15d"; } + +.fad.fa-sort-alpha-down-alt:after { + content: "\10f881"; } + +.fad.fa-sort-alpha-up:after { + content: "\10f15e"; } + +.fad.fa-sort-alpha-up-alt:after { + content: "\10f882"; } + +.fad.fa-sort-alt:after { + content: "\10f883"; } + +.fad.fa-sort-amount-down:after { + content: "\10f160"; } + +.fad.fa-sort-amount-down-alt:after { + content: "\10f884"; } + +.fad.fa-sort-amount-up:after { + content: "\10f161"; } + +.fad.fa-sort-amount-up-alt:after { + content: "\10f885"; } + +.fad.fa-sort-circle:after { + content: "\10e030"; } + +.fad.fa-sort-circle-down:after { + content: "\10e031"; } + +.fad.fa-sort-circle-up:after { + content: "\10e032"; } + +.fad.fa-sort-down:after { + content: "\10f0dd"; } + +.fad.fa-sort-numeric-down:after { + content: "\10f162"; } + +.fad.fa-sort-numeric-down-alt:after { + content: "\10f886"; } + +.fad.fa-sort-numeric-up:after { + content: "\10f163"; } + +.fad.fa-sort-numeric-up-alt:after { + content: "\10f887"; } + +.fad.fa-sort-shapes-down:after { + content: "\10f888"; } + +.fad.fa-sort-shapes-down-alt:after { + content: "\10f889"; } + +.fad.fa-sort-shapes-up:after { + content: "\10f88a"; } + +.fad.fa-sort-shapes-up-alt:after { + content: "\10f88b"; } + +.fad.fa-sort-size-down:after { + content: "\10f88c"; } + +.fad.fa-sort-size-down-alt:after { + content: "\10f88d"; } + +.fad.fa-sort-size-up:after { + content: "\10f88e"; } + +.fad.fa-sort-size-up-alt:after { + content: "\10f88f"; } + +.fad.fa-sort-up:after { + content: "\10f0de"; } + +.fad.fa-soup:after { + content: "\10f823"; } + +.fad.fa-spa:after { + content: "\10f5bb"; } + +.fad.fa-space-shuttle:after { + content: "\10f197"; } + +.fad.fa-space-station-moon:after { + content: "\10e033"; } + +.fad.fa-space-station-moon-alt:after { + content: "\10e034"; } + +.fad.fa-spade:after { + content: "\10f2f4"; } + +.fad.fa-sparkles:after { + content: "\10f890"; } + +.fad.fa-speaker:after { + content: "\10f8df"; } + +.fad.fa-speakers:after { + content: "\10f8e0"; } + +.fad.fa-spell-check:after { + content: "\10f891"; } + +.fad.fa-spider:after { + content: "\10f717"; } + +.fad.fa-spider-black-widow:after { + content: "\10f718"; } + +.fad.fa-spider-web:after { + content: "\10f719"; } + +.fad.fa-spinner:after { + content: "\10f110"; } + +.fad.fa-spinner-third:after { + content: "\10f3f4"; } + +.fad.fa-splotch:after { + content: "\10f5bc"; } + +.fad.fa-spray-can:after { + content: "\10f5bd"; } + +.fad.fa-sprinkler:after { + content: "\10e035"; } + +.fad.fa-square:after { + content: "\10f0c8"; } + +.fad.fa-square-full:after { + content: "\10f45c"; } + +.fad.fa-square-root:after { + content: "\10f697"; } + +.fad.fa-square-root-alt:after { + content: "\10f698"; } + +.fad.fa-squirrel:after { + content: "\10f71a"; } + +.fad.fa-staff:after { + content: "\10f71b"; } + +.fad.fa-stamp:after { + content: "\10f5bf"; } + +.fad.fa-star:after { + content: "\10f005"; } + +.fad.fa-star-and-crescent:after { + content: "\10f699"; } + +.fad.fa-star-christmas:after { + content: "\10f7d4"; } + +.fad.fa-star-exclamation:after { + content: "\10f2f3"; } + +.fad.fa-star-half:after { + content: "\10f089"; } + +.fad.fa-star-half-alt:after { + content: "\10f5c0"; } + +.fad.fa-star-of-david:after { + content: "\10f69a"; } + +.fad.fa-star-of-life:after { + content: "\10f621"; } + +.fad.fa-star-shooting:after { + content: "\10e036"; } + +.fad.fa-starfighter:after { + content: "\10e037"; } + +.fad.fa-starfighter-alt:after { + content: "\10e038"; } + +.fad.fa-stars:after { + content: "\10f762"; } + +.fad.fa-starship:after { + content: "\10e039"; } + +.fad.fa-starship-freighter:after { + content: "\10e03a"; } + +.fad.fa-steak:after { + content: "\10f824"; } + +.fad.fa-steering-wheel:after { + content: "\10f622"; } + +.fad.fa-step-backward:after { + content: "\10f048"; } + +.fad.fa-step-forward:after { + content: "\10f051"; } + +.fad.fa-stethoscope:after { + content: "\10f0f1"; } + +.fad.fa-sticky-note:after { + content: "\10f249"; } + +.fad.fa-stocking:after { + content: "\10f7d5"; } + +.fad.fa-stomach:after { + content: "\10f623"; } + +.fad.fa-stop:after { + content: "\10f04d"; } + +.fad.fa-stop-circle:after { + content: "\10f28d"; } + +.fad.fa-stopwatch:after { + content: "\10f2f2"; } + +.fad.fa-stopwatch-20:after { + content: "\10e06f"; } + +.fad.fa-store:after { + content: "\10f54e"; } + +.fad.fa-store-alt:after { + content: "\10f54f"; } + +.fad.fa-store-alt-slash:after { + content: "\10e070"; } + +.fad.fa-store-slash:after { + content: "\10e071"; } + +.fad.fa-stream:after { + content: "\10f550"; } + +.fad.fa-street-view:after { + content: "\10f21d"; } + +.fad.fa-stretcher:after { + content: "\10f825"; } + +.fad.fa-strikethrough:after { + content: "\10f0cc"; } + +.fad.fa-stroopwafel:after { + content: "\10f551"; } + +.fad.fa-subscript:after { + content: "\10f12c"; } + +.fad.fa-subway:after { + content: "\10f239"; } + +.fad.fa-suitcase:after { + content: "\10f0f2"; } + +.fad.fa-suitcase-rolling:after { + content: "\10f5c1"; } + +.fad.fa-sun:after { + content: "\10f185"; } + +.fad.fa-sun-cloud:after { + content: "\10f763"; } + +.fad.fa-sun-dust:after { + content: "\10f764"; } + +.fad.fa-sun-haze:after { + content: "\10f765"; } + +.fad.fa-sunglasses:after { + content: "\10f892"; } + +.fad.fa-sunrise:after { + content: "\10f766"; } + +.fad.fa-sunset:after { + content: "\10f767"; } + +.fad.fa-superscript:after { + content: "\10f12b"; } + +.fad.fa-surprise:after { + content: "\10f5c2"; } + +.fad.fa-swatchbook:after { + content: "\10f5c3"; } + +.fad.fa-swimmer:after { + content: "\10f5c4"; } + +.fad.fa-swimming-pool:after { + content: "\10f5c5"; } + +.fad.fa-sword:after { + content: "\10f71c"; } + +.fad.fa-sword-laser:after { + content: "\10e03b"; } + +.fad.fa-sword-laser-alt:after { + content: "\10e03c"; } + +.fad.fa-swords:after { + content: "\10f71d"; } + +.fad.fa-swords-laser:after { + content: "\10e03d"; } + +.fad.fa-synagogue:after { + content: "\10f69b"; } + +.fad.fa-sync:after { + content: "\10f021"; } + +.fad.fa-sync-alt:after { + content: "\10f2f1"; } + +.fad.fa-syringe:after { + content: "\10f48e"; } + +.fad.fa-table:after { + content: "\10f0ce"; } + +.fad.fa-table-tennis:after { + content: "\10f45d"; } + +.fad.fa-tablet:after { + content: "\10f10a"; } + +.fad.fa-tablet-alt:after { + content: "\10f3fa"; } + +.fad.fa-tablet-android:after { + content: "\10f3fb"; } + +.fad.fa-tablet-android-alt:after { + content: "\10f3fc"; } + +.fad.fa-tablet-rugged:after { + content: "\10f48f"; } + +.fad.fa-tablets:after { + content: "\10f490"; } + +.fad.fa-tachometer:after { + content: "\10f0e4"; } + +.fad.fa-tachometer-alt:after { + content: "\10f3fd"; } + +.fad.fa-tachometer-alt-average:after { + content: "\10f624"; } + +.fad.fa-tachometer-alt-fast:after { + content: "\10f625"; } + +.fad.fa-tachometer-alt-fastest:after { + content: "\10f626"; } + +.fad.fa-tachometer-alt-slow:after { + content: "\10f627"; } + +.fad.fa-tachometer-alt-slowest:after { + content: "\10f628"; } + +.fad.fa-tachometer-average:after { + content: "\10f629"; } + +.fad.fa-tachometer-fast:after { + content: "\10f62a"; } + +.fad.fa-tachometer-fastest:after { + content: "\10f62b"; } + +.fad.fa-tachometer-slow:after { + content: "\10f62c"; } + +.fad.fa-tachometer-slowest:after { + content: "\10f62d"; } + +.fad.fa-taco:after { + content: "\10f826"; } + +.fad.fa-tag:after { + content: "\10f02b"; } + +.fad.fa-tags:after { + content: "\10f02c"; } + +.fad.fa-tally:after { + content: "\10f69c"; } + +.fad.fa-tanakh:after { + content: "\10f827"; } + +.fad.fa-tape:after { + content: "\10f4db"; } + +.fad.fa-tasks:after { + content: "\10f0ae"; } + +.fad.fa-tasks-alt:after { + content: "\10f828"; } + +.fad.fa-taxi:after { + content: "\10f1ba"; } + +.fad.fa-teeth:after { + content: "\10f62e"; } + +.fad.fa-teeth-open:after { + content: "\10f62f"; } + +.fad.fa-telescope:after { + content: "\10e03e"; } + +.fad.fa-temperature-down:after { + content: "\10e03f"; } + +.fad.fa-temperature-frigid:after { + content: "\10f768"; } + +.fad.fa-temperature-high:after { + content: "\10f769"; } + +.fad.fa-temperature-hot:after { + content: "\10f76a"; } + +.fad.fa-temperature-low:after { + content: "\10f76b"; } + +.fad.fa-temperature-up:after { + content: "\10e040"; } + +.fad.fa-tenge:after { + content: "\10f7d7"; } + +.fad.fa-tennis-ball:after { + content: "\10f45e"; } + +.fad.fa-terminal:after { + content: "\10f120"; } + +.fad.fa-text:after { + content: "\10f893"; } + +.fad.fa-text-height:after { + content: "\10f034"; } + +.fad.fa-text-size:after { + content: "\10f894"; } + +.fad.fa-text-width:after { + content: "\10f035"; } + +.fad.fa-th:after { + content: "\10f00a"; } + +.fad.fa-th-large:after { + content: "\10f009"; } + +.fad.fa-th-list:after { + content: "\10f00b"; } + +.fad.fa-theater-masks:after { + content: "\10f630"; } + +.fad.fa-thermometer:after { + content: "\10f491"; } + +.fad.fa-thermometer-empty:after { + content: "\10f2cb"; } + +.fad.fa-thermometer-full:after { + content: "\10f2c7"; } + +.fad.fa-thermometer-half:after { + content: "\10f2c9"; } + +.fad.fa-thermometer-quarter:after { + content: "\10f2ca"; } + +.fad.fa-thermometer-three-quarters:after { + content: "\10f2c8"; } + +.fad.fa-theta:after { + content: "\10f69e"; } + +.fad.fa-thumbs-down:after { + content: "\10f165"; } + +.fad.fa-thumbs-up:after { + content: "\10f164"; } + +.fad.fa-thumbtack:after { + content: "\10f08d"; } + +.fad.fa-thunderstorm:after { + content: "\10f76c"; } + +.fad.fa-thunderstorm-moon:after { + content: "\10f76d"; } + +.fad.fa-thunderstorm-sun:after { + content: "\10f76e"; } + +.fad.fa-ticket:after { + content: "\10f145"; } + +.fad.fa-ticket-alt:after { + content: "\10f3ff"; } + +.fad.fa-tilde:after { + content: "\10f69f"; } + +.fad.fa-times:after { + content: "\10f00d"; } + +.fad.fa-times-circle:after { + content: "\10f057"; } + +.fad.fa-times-hexagon:after { + content: "\10f2ee"; } + +.fad.fa-times-octagon:after { + content: "\10f2f0"; } + +.fad.fa-times-square:after { + content: "\10f2d3"; } + +.fad.fa-tint:after { + content: "\10f043"; } + +.fad.fa-tint-slash:after { + content: "\10f5c7"; } + +.fad.fa-tire:after { + content: "\10f631"; } + +.fad.fa-tire-flat:after { + content: "\10f632"; } + +.fad.fa-tire-pressure-warning:after { + content: "\10f633"; } + +.fad.fa-tire-rugged:after { + content: "\10f634"; } + +.fad.fa-tired:after { + content: "\10f5c8"; } + +.fad.fa-toggle-off:after { + content: "\10f204"; } + +.fad.fa-toggle-on:after { + content: "\10f205"; } + +.fad.fa-toilet:after { + content: "\10f7d8"; } + +.fad.fa-toilet-paper:after { + content: "\10f71e"; } + +.fad.fa-toilet-paper-alt:after { + content: "\10f71f"; } + +.fad.fa-toilet-paper-slash:after { + content: "\10e072"; } + +.fad.fa-tombstone:after { + content: "\10f720"; } + +.fad.fa-tombstone-alt:after { + content: "\10f721"; } + +.fad.fa-toolbox:after { + content: "\10f552"; } + +.fad.fa-tools:after { + content: "\10f7d9"; } + +.fad.fa-tooth:after { + content: "\10f5c9"; } + +.fad.fa-toothbrush:after { + content: "\10f635"; } + +.fad.fa-torah:after { + content: "\10f6a0"; } + +.fad.fa-torii-gate:after { + content: "\10f6a1"; } + +.fad.fa-tornado:after { + content: "\10f76f"; } + +.fad.fa-tractor:after { + content: "\10f722"; } + +.fad.fa-trademark:after { + content: "\10f25c"; } + +.fad.fa-traffic-cone:after { + content: "\10f636"; } + +.fad.fa-traffic-light:after { + content: "\10f637"; } + +.fad.fa-traffic-light-go:after { + content: "\10f638"; } + +.fad.fa-traffic-light-slow:after { + content: "\10f639"; } + +.fad.fa-traffic-light-stop:after { + content: "\10f63a"; } + +.fad.fa-trailer:after { + content: "\10e041"; } + +.fad.fa-train:after { + content: "\10f238"; } + +.fad.fa-tram:after { + content: "\10f7da"; } + +.fad.fa-transgender:after { + content: "\10f224"; } + +.fad.fa-transgender-alt:after { + content: "\10f225"; } + +.fad.fa-transporter:after { + content: "\10e042"; } + +.fad.fa-transporter-1:after { + content: "\10e043"; } + +.fad.fa-transporter-2:after { + content: "\10e044"; } + +.fad.fa-transporter-3:after { + content: "\10e045"; } + +.fad.fa-transporter-empty:after { + content: "\10e046"; } + +.fad.fa-trash:after { + content: "\10f1f8"; } + +.fad.fa-trash-alt:after { + content: "\10f2ed"; } + +.fad.fa-trash-restore:after { + content: "\10f829"; } + +.fad.fa-trash-restore-alt:after { + content: "\10f82a"; } + +.fad.fa-trash-undo:after { + content: "\10f895"; } + +.fad.fa-trash-undo-alt:after { + content: "\10f896"; } + +.fad.fa-treasure-chest:after { + content: "\10f723"; } + +.fad.fa-tree:after { + content: "\10f1bb"; } + +.fad.fa-tree-alt:after { + content: "\10f400"; } + +.fad.fa-tree-christmas:after { + content: "\10f7db"; } + +.fad.fa-tree-decorated:after { + content: "\10f7dc"; } + +.fad.fa-tree-large:after { + content: "\10f7dd"; } + +.fad.fa-tree-palm:after { + content: "\10f82b"; } + +.fad.fa-trees:after { + content: "\10f724"; } + +.fad.fa-triangle:after { + content: "\10f2ec"; } + +.fad.fa-triangle-music:after { + content: "\10f8e2"; } + +.fad.fa-trophy:after { + content: "\10f091"; } + +.fad.fa-trophy-alt:after { + content: "\10f2eb"; } + +.fad.fa-truck:after { + content: "\10f0d1"; } + +.fad.fa-truck-container:after { + content: "\10f4dc"; } + +.fad.fa-truck-couch:after { + content: "\10f4dd"; } + +.fad.fa-truck-loading:after { + content: "\10f4de"; } + +.fad.fa-truck-monster:after { + content: "\10f63b"; } + +.fad.fa-truck-moving:after { + content: "\10f4df"; } + +.fad.fa-truck-pickup:after { + content: "\10f63c"; } + +.fad.fa-truck-plow:after { + content: "\10f7de"; } + +.fad.fa-truck-ramp:after { + content: "\10f4e0"; } + +.fad.fa-trumpet:after { + content: "\10f8e3"; } + +.fad.fa-tshirt:after { + content: "\10f553"; } + +.fad.fa-tty:after { + content: "\10f1e4"; } + +.fad.fa-turkey:after { + content: "\10f725"; } + +.fad.fa-turntable:after { + content: "\10f8e4"; } + +.fad.fa-turtle:after { + content: "\10f726"; } + +.fad.fa-tv:after { + content: "\10f26c"; } + +.fad.fa-tv-alt:after { + content: "\10f8e5"; } + +.fad.fa-tv-music:after { + content: "\10f8e6"; } + +.fad.fa-tv-retro:after { + content: "\10f401"; } + +.fad.fa-typewriter:after { + content: "\10f8e7"; } + +.fad.fa-ufo:after { + content: "\10e047"; } + +.fad.fa-ufo-beam:after { + content: "\10e048"; } + +.fad.fa-umbrella:after { + content: "\10f0e9"; } + +.fad.fa-umbrella-beach:after { + content: "\10f5ca"; } + +.fad.fa-underline:after { + content: "\10f0cd"; } + +.fad.fa-undo:after { + content: "\10f0e2"; } + +.fad.fa-undo-alt:after { + content: "\10f2ea"; } + +.fad.fa-unicorn:after { + content: "\10f727"; } + +.fad.fa-union:after { + content: "\10f6a2"; } + +.fad.fa-universal-access:after { + content: "\10f29a"; } + +.fad.fa-university:after { + content: "\10f19c"; } + +.fad.fa-unlink:after { + content: "\10f127"; } + +.fad.fa-unlock:after { + content: "\10f09c"; } + +.fad.fa-unlock-alt:after { + content: "\10f13e"; } + +.fad.fa-upload:after { + content: "\10f093"; } + +.fad.fa-usb-drive:after { + content: "\10f8e9"; } + +.fad.fa-usd-circle:after { + content: "\10f2e8"; } + +.fad.fa-usd-square:after { + content: "\10f2e9"; } + +.fad.fa-user:after { + content: "\10f007"; } + +.fad.fa-user-alien:after { + content: "\10e04a"; } + +.fad.fa-user-alt:after { + content: "\10f406"; } + +.fad.fa-user-alt-slash:after { + content: "\10f4fa"; } + +.fad.fa-user-astronaut:after { + content: "\10f4fb"; } + +.fad.fa-user-chart:after { + content: "\10f6a3"; } + +.fad.fa-user-check:after { + content: "\10f4fc"; } + +.fad.fa-user-circle:after { + content: "\10f2bd"; } + +.fad.fa-user-clock:after { + content: "\10f4fd"; } + +.fad.fa-user-cog:after { + content: "\10f4fe"; } + +.fad.fa-user-cowboy:after { + content: "\10f8ea"; } + +.fad.fa-user-crown:after { + content: "\10f6a4"; } + +.fad.fa-user-edit:after { + content: "\10f4ff"; } + +.fad.fa-user-friends:after { + content: "\10f500"; } + +.fad.fa-user-graduate:after { + content: "\10f501"; } + +.fad.fa-user-hard-hat:after { + content: "\10f82c"; } + +.fad.fa-user-headset:after { + content: "\10f82d"; } + +.fad.fa-user-injured:after { + content: "\10f728"; } + +.fad.fa-user-lock:after { + content: "\10f502"; } + +.fad.fa-user-md:after { + content: "\10f0f0"; } + +.fad.fa-user-md-chat:after { + content: "\10f82e"; } + +.fad.fa-user-minus:after { + content: "\10f503"; } + +.fad.fa-user-music:after { + content: "\10f8eb"; } + +.fad.fa-user-ninja:after { + content: "\10f504"; } + +.fad.fa-user-nurse:after { + content: "\10f82f"; } + +.fad.fa-user-plus:after { + content: "\10f234"; } + +.fad.fa-user-robot:after { + content: "\10e04b"; } + +.fad.fa-user-secret:after { + content: "\10f21b"; } + +.fad.fa-user-shield:after { + content: "\10f505"; } + +.fad.fa-user-slash:after { + content: "\10f506"; } + +.fad.fa-user-tag:after { + content: "\10f507"; } + +.fad.fa-user-tie:after { + content: "\10f508"; } + +.fad.fa-user-times:after { + content: "\10f235"; } + +.fad.fa-user-unlock:after { + content: "\10e058"; } + +.fad.fa-user-visor:after { + content: "\10e04c"; } + +.fad.fa-users:after { + content: "\10f0c0"; } + +.fad.fa-users-class:after { + content: "\10f63d"; } + +.fad.fa-users-cog:after { + content: "\10f509"; } + +.fad.fa-users-crown:after { + content: "\10f6a5"; } + +.fad.fa-users-medical:after { + content: "\10f830"; } + +.fad.fa-users-slash:after { + content: "\10e073"; } + +.fad.fa-utensil-fork:after { + content: "\10f2e3"; } + +.fad.fa-utensil-knife:after { + content: "\10f2e4"; } + +.fad.fa-utensil-spoon:after { + content: "\10f2e5"; } + +.fad.fa-utensils:after { + content: "\10f2e7"; } + +.fad.fa-utensils-alt:after { + content: "\10f2e6"; } + +.fad.fa-vacuum:after { + content: "\10e04d"; } + +.fad.fa-vacuum-robot:after { + content: "\10e04e"; } + +.fad.fa-value-absolute:after { + content: "\10f6a6"; } + +.fad.fa-vector-square:after { + content: "\10f5cb"; } + +.fad.fa-venus:after { + content: "\10f221"; } + +.fad.fa-venus-double:after { + content: "\10f226"; } + +.fad.fa-venus-mars:after { + content: "\10f228"; } + +.fad.fa-vest:after { + content: "\10e085"; } + +.fad.fa-vest-patches:after { + content: "\10e086"; } + +.fad.fa-vhs:after { + content: "\10f8ec"; } + +.fad.fa-vial:after { + content: "\10f492"; } + +.fad.fa-vials:after { + content: "\10f493"; } + +.fad.fa-video:after { + content: "\10f03d"; } + +.fad.fa-video-plus:after { + content: "\10f4e1"; } + +.fad.fa-video-slash:after { + content: "\10f4e2"; } + +.fad.fa-vihara:after { + content: "\10f6a7"; } + +.fad.fa-violin:after { + content: "\10f8ed"; } + +.fad.fa-virus:after { + content: "\10e074"; } + +.fad.fa-virus-slash:after { + content: "\10e075"; } + +.fad.fa-viruses:after { + content: "\10e076"; } + +.fad.fa-voicemail:after { + content: "\10f897"; } + +.fad.fa-volcano:after { + content: "\10f770"; } + +.fad.fa-volleyball-ball:after { + content: "\10f45f"; } + +.fad.fa-volume:after { + content: "\10f6a8"; } + +.fad.fa-volume-down:after { + content: "\10f027"; } + +.fad.fa-volume-mute:after { + content: "\10f6a9"; } + +.fad.fa-volume-off:after { + content: "\10f026"; } + +.fad.fa-volume-slash:after { + content: "\10f2e2"; } + +.fad.fa-volume-up:after { + content: "\10f028"; } + +.fad.fa-vote-nay:after { + content: "\10f771"; } + +.fad.fa-vote-yea:after { + content: "\10f772"; } + +.fad.fa-vr-cardboard:after { + content: "\10f729"; } + +.fad.fa-wagon-covered:after { + content: "\10f8ee"; } + +.fad.fa-walker:after { + content: "\10f831"; } + +.fad.fa-walkie-talkie:after { + content: "\10f8ef"; } + +.fad.fa-walking:after { + content: "\10f554"; } + +.fad.fa-wallet:after { + content: "\10f555"; } + +.fad.fa-wand:after { + content: "\10f72a"; } + +.fad.fa-wand-magic:after { + content: "\10f72b"; } + +.fad.fa-warehouse:after { + content: "\10f494"; } + +.fad.fa-warehouse-alt:after { + content: "\10f495"; } + +.fad.fa-washer:after { + content: "\10f898"; } + +.fad.fa-watch:after { + content: "\10f2e1"; } + +.fad.fa-watch-calculator:after { + content: "\10f8f0"; } + +.fad.fa-watch-fitness:after { + content: "\10f63e"; } + +.fad.fa-water:after { + content: "\10f773"; } + +.fad.fa-water-lower:after { + content: "\10f774"; } + +.fad.fa-water-rise:after { + content: "\10f775"; } + +.fad.fa-wave-sine:after { + content: "\10f899"; } + +.fad.fa-wave-square:after { + content: "\10f83e"; } + +.fad.fa-wave-triangle:after { + content: "\10f89a"; } + +.fad.fa-waveform:after { + content: "\10f8f1"; } + +.fad.fa-waveform-path:after { + content: "\10f8f2"; } + +.fad.fa-webcam:after { + content: "\10f832"; } + +.fad.fa-webcam-slash:after { + content: "\10f833"; } + +.fad.fa-weight:after { + content: "\10f496"; } + +.fad.fa-weight-hanging:after { + content: "\10f5cd"; } + +.fad.fa-whale:after { + content: "\10f72c"; } + +.fad.fa-wheat:after { + content: "\10f72d"; } + +.fad.fa-wheelchair:after { + content: "\10f193"; } + +.fad.fa-whistle:after { + content: "\10f460"; } + +.fad.fa-wifi:after { + content: "\10f1eb"; } + +.fad.fa-wifi-1:after { + content: "\10f6aa"; } + +.fad.fa-wifi-2:after { + content: "\10f6ab"; } + +.fad.fa-wifi-slash:after { + content: "\10f6ac"; } + +.fad.fa-wind:after { + content: "\10f72e"; } + +.fad.fa-wind-turbine:after { + content: "\10f89b"; } + +.fad.fa-wind-warning:after { + content: "\10f776"; } + +.fad.fa-window:after { + content: "\10f40e"; } + +.fad.fa-window-alt:after { + content: "\10f40f"; } + +.fad.fa-window-close:after { + content: "\10f410"; } + +.fad.fa-window-frame:after { + content: "\10e04f"; } + +.fad.fa-window-frame-open:after { + content: "\10e050"; } + +.fad.fa-window-maximize:after { + content: "\10f2d0"; } + +.fad.fa-window-minimize:after { + content: "\10f2d1"; } + +.fad.fa-window-restore:after { + content: "\10f2d2"; } + +.fad.fa-windsock:after { + content: "\10f777"; } + +.fad.fa-wine-bottle:after { + content: "\10f72f"; } + +.fad.fa-wine-glass:after { + content: "\10f4e3"; } + +.fad.fa-wine-glass-alt:after { + content: "\10f5ce"; } + +.fad.fa-won-sign:after { + content: "\10f159"; } + +.fad.fa-wreath:after { + content: "\10f7e2"; } + +.fad.fa-wrench:after { + content: "\10f0ad"; } + +.fad.fa-x-ray:after { + content: "\10f497"; } + +.fad.fa-yen-sign:after { + content: "\10f157"; } + +.fad.fa-yin-yang:after { + content: "\10f6ad"; } +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 300; + font-display: block; + src: url("/fonts/fa-light-300.eot"); + src: url("/fonts/fa-light-300.eot?#iefix") format("embedded-opentype"), url("/fonts/fa-light-300.woff2") format("woff2"), url("/fonts/fa-light-300.woff") format("woff"), url("/fonts/fa-light-300.ttf") format("truetype"), url("/fonts/fa-light-300.svg#fontawesome") format("svg"); } + +.fal { + font-family: 'Font Awesome 5 Free'; + font-weight: 300; } @font-face { font-family: 'Font Awesome 5 Free'; font-style: normal; font-weight: 400; - font-display: swap; + font-display: block; src: url("/fonts/fa-regular-400.eot"); src: url("/fonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("/fonts/fa-regular-400.woff2") format("woff2"), url("/fonts/fa-regular-400.woff") format("woff"), url("/fonts/fa-regular-400.ttf") format("truetype"), url("/fonts/fa-regular-400.svg#fontawesome") format("svg"); } @@ -4276,7 +12771,7 @@ readers do not read off random characters that represent icons */ font-family: 'Font Awesome 5 Free'; font-style: normal; font-weight: 900; - font-display: swap; + font-display: block; src: url("/fonts/fa-solid-900.eot"); src: url("/fonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("/fonts/fa-solid-900.woff2") format("woff2"), url("/fonts/fa-solid-900.woff") format("woff"), url("/fonts/fa-solid-900.ttf") format("truetype"), url("/fonts/fa-solid-900.svg#fontawesome") format("svg"); } diff --git a/resources/assets/sass/lib/nucleo.css b/resources/assets/sass/lib/nucleo.css index b03698950..4171a5523 100644 --- a/resources/assets/sass/lib/nucleo.css +++ b/resources/assets/sass/lib/nucleo.css @@ -10,6 +10,7 @@ License - nucleoapp.com/license/ src: url('/fonts/nucleo-icons.eot') format('embedded-opentype'), url('/fonts/nucleo-icons.woff2') format('woff2'), url('/fonts/nucleo-icons.woff') format('woff'), url('/fonts/nucleo-icons.ttf') format('truetype'), url('/fonts/nucleo-icons.svg') format('svg'); font-weight: normal; font-style: normal; + font-display: swap; } /*------------------------ base class definition diff --git a/resources/assets/sass/spa.scss b/resources/assets/sass/spa.scss index 72e08e16b..8740f8879 100644 --- a/resources/assets/sass/spa.scss +++ b/resources/assets/sass/spa.scss @@ -189,6 +189,10 @@ a.text-dark:hover { .border { border: 1px solid var(--border-color) !important; + + &-right { + border-color: var(--border-color) !important; + } } .bg-white, @@ -392,6 +396,14 @@ span.twitter-typeahead .tt-suggestion:focus { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } +.timestamp-overlay-badge { + color: var(--dark); +} + +.modal-backdrop { + opacity: 0.8; +} + .timeline-status-component { .username { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; diff --git a/resources/views/account/moderation/post/autospam.blade.php b/resources/views/account/moderation/post/autospam.blade.php index 91296759d..d16d85f3e 100644 --- a/resources/views/account/moderation/post/autospam.blade.php +++ b/resources/views/account/moderation/post/autospam.blade.php @@ -69,7 +69,7 @@

Review the Community Guidelines

-

We want to keep {{config('app.name')}} a safe place for everyone, and we created these Community Guidelines to support and protect our community.

+

We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these Community Guidelines to support and protect our community.

@@ -100,4 +100,4 @@ ctx.putImageData(imageData, 0, 0); @endif -@endpush \ No newline at end of file +@endpush diff --git a/resources/views/account/moderation/post/cw.blade.php b/resources/views/account/moderation/post/cw.blade.php index d1e1d3537..bffa7a186 100644 --- a/resources/views/account/moderation/post/cw.blade.php +++ b/resources/views/account/moderation/post/cw.blade.php @@ -70,7 +70,7 @@

Review the Community Guidelines

-

We want to keep {{config('app.name')}} a safe place for everyone, and we created these Community Guidelines to support and protect our community.

+

We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these Community Guidelines to support and protect our community.

@@ -127,4 +127,4 @@ ctx.putImageData(imageData, 0, 0); @endif -@endpush \ No newline at end of file +@endpush diff --git a/resources/views/account/moderation/post/removed.blade.php b/resources/views/account/moderation/post/removed.blade.php index 123863489..4b8a1fee4 100644 --- a/resources/views/account/moderation/post/removed.blade.php +++ b/resources/views/account/moderation/post/removed.blade.php @@ -62,7 +62,7 @@

Review the Community Guidelines

-

We want to keep {{config('app.name')}} a safe place for everyone, and we created these Community Guidelines to support and protect our community.

+

We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these Community Guidelines to support and protect our community.

@@ -96,4 +96,4 @@ ctx.putImageData(imageData, 0, 0); @endif -@endpush \ No newline at end of file +@endpush diff --git a/resources/views/account/moderation/post/unlist.blade.php b/resources/views/account/moderation/post/unlist.blade.php index 3c86acb76..4f62a10bb 100644 --- a/resources/views/account/moderation/post/unlist.blade.php +++ b/resources/views/account/moderation/post/unlist.blade.php @@ -69,7 +69,7 @@

Review the Community Guidelines

-

We want to keep {{config('app.name')}} a safe place for everyone, and we created these Community Guidelines to support and protect our community.

+

We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these Community Guidelines to support and protect our community.

@@ -125,4 +125,4 @@ ctx.putImageData(imageData, 0, 0); @endif -@endpush \ No newline at end of file +@endpush diff --git a/resources/views/admin/curated-register/index.blade.php b/resources/views/admin/curated-register/index.blade.php index 5ff8c4dd5..a1564b971 100644 --- a/resources/views/admin/curated-register/index.blade.php +++ b/resources/views/admin/curated-register/index.blade.php @@ -20,10 +20,32 @@ @include('admin.curated-register.partials.nav') @if($records && $records->count()) +
+ @if(in_array($filter, ['all', 'open', 'awaiting', 'responses'])) @@ -37,6 +59,12 @@ @foreach($records as $record) + - + @@ -358,7 +358,7 @@ - + @@ -368,7 +368,7 @@ - + @@ -545,7 +545,7 @@ - + @@ -740,7 +740,7 @@ - + @@ -750,12 +750,12 @@ - + - + @@ -810,12 +810,12 @@ - + - + diff --git a/resources/views/admin/settings/customcss.blade.php b/resources/views/admin/settings/customcss.blade.php new file mode 100644 index 000000000..07049f0a6 --- /dev/null +++ b/resources/views/admin/settings/customcss.blade.php @@ -0,0 +1,45 @@ +@extends('admin.partial.template-full') + +@section('section') + +
+
+
+
+
+

Custom CSS

+

Customize your instance with custom css.

+
+
+
+
+
+
+
+
+ @csrf +
+
+ + +
+
+
+ + +
+ + +
+
+@endsection diff --git a/resources/views/admin/settings/home.blade.php b/resources/views/admin/settings/home.blade.php index c2254f700..d78780878 100644 --- a/resources/views/admin/settings/home.blade.php +++ b/resources/views/admin/settings/home.blade.php @@ -1,421 +1,12 @@ @extends('admin.partial.template-full') @section('section') -
-

Settings

-@if(config('instance.enable_cc')) -

Manage instance settings

- - @csrf - -
- -
- {{--
- -
    -
  • - Max Upload Size: - {{$system['max_upload_size']}} -
  • -
  • - Image Driver: - {{$system['image_driver']}} -
  • -
  • - Image Driver Loaded: - - @if($system['image_driver_loaded']) - - @else - - @endif - -
  • -
  • - File Permissions: - - @if($system['permissions']) - - @else - - @endif - -
  • -
  • - - -
  • -
-
--}} -
-
- - -
- -
- -
-
- - @if($cloud_ready) -
- - -
-

Store photos & videos on S3 compatible object storage providers.

- @endif - -
- - -
-

ActivityPub federation, compatible with Pixelfed, Mastodon and other projects.

- -
- - -
- @if((bool) config_cache('federation.activitypub.enabled')) -

Allow local accounts to migrate to other local or remote accounts.

- @else -

ActivityPub Required Allow local accounts to migrate to other local or remote accounts.

- @endif - - {{--
- - -
-

Allow new user registrations.

--}} - - - {{--
- - -
-

Manually review new account registration applications.

--}} - -
- - -
-

Enable apis required for mobile app support.

- -
- - -
-

Allow users to share ephemeral Stories.

- -
- - -
-

Allow experimental Instagram Import support.

- -
- - -
-

Detect and remove spam from timelines.

-
-
- {{--
-
- - -

The instance name used in titles, metadata and apis.

-
-
-
-
- - -

Short description of instance used on various pages and apis.

-
-
-
-
- - -

Longer description of instance used on about page.

-
-
--}} -
- -
-
-
-

Configure your landing page

-
-
-
-
-

Discovery

- -
-
- - -
-
- -
-
- - -
-
-
-
-
-
-

Admin Account

- -
- -
-
-
-
- -
-
-
- - -

The instance name used in titles, metadata and apis.

-
-
-
-
- - -

Short description of instance used on various pages and apis.

-
-
-
-
- - -

Longer description of instance used on about page.

-
-
-
-
- - -

The header title used on the about page.

-
-
-
- -
-
-
-
- - -
-
-
- -
-
-
- - -

Set a storage limit per user account.

-
- - -

Account limit size in KB.

-

{{config_cache('pixelfed.max_account_size')}} KB = {{floor(config_cache('pixelfed.max_account_size') / 1024)}} MB

-
-
- -
-
-
- - -

Enable auto follow accounts, new accounts will follow accounts you set.

-
- - -

Add account usernames to follow separated by commas.

-
-
-
- -
-
-
- - -

Maximum file upload size in KB

-

{{config_cache('pixelfed.max_photo_size')}} KB = {{number_format(config_cache('pixelfed.max_photo_size') / 1024)}} MB

-
-
-
-
- - -

The maximum number of photos or videos per album

-
-
-
-
- - -

Image optimization quality from 0-100%. Set to 0 to disable image optimization.

-
-
-
-
- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-

Allowed media types.

-
-
-
- -
-
-

Add rules that explain what is acceptable use.

-
-
-

Active Rules

-
    - @if($rules) - @foreach($rules as $rule) -
  1. -

    - {{$rule}} -

    -

    - -

    -
  2. - @endforeach - @endif -
-
-
-
- - -
-
-
- -
-
-
- -
- - -
- -

Add custom CSS, will be used on all pages

-
-
-
- -
- -
-
- -
-
- -@else - -
-

Not enabled

-

Add ENABLE_CONFIG_CACHE=true in your .env file
and run php artisan config:cache

-
-@endif + @endsection @push('scripts') @endpush diff --git a/resources/views/auth/email/forgot.blade.php b/resources/views/auth/email/forgot.blade.php index 898d19fb5..e4b67d792 100644 --- a/resources/views/auth/email/forgot.blade.php +++ b/resources/views/auth/email/forgot.blade.php @@ -65,7 +65,7 @@ - @if(config('captcha.enabled') || config('captcha.active.login') || config('captcha.active.register')) + @if((bool) config_cache('captcha.enabled'))
{!! Captcha::display(['data-theme' => 'dark']) !!} diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 9df9ea8c9..0f77f778e 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -76,10 +76,10 @@
@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') ) diff --git a/resources/views/auth/passwords/email.blade.php b/resources/views/auth/passwords/email.blade.php index 4f2825e29..19461fa29 100644 --- a/resources/views/auth/passwords/email.blade.php +++ b/resources/views/auth/passwords/email.blade.php @@ -54,7 +54,7 @@ - @if(config('captcha.enabled')) + @if((bool) config_cache('captcha.enabled'))
{!! Captcha::display(['data-theme' => 'dark']) !!} diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php index 1a740fa7d..ecabcaddf 100644 --- a/resources/views/auth/passwords/reset.blade.php +++ b/resources/views/auth/passwords/reset.blade.php @@ -109,7 +109,7 @@
- @if(config('captcha.enabled')) + @if((bool) config_cache('captcha.enabled'))
{!! Captcha::display(['data-theme' => 'dark']) !!} diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index a2c008bd7..3cb70c7fe 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -81,7 +81,7 @@
- @if(config('captcha.enabled') || config('captcha.active.register')) + @if((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register'))
{!! Captcha::display() !!}
diff --git a/resources/views/groups/confirm-leave.blade.php b/resources/views/groups/confirm-leave.blade.php new file mode 100644 index 000000000..41d400716 --- /dev/null +++ b/resources/views/groups/confirm-leave.blade.php @@ -0,0 +1,34 @@ +@extends('layouts.app') + +@section('content') +
+
+ +
+

Are you sure you want to leave this group?

+ {{--

If you decide to leave this group, all of your content will be permanently deleted.

+

All of your interactions will be removed, including group

+
    +
  • Posts
  • +
  • Photos & Videos
  • +
  • Comments
  • +
  • Events
  • +
  • Polls
  • +
  • Likes
  • +
  • Shares
  • +
  • Reactions
  • +
  • Group Invitations
  • +
  • Moderation Reports
  • +
  • Recommendations
  • +
--}} +

Any content you shared will remain accessible

+

You will not be able to re-join this group for 24 hours

+
+
+ + + +
+
+
+@endsection diff --git a/resources/views/groups/index.blade.php b/resources/views/groups/index.blade.php new file mode 100644 index 000000000..f3928546c --- /dev/null +++ b/resources/views/groups/index.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/groups/invite-claim.blade.php b/resources/views/groups/invite-claim.blade.php new file mode 100644 index 000000000..3c27f17a8 --- /dev/null +++ b/resources/views/groups/invite-claim.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/groups/invite.blade.php b/resources/views/groups/invite.blade.php new file mode 100644 index 000000000..47eec9b53 --- /dev/null +++ b/resources/views/groups/invite.blade.php @@ -0,0 +1,23 @@ +@extends('layouts.app') + +@section('content') +
+
+
+

You were invited to join the {{ $group->name }} group.

+
+
+ +
+

{{ $group->name }}

+

34 members

+
+
+
+
+
+
+

+
+
+@endsection diff --git a/resources/views/groups/profile.blade.php b/resources/views/groups/profile.blade.php new file mode 100644 index 000000000..cbb95d866 --- /dev/null +++ b/resources/views/groups/profile.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/groups/report.blade.php b/resources/views/groups/report.blade.php new file mode 100644 index 000000000..be090b024 --- /dev/null +++ b/resources/views/groups/report.blade.php @@ -0,0 +1,39 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+ Report Group +
+
+ {{--

+ Only report groups if they are violating the Terms of Service or Community Guidelines. +

--}} +

Reason (select one)

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+@endsection diff --git a/resources/views/groups/settings.blade.php b/resources/views/groups/settings.blade.php new file mode 100644 index 000000000..c7c0be6e5 --- /dev/null +++ b/resources/views/groups/settings.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/groups/show.blade.php b/resources/views/groups/show.blade.php new file mode 100644 index 000000000..3fa190f3b --- /dev/null +++ b/resources/views/groups/show.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.spa') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/groups/status.blade.php b/resources/views/groups/status.blade.php new file mode 100644 index 000000000..31b5ce8b4 --- /dev/null +++ b/resources/views/groups/status.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/groups/topic-feed.blade.php b/resources/views/groups/topic-feed.blade.php new file mode 100644 index 000000000..c9d31d4e0 --- /dev/null +++ b/resources/views/groups/topic-feed.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/groups/unavailable.blade.php b/resources/views/groups/unavailable.blade.php new file mode 100644 index 000000000..ab48447dd --- /dev/null +++ b/resources/views/groups/unavailable.blade.php @@ -0,0 +1,25 @@ +@extends('layouts.app') + +@section('content') +
+
+

Group Unavailable

+

The group you are trying to view is unavailable

+ +
+

This can happen for a few reasons:

+
    +
  • The group url is invalid or has a typo
  • +
  • We are experiencing higher than usual traffic to this group and have temporarily limited access to this group
  • +
  • The group has been flagged for review by our automated abuse detection systems
  • +
  • The group is temporarily disabled by group administrators
  • +
  • The group has been deleted
  • +
+ +

+ If you are a group administrator, you can view your groups settings for more information. +

+
+
+
+@endsection diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 2f7b05cd3..e5aed6dad 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -1,4 +1,4 @@ -@extends('layouts.app',['title' => 'Welcome to ' . config('app.name')]) +@extends('layouts.app',['title' => 'Welcome to ' . config_cache('app.name')]) @section('content')
@@ -14,7 +14,7 @@
@endif -

Welcome to {{config('app.name')}}!

+

Welcome to {{config_cache('app.name')}}!

diff --git a/resources/views/layouts/app-guest.blade.php b/resources/views/layouts/app-guest.blade.php index 6adcffac4..7d8dbd201 100644 --- a/resources/views/layouts/app-guest.blade.php +++ b/resources/views/layouts/app-guest.blade.php @@ -5,11 +5,11 @@ - {{ $title ?? config('app.name', 'Pixelfed') }} + {{ $title ?? config_cache('app.name', 'Pixelfed') }} - - + + @stack('meta') diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 0136842bb..168992aaf 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -70,11 +70,11 @@ - {{ $title ?? config('app.name', 'Pixelfed') }} + {{ $title ?? config_cache('app.name', 'Pixelfed') }} - + @stack('meta') diff --git a/resources/views/layouts/blank.blade.php b/resources/views/layouts/blank.blade.php index 00042315d..deaf71d62 100644 --- a/resources/views/layouts/blank.blade.php +++ b/resources/views/layouts/blank.blade.php @@ -11,8 +11,8 @@ {{ $title ?? config_cache('app.name') }} - - + + @stack('meta') diff --git a/resources/views/layouts/bundle.blade.php b/resources/views/layouts/bundle.blade.php index 1050a39d6..94e0c2c00 100644 --- a/resources/views/layouts/bundle.blade.php +++ b/resources/views/layouts/bundle.blade.php @@ -9,11 +9,11 @@ - {{ $title ?? config('app.name', 'Laravel') }} + {{ $title ?? config_cache('app.name', 'Pixelfed') }} - - + + @stack('meta') diff --git a/resources/views/layouts/partial/nav.blade.php b/resources/views/layouts/partial/nav.blade.php index 1d89902bf..56fe7da4a 100644 --- a/resources/views/layouts/partial/nav.blade.php +++ b/resources/views/layouts/partial/nav.blade.php @@ -105,7 +105,7 @@ {{__('navmenu.discover')}} - @if(config_cache('instance.stories.enabled')) + @if((bool) config_cache('instance.stories.enabled')) diff --git a/resources/views/layouts/partial/noauthnav.blade.php b/resources/views/layouts/partial/noauthnav.blade.php index 465b51354..004c8497f 100644 --- a/resources/views/layouts/partial/noauthnav.blade.php +++ b/resources/views/layouts/partial/noauthnav.blade.php @@ -2,7 +2,7 @@ diff --git a/resources/views/portfolio/layout.blade.php b/resources/views/portfolio/layout.blade.php index 14158fb37..89e909284 100644 --- a/resources/views/portfolio/layout.blade.php +++ b/resources/views/portfolio/layout.blade.php @@ -11,8 +11,8 @@ {!! $title ?? config_cache('app.name') !!} - - + + @stack('meta') diff --git a/resources/views/profile/embed-removed.blade.php b/resources/views/profile/embed-removed.blade.php index 7a49d2e79..c236eb790 100644 --- a/resources/views/profile/embed-removed.blade.php +++ b/resources/views/profile/embed-removed.blade.php @@ -9,8 +9,8 @@ Pixelfed | 404 Embed Not Found - - + + diff --git a/resources/views/profile/embed.blade.php b/resources/views/profile/embed.blade.php index cc6097e3a..d0edb9f59 100644 --- a/resources/views/profile/embed.blade.php +++ b/resources/views/profile/embed.blade.php @@ -1,118 +1,110 @@ - - - + + - - {{ $title ?? config('app.name', 'Pixelfed') }} - - - - + {{ $title ?? config_cache('app.name', 'Pixelfed') }} + + + - - + -
-
-
- -
- {{config('pixelfed.domain.app')}} - +
+
+ +
+
+
+

+

Posts

+
+
+

+

Followers

+
+
+

Follow

+
+
+
+
+ +
-
-
-
-
-

-

Posts

-
-
-

-

Followers

-
-
-

Follow

-
-
-
-
- -
-
- - - - - - - + diff --git a/resources/views/profile/private.blade.php b/resources/views/profile/private.blade.php index ffff37d49..118ef643e 100644 --- a/resources/views/profile/private.blade.php +++ b/resources/views/profile/private.blade.php @@ -1,4 +1,4 @@ -@extends('layouts.app-guest',['title' => $user->username . " on " . config('app.name')]) +@extends('layouts.app-guest',['title' => $user->username . " on " . config_cache('app.name')]) @section('content') @if (session('error')) diff --git a/resources/views/settings/applications.blade.php b/resources/views/settings/applications.blade.php index 691ab0c81..97270fb62 100644 --- a/resources/views/settings/applications.blade.php +++ b/resources/views/settings/applications.blade.php @@ -6,7 +6,7 @@

Applications


-@if(config_cache('pixelfed.oauth_enabled') == true) +@if((bool) config_cache('pixelfed.oauth_enabled') == true) @else diff --git a/resources/views/settings/developers.blade.php b/resources/views/settings/developers.blade.php index 8b4c94471..22d869580 100644 --- a/resources/views/settings/developers.blade.php +++ b/resources/views/settings/developers.blade.php @@ -6,7 +6,7 @@

Developers


-@if(config_cache('pixelfed.oauth_enabled') == true) +@if((bool) config_cache('pixelfed.oauth_enabled') == true) @else

OAuth has not been enabled on this instance.

diff --git a/resources/views/settings/parental-controls/invite-register-form.blade.php b/resources/views/settings/parental-controls/invite-register-form.blade.php index 5b894e8d2..a21808efa 100644 --- a/resources/views/settings/parental-controls/invite-register-form.blade.php +++ b/resources/views/settings/parental-controls/invite-register-form.blade.php @@ -91,7 +91,7 @@ - @if(config('captcha.enabled') || config('captcha.active.register')) + @if((bool) config_cache('captcha.enabled'))
{!! Captcha::display() !!}
diff --git a/resources/views/settings/partial/sidebar.blade.php b/resources/views/settings/partial/sidebar.blade.php index b971e1f5d..0eadb9773 100644 --- a/resources/views/settings/partial/sidebar.blade.php +++ b/resources/views/settings/partial/sidebar.blade.php @@ -39,7 +39,7 @@
- @if(config_cache('pixelfed.oauth_enabled') == true) + @if((bool) config_cache('pixelfed.oauth_enabled') == true) diff --git a/resources/views/settings/privacy.blade.php b/resources/views/settings/privacy.blade.php index 369ddbbb4..9a0ca36a2 100644 --- a/resources/views/settings/privacy.blade.php +++ b/resources/views/settings/privacy.blade.php @@ -97,6 +97,14 @@

Display following count on profile

+
+ disable_embeds ? 'checked=""':''}}> + +

Disable post and profile embeds

+
+ @if(!$settings->is_private)
show_atom ? 'checked=""':''}}> diff --git a/resources/views/settings/relationships/home.blade.php b/resources/views/settings/relationships/home.blade.php index b246de49c..fcde2b43e 100644 --- a/resources/views/settings/relationships/home.blade.php +++ b/resources/views/settings/relationships/home.blade.php @@ -6,18 +6,14 @@

Relationships


- -
+ + @if(empty($data))

You are not {{$mode == 'hashtags' ? 'following any hashtags.' : ($mode == 'following' ? 'following anyone.' : 'followed by anyone.')}}

@else @@ -149,9 +145,7 @@ break; case 'unfollowhashtag': - axios.post('/api/local/discover/tag/subscribe', { - name: id - }).then(res => { + axios.post('/api/v1/tags/' + id + '/unfollow').then(res => { swal( 'Unfollow Successful', 'You have successfully unfollowed that hashtag', diff --git a/resources/views/site/index.blade.php b/resources/views/site/index.blade.php index e6753a727..b7d3befaa 100644 --- a/resources/views/site/index.blade.php +++ b/resources/views/site/index.blade.php @@ -8,7 +8,7 @@ - {{ config('app.name', 'Pixelfed') }} + {{ config_cache('app.name', 'Pixelfed') }} diff --git a/resources/views/status/embed-removed.blade.php b/resources/views/status/embed-removed.blade.php index e5f94525b..b9f0a2df6 100644 --- a/resources/views/status/embed-removed.blade.php +++ b/resources/views/status/embed-removed.blade.php @@ -9,8 +9,8 @@ Pixelfed | 404 Embed Not Found - - + + diff --git a/resources/views/status/embed.blade.php b/resources/views/status/embed.blade.php index 54d9b7330..7446ee528 100644 --- a/resources/views/status/embed.blade.php +++ b/resources/views/status/embed.blade.php @@ -1,172 +1,82 @@ - + - - - - - - - {{ $title ?? config('app.name', 'Pixelfed') }} - - - - - - - - - - - - + + + + + {{ $title ?? config_cache('app.name', 'Pixelfed') }} + + + + + + + + + + -
- @php($item = $status) -
- - - @php($status = $item) - @switch($status->viewType()) - @case('photo') - @case('image') - @if($status->is_nsfw) -
- -

CW / NSFW / Hidden Media

-

(click to show)

-
-
- - -
- @else -
- -
- @endif - @break - @case('photo:album') - - @break - @case('video') - @if($status->is_nsfw) -
- -

CW / NSFW / Hidden Media

-

(click to show)

-
-
- -
-
- @else -
- -
- @endif - @break - @case('video-album') - @if($status->is_nsfw) -
- -

CW / NSFW / Hidden Media

-

(click to show)

-
-
- -
-
- @else -
- -
- @endif - @break - @endswitch - - @if($layout != 'compact') -
- -
-
-

- - {{$item->profile->username}} - - @if($showCaption) - {!! $item->rendered ?? e($item->caption) !!} - @endif -

-
-
- @endif - -
-
- - - - - +
+
+ + @if($status['pf_type'] === 'photo') + +
+ +
+
+ @elseif($status['pf_type'] === 'photo:album') + + @foreach($status['media_attachments'] as $media) + + @endforeach + + @endif + @if($layout != 'compact') +
+ +
+
+

+ + {{$status['account']['username']}} + + @if($showCaption) + {{ $status['content_text'] }} + @endif +

+
+
+ @endif + +
+
+ diff --git a/resources/views/vendor/mail/html/message.blade.php b/resources/views/vendor/mail/html/message.blade.php index deec4a1f4..26c1f7d80 100644 --- a/resources/views/vendor/mail/html/message.blade.php +++ b/resources/views/vendor/mail/html/message.blade.php @@ -2,7 +2,7 @@ {{-- Header --}} @slot('header') @component('mail::header', ['url' => config('app.url')]) -{{ config('app.name') }} +{{ config_cache('app.name') }} @endcomponent @endslot @@ -21,7 +21,7 @@ {{-- Footer --}} @slot('footer') @component('mail::footer') -© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') +© {{ date('Y') }} {{ config_cache('app.name') }}. @lang('All rights reserved.') @endcomponent @endslot @endcomponent diff --git a/resources/views/vendor/mail/text/message.blade.php b/resources/views/vendor/mail/text/message.blade.php index 1ae9ed8f1..3416a9bd4 100644 --- a/resources/views/vendor/mail/text/message.blade.php +++ b/resources/views/vendor/mail/text/message.blade.php @@ -2,7 +2,7 @@ {{-- Header --}} @slot('header') @component('mail::header', ['url' => config('app.url')]) - {{ config('app.name') }} + {{ config_cache('app.name') }} @endcomponent @endslot @@ -21,7 +21,7 @@ {{-- Footer --}} @slot('footer') @component('mail::footer') - © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') + © {{ date('Y') }} {{ config_cache('app.name') }}. @lang('All rights reserved.') @endcomponent @endslot @endcomponent diff --git a/resources/views/vendor/passport/authorize.blade.php b/resources/views/vendor/passport/authorize.blade.php index 986f76801..a644289ca 100644 --- a/resources/views/vendor/passport/authorize.blade.php +++ b/resources/views/vendor/passport/authorize.blade.php @@ -4,7 +4,7 @@ - {{ config('app.name') }} - Authorization + {{ config_cache('app.name') }} - Authorization
+
+ + +
+
ID Username
+
+ + +
+
#{{ $record->id }} @@ -84,3 +112,200 @@ @include('admin.curated-register.partials.not-enabled') @endif @endsection + +@push('scripts') + +@endpush diff --git a/resources/views/admin/diagnostics/home.blade.php b/resources/views/admin/diagnostics/home.blade.php index db44a2332..b23652b51 100644 --- a/resources/views/admin/diagnostics/home.blade.php +++ b/resources/views/admin/diagnostics/home.blade.php @@ -66,7 +66,7 @@
  • OAUTH enabled: - {{ config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }} + {{ (bool) config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}
  • OAUTH token_expiration @@ -298,7 +298,7 @@
  • FEDERATION ACTIVITY_PUB{{config_cache('federation.activitypub.enabled') ? '✅ true' : '❌ false' }}{{(bool) config_cache('federation.activitypub.enabled') ? '✅ true' : '❌ false' }}
    FEDERATION
    FEDERATION PF_NETWORK_TIMELINE{{config_cache('federation.network_timeline') ? '✅ true' : '❌ false' }}{{(bool) config_cache('federation.network_timeline') ? '✅ true' : '❌ false' }}
    FEDERATION
    FEDERATION CUSTOM_EMOJI{{config_cache('federation.custom_emoji.enabled') ? '✅ true' : '❌ false' }}{{(bool) config_cache('federation.custom_emoji.enabled') ? '✅ true' : '❌ false' }}
    FEDERATION
    INSTANCE STORIES_ENABLED{{config_cache('instance.stories.enabled') ? '✅ true' : '❌ false' }}{{(bool) config_cache('instance.stories.enabled') ? '✅ true' : '❌ false' }}
    INSTANCE
    PIXELFED PF_ENABLE_CLOUD{{config_cache('pixelfed.cloud_storage') ? '✅ true' : '❌ false' }}{{(bool) config_cache('pixelfed.cloud_storage') ? '✅ true' : '❌ false' }}
    PIXELFED
    PIXELFED PF_OPTIMIZE_IMAGES{{config_cache('pixelfed.optimize_image') ? '✅ true' : '❌ false' }}{{(bool) config_cache('pixelfed.optimize_image') ? '✅ true' : '❌ false' }}
    PIXELFED PF_OPTIMIZE_VIDEOS{{config_cache('pixelfed.optimize_video') ? '✅ true' : '❌ false' }}{{(bool) config_cache('pixelfed.optimize_video') ? '✅ true' : '❌ false' }}
    PIXELFED
    PIXELFED OAUTH_ENABLED{{config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}{{ (bool) config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}
    PIXELFED PF_BOUNCER_ENABLED{{config_cache('pixelfed.bouncer.enabled') ? '✅ true' : '❌ false' }}{{(bool) config_cache('pixelfed.bouncer.enabled') ? '✅ true' : '❌ false' }}
    PIXELFED