forked from mirror/pixelfed
Compare commits
83 Commits
dev
...
l10n_stagi
Author | SHA1 | Date |
---|---|---|
daniel | 8a4aefd9c1 | |
daniel | 26647ec891 | |
daniel | 4a1e3eb20e | |
daniel | b209bca393 | |
daniel | d7dbd78532 | |
daniel | d6353b7d1d | |
daniel | 19ece2a2f3 | |
daniel | 13db7d3756 | |
daniel | 9d45f4ed3b | |
daniel | 10db7eedb4 | |
daniel | 0a1b2eafc3 | |
daniel | 55188be6e1 | |
daniel | 7edb3c1329 | |
daniel | 173d08f1a7 | |
daniel | 58c955e0ba | |
daniel | ae091ed607 | |
daniel | 5b5c043f0e | |
daniel | 389e2e07fd | |
daniel | 84ed608c44 | |
daniel | 0ea92c090c | |
daniel | 3b7758b15f | |
daniel | 358b1d9f9d | |
daniel | d6135ccdb9 | |
daniel | c696ad4fbe | |
daniel | e45018b38e | |
daniel | 866328576c | |
daniel | b6b8b2ffb4 | |
daniel | 30591bd867 | |
daniel | 914137b6a7 | |
daniel | 5e0fc6ca4b | |
daniel | 1699ce840f | |
daniel | 438bdea1fb | |
daniel | a2b59dd07e | |
daniel | e3e18c6a75 | |
daniel | b7ae83feb4 | |
daniel | 858071d008 | |
daniel | aaaa27863d | |
daniel | ff57f05395 | |
daniel | a3d2320455 | |
daniel | ddca8b3a86 | |
daniel | 80be2c6c80 | |
daniel | cee9fb5341 | |
daniel | 60ad0bac16 | |
daniel | 433b6d7938 | |
daniel | 9c31568fda | |
daniel | a70a65ef05 | |
daniel | 6b36fa9b3c | |
daniel | 4a746a69f9 | |
daniel | 9617a71d2d | |
daniel | a9a84ab2e8 | |
daniel | 280841f88b | |
daniel | 1fe8b748f2 | |
daniel | e6085838ff | |
daniel | 8f7ad39498 | |
daniel | 310fb61d9b | |
daniel | 343f3c215e | |
daniel | 09ccda9a60 | |
daniel | 7b2be137b9 | |
daniel | 1863db640e | |
daniel | 4497ac872e | |
daniel | df8ef8e02c | |
daniel | d4c5450928 | |
daniel | 5f60e321bc | |
daniel | 7063b0507c | |
daniel | f999424751 | |
daniel | e677c09430 | |
daniel | 9a0cdcd2da | |
daniel | 557190fed1 | |
daniel | e44bac7b98 | |
daniel | e66fb636be | |
daniel | 1db9824065 | |
daniel | 36e183a220 | |
daniel | 09db33c7ce | |
daniel | 4bae1d3b42 | |
daniel | 190d4f2844 | |
daniel | af142fa652 | |
daniel | 34f1c99861 | |
daniel | a29deda5b0 | |
daniel | 30d916a3e8 | |
daniel | 61c35dff70 | |
daniel | db221c14d2 | |
daniel | 4fc9a887bb | |
daniel | b6603ea46d |
|
@ -7,7 +7,7 @@ jobs:
|
|||
build:
|
||||
docker:
|
||||
# Specify the version you desire here
|
||||
- image: cimg/php:8.2.5
|
||||
- image: cimg/php:7.4.26
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
|
@ -21,12 +21,7 @@ jobs:
|
|||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: "Create Environment file and generate app key"
|
||||
command: |
|
||||
mv .env.testing .env
|
||||
|
||||
- run: sudo apt install zlib1g-dev libsqlite3-dev
|
||||
- run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
|
||||
|
||||
# Download and cache dependencies
|
||||
|
||||
|
@ -34,24 +29,25 @@ jobs:
|
|||
- restore_cache:
|
||||
keys:
|
||||
# "composer.lock" can be used if it is committed to the repo
|
||||
- v2-dependencies-{{ checksum "composer.json" }}
|
||||
- v1-dependencies-{{ checksum "composer.json" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v2-dependencies-
|
||||
- v1-dependencies-
|
||||
|
||||
- run: composer install -n --prefer-dist
|
||||
|
||||
- save_cache:
|
||||
key: v2-dependencies-{{ checksum "composer.json" }}
|
||||
key: composer-v1-{{ checksum "composer.lock" }}
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
- run: php artisan config:cache
|
||||
- run: cp .env.testing .env
|
||||
- run: php artisan route:clear
|
||||
- run: php artisan storage:link
|
||||
- run: php artisan key:generate
|
||||
- run: php artisan config:clear
|
||||
|
||||
# run tests with phpunit or codecept
|
||||
- run: php artisan test
|
||||
- run: ./vendor/bin/phpunit
|
||||
- store_test_results:
|
||||
path: tests/_output
|
||||
- store_artifacts:
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/bash
|
||||
#ddev-generated
|
||||
## Description: Run redis-cli inside the redis container
|
||||
## Usage: redis-cli [flags] [args]
|
||||
## Example: "redis-cli KEYS *" or "ddev redis-cli INFO" or "ddev redis-cli --version"
|
||||
|
||||
exec redis-cli -p 6379 -h redis "$@"
|
|
@ -1,32 +0,0 @@
|
|||
type: laravel
|
||||
docroot: public
|
||||
php_version: "8.1"
|
||||
webserver_type: nginx-fpm
|
||||
database:
|
||||
type: mariadb
|
||||
version: "10.4"
|
||||
disable_settings_management: true
|
||||
web_environment:
|
||||
- DB_CONNECTION=mysql
|
||||
- DB_HOST=ddev-pixelfed-db
|
||||
- DB_DATABASE=db
|
||||
- DB_USERNAME=db
|
||||
- DB_PASSWORD=db
|
||||
- REDIS_HOST=ddev-pixelfed-redis
|
||||
- MAIL_DRIVER=smtp
|
||||
- MAIL_HOST=localhost
|
||||
- MAIL_PORT=1025
|
||||
- MAIL_USERNAME=null
|
||||
- MAIL_PASSWORD=null
|
||||
- MAIL_ENCRYPTION=null
|
||||
- APP_KEY=placeholder
|
||||
- APP_NAME=PixelfedTest
|
||||
- APP_ENV=local
|
||||
- APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E=
|
||||
- APP_DEBUG=true
|
||||
- APP_URL=https://pixelfed.ddev.site
|
||||
- APP_DOMAIN=pixelfed.ddev.site
|
||||
- ADMIN_DOMAIN=pixelfed.ddev.site
|
||||
- SESSION_DOMAIN=pixelfed.ddev.site
|
||||
- "TRUST_PROXIES=*"
|
||||
- LOG_CHANNEL=stack
|
|
@ -1,14 +0,0 @@
|
|||
#ddev-generated
|
||||
version: '3.6'
|
||||
services:
|
||||
redis:
|
||||
container_name: ddev-${DDEV_SITENAME}-redis
|
||||
image: redis:6
|
||||
# These labels ensure this service is discoverable by ddev.
|
||||
labels:
|
||||
com.ddev.site-name: ${DDEV_SITENAME}
|
||||
com.ddev.approot: $DDEV_APPROOT
|
||||
volumes:
|
||||
- ".:/mnt/ddev_config"
|
||||
- "./redis:/usr/local/etc/redis"
|
||||
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
|
|
@ -1,8 +0,0 @@
|
|||
# Redis configuration.
|
||||
# #ddev-generated
|
||||
# Example configuration files for reference:
|
||||
# http://download.redis.io/redis-stable/redis.conf
|
||||
# http://download.redis.io/redis-stable/sentinel.conf
|
||||
|
||||
maxmemory 2048mb
|
||||
maxmemory-policy allkeys-lfu
|
|
@ -1,30 +1,8 @@
|
|||
.DS_Store
|
||||
/.bash_history
|
||||
/.bash_profile
|
||||
/.bashrc
|
||||
/.composer
|
||||
/.env
|
||||
/.env.dottie-backup
|
||||
/.git
|
||||
/.git-credentials
|
||||
/.gitconfig
|
||||
/.gitignore
|
||||
/.idea
|
||||
/.vagrant
|
||||
/bootstrap/cache
|
||||
/docker-compose-state/
|
||||
/Homestead.json
|
||||
/Homestead.yaml
|
||||
/node_modules
|
||||
/npm-debug.log
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/vendor/horizon
|
||||
/storage/*.key
|
||||
/storage/docker
|
||||
/vendor
|
||||
/yarn-error.log
|
||||
|
||||
# Exceptions - these *MUST* be last
|
||||
!/bootstrap/cache/.gitignore
|
||||
!/public/vendor/horizon/.gitignore
|
||||
data
|
||||
Dockerfile
|
||||
contrib/docker/Dockerfile.*
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
|
|
|
@ -7,21 +7,3 @@ end_of_line = lf
|
|||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{sh,envsh,env,env*}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# ShellCheck config
|
||||
shell_variant = bash # like -ln=bash
|
||||
binary_next_line = true # like -bn
|
||||
switch_case_indent = true # like -ci
|
||||
space_redirects = false # like -sr
|
||||
keep_padding = false # like -kp
|
||||
function_next_line = true # like -fn
|
||||
never_split = true # like -ns
|
||||
simplify = true
|
||||
|
|
1393
.env.docker
1393
.env.docker
File diff suppressed because it is too large
Load Diff
89
.env.example
89
.env.example
|
@ -1,62 +1,33 @@
|
|||
APP_NAME="Pixelfed"
|
||||
APP_ENV="production"
|
||||
APP_NAME="Pixelfed Prod"
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG="false"
|
||||
APP_DEBUG=false
|
||||
|
||||
# Instance Configuration
|
||||
OPEN_REGISTRATION="false"
|
||||
ENFORCE_EMAIL_VERIFICATION="false"
|
||||
PF_MAX_USERS="1000"
|
||||
OAUTH_ENABLED="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_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"
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_CLIENT="predis"
|
||||
REDIS_SCHEME="tcp"
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PASSWORD="null"
|
||||
REDIS_PORT="6379"
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=pixelfed
|
||||
DB_USERNAME=pixelfed
|
||||
DB_PASSWORD=pixelfed
|
||||
|
||||
# Laravel Configuration
|
||||
SESSION_DRIVER="database"
|
||||
CACHE_DRIVER="redis"
|
||||
QUEUE_DRIVER="redis"
|
||||
BROADCAST_DRIVER="log"
|
||||
LOG_CHANNEL="stack"
|
||||
HORIZON_PREFIX="horizon-"
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=redis
|
||||
SESSION_DRIVER=redis
|
||||
QUEUE_DRIVER=redis
|
||||
|
||||
# ActivityPub Configuration
|
||||
ACTIVITY_PUB="false"
|
||||
AP_REMOTE_FOLLOW="false"
|
||||
AP_INBOX="false"
|
||||
AP_OUTBOX="false"
|
||||
AP_SHAREDINBOX="false"
|
||||
REDIS_SCHEME=tcp
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Experimental Configuration
|
||||
EXP_EMC="true"
|
||||
|
||||
## Mail Configuration (Post-Installer)
|
||||
MAIL_DRIVER=log
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
|
@ -66,13 +37,15 @@ 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=<BucketName>
|
||||
#AWS_URL=
|
||||
#AWS_ENDPOINT=
|
||||
#AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
OPEN_REGISTRATION=true
|
||||
ENFORCE_EMAIL_VERIFICATION=true
|
||||
PF_MAX_USERS=1000
|
||||
|
||||
MAX_PHOTO_SIZE=15000
|
||||
MAX_CAPTION_LENGTH=150
|
||||
MAX_ALBUM_LENGTH=4
|
||||
|
||||
ACTIVITY_PUB=false
|
||||
AP_REMOTE_FOLLOW=false
|
||||
AP_INBOX=false
|
||||
PF_COSTAR_ENABLED=false
|
||||
|
|
10
.env.testing
10
.env.testing
|
@ -1,5 +1,3 @@
|
|||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
APP_NAME="Pixelfed Test"
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E=
|
||||
|
@ -30,8 +28,6 @@ REDIS_HOST=127.0.0.1
|
|||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
HORIZON_PREFIX="horizon-"
|
||||
|
||||
MAIL_DRIVER=log
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
|
@ -64,8 +60,6 @@ CS_BLOCKED_DOMAINS='example.org,example.net,example.com'
|
|||
CS_CW_DOMAINS='example.org,example.net,example.com'
|
||||
CS_UNLISTED_DOMAINS='example.org,example.net,example.com'
|
||||
|
||||
## Optional
|
||||
## Optional
|
||||
#HORIZON_DARKMODE=false # Horizon theme darkmode
|
||||
#HORIZON_EMBED=false # Single Docker Container mode
|
||||
|
||||
ENABLE_CONFIG_CACHE=false
|
||||
#HORIZON_EMBED=false # Single Docker Container mode
|
||||
|
|
|
@ -3,10 +3,3 @@
|
|||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
CHANGELOG.md export-ignore
|
||||
|
||||
# Collapse diffs for generated files:
|
||||
public/**/*.js text -diff
|
||||
public/**/*.json text -diff
|
||||
public/**/*.css text -diff
|
||||
public/img/* binary -diff
|
||||
public/fonts/* binary -diff
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "composer"
|
||||
directory: "/"
|
||||
target-branch: "staging"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
versioning-strategy: lockfile-only
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
target-branch: "staging"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
versioning-strategy: lockfile-only
|
|
@ -1,230 +0,0 @@
|
|||
---
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
|
||||
workflow_dispatch:
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: hadolint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Lint
|
||||
uses: hadolint/hadolint-action@v3.1.0
|
||||
with:
|
||||
dockerfile: Dockerfile
|
||||
failure-threshold: error
|
||||
|
||||
shellcheck:
|
||||
name: ShellCheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run ShellCheck
|
||||
uses: ludeeus/action-shellcheck@master
|
||||
env:
|
||||
SHELLCHECK_OPTS: --shell=bash --external-sources
|
||||
with:
|
||||
version: v0.9.0
|
||||
additional_files: "*.envsh .env .env.docker .env.example .env.testing"
|
||||
|
||||
bats:
|
||||
name: Bats Testing
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run bats
|
||||
run: docker run -v "$PWD:/var/www" bats/bats:latest /var/www/tests/bats
|
||||
|
||||
build:
|
||||
name: Build, Test, and Push
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
|
||||
matrix:
|
||||
php_version:
|
||||
- 8.2
|
||||
- 8.3
|
||||
target_runtime:
|
||||
- apache
|
||||
- fpm
|
||||
- nginx
|
||||
php_base:
|
||||
- apache
|
||||
- fpm
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#excluding-matrix-configurations
|
||||
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixexclude
|
||||
exclude:
|
||||
# targeting [apache] runtime with [fpm] base type doesn't make sense
|
||||
- target_runtime: apache
|
||||
php_base: fpm
|
||||
|
||||
# targeting [fpm] runtime with [apache] base type doesn't make sense
|
||||
- target_runtime: fpm
|
||||
php_base: apache
|
||||
|
||||
# targeting [nginx] runtime with [apache] base type doesn't make sense
|
||||
- target_runtime: nginx
|
||||
php_base: apache
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-concurrency-and-the-default-behavior
|
||||
concurrency:
|
||||
group: docker-build-${{ github.ref }}-${{ matrix.php_base }}-${{ matrix.php_version }}-${{ matrix.target_runtime }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
# Set the repo variable [DOCKER_HUB_USERNAME] to override the default
|
||||
# at https://github.com/<user>/<project>/settings/variables/actions
|
||||
DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'pixelfed' }}
|
||||
|
||||
# Set the repo variable [DOCKER_HUB_ORGANISATION] to override the default
|
||||
# at https://github.com/<user>/<project>/settings/variables/actions
|
||||
DOCKER_HUB_ORGANISATION: ${{ vars.DOCKER_HUB_ORGANISATION || 'pixelfed' }}
|
||||
|
||||
# Set the repo variable [DOCKER_HUB_REPO] to override the default
|
||||
# at https://github.com/<user>/<project>/settings/variables/actions
|
||||
DOCKER_HUB_REPO: ${{ vars.DOCKER_HUB_REPO || 'pixelfed' }}
|
||||
|
||||
# For Docker Hub pushing to work, you need the secret [DOCKER_HUB_TOKEN]
|
||||
# set to your Personal Access Token at https://github.com/<user>/<project>/settings/secrets/actions
|
||||
#
|
||||
# ! NOTE: no [login] or [push] will happen to Docker Hub until this secret is set!
|
||||
HAS_DOCKER_HUB_CONFIGURED: ${{ secrets.DOCKER_HUB_TOKEN != '' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
id: buildx
|
||||
with:
|
||||
version: v0.12.0 # *or* newer, needed for annotations to work
|
||||
|
||||
# See: https://github.com/docker/login-action?tab=readme-ov-file#github-container-registry
|
||||
- name: Log in to the GitHub Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# See: https://github.com/docker/login-action?tab=readme-ov-file#docker-hub
|
||||
- name: Login to Docker Hub registry (conditionally)
|
||||
if: ${{ env.HAS_DOCKER_HUB_CONFIGURED == true }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ env.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Docker meta
|
||||
uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository }},enable=true
|
||||
name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }},enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }}
|
||||
tags: |
|
||||
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }}
|
||||
type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }}
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=branch,prefix=branch-
|
||||
type=ref,event=pr,prefix=pr-
|
||||
type=ref,event=tag
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
|
||||
- name: Docker meta (Cache)
|
||||
uses: docker/metadata-action@v5
|
||||
id: cache
|
||||
with:
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository }}-cache,enable=true
|
||||
name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }}-cache,enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }}
|
||||
tags: |
|
||||
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }}
|
||||
type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }}
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=branch,prefix=branch-
|
||||
type=ref,event=pr,prefix=pr-
|
||||
type=ref,event=tag
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
target: ${{ matrix.target_runtime }}-runtime
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
push: true
|
||||
sbom: true
|
||||
provenance: true
|
||||
build-args: |
|
||||
PHP_VERSION=${{ matrix.php_version }}
|
||||
PHP_BASE_TYPE=${{ matrix.php_base }}
|
||||
cache-from: |
|
||||
type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}
|
||||
${{ steps.cache.outputs.tags }}
|
||||
|
||||
# goss validate the image
|
||||
#
|
||||
# See: https://github.com/goss-org/goss
|
||||
- uses: e1himself/goss-installation-action@v1
|
||||
with:
|
||||
version: "v0.4.4"
|
||||
- name: Execute Goss tests
|
||||
run: |
|
||||
dgoss run \
|
||||
-v "./.env.testing:/var/www/.env" \
|
||||
-e "EXPECTED_PHP_VERSION=${{ matrix.php_version }}" \
|
||||
-e "PHP_BASE_TYPE=${{ matrix.php_base }}" \
|
||||
${{ steps.meta.outputs.tags }}
|
|
@ -1,31 +1,22 @@
|
|||
.DS_Store
|
||||
/.bash_history
|
||||
/.bash_profile
|
||||
/.bashrc
|
||||
/.composer
|
||||
/.env
|
||||
/.env.dottie-backup
|
||||
#/.git
|
||||
/.git-credentials
|
||||
/.gitconfig
|
||||
#/.gitignore
|
||||
/.idea
|
||||
/.vagrant
|
||||
/bootstrap/cache
|
||||
/docker-compose-state/
|
||||
/Homestead.json
|
||||
/Homestead.yaml
|
||||
/node_modules
|
||||
/npm-debug.log
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/vendor/horizon
|
||||
/storage/*.key
|
||||
/storage/docker
|
||||
/vendor
|
||||
/yarn-error.log
|
||||
/public/build
|
||||
|
||||
# Exceptions - these *MUST* be last
|
||||
!/bootstrap/cache/.gitignore
|
||||
!/public/vendor/horizon/.gitignore
|
||||
/.idea
|
||||
/.vscode
|
||||
/.vagrant
|
||||
/docker-volumes
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.env
|
||||
.DS_Store
|
||||
.bash_profile
|
||||
.bash_history
|
||||
.bashrc
|
||||
.gitconfig
|
||||
.git-credentials
|
||||
/.composer/
|
||||
/nginx.conf
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
ignored:
|
||||
- DL3002 # warning: Last USER should not be root
|
||||
- DL3008 # warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
|
||||
- SC2046 # warning: Quote this to prevent word splitting.
|
||||
- SC2086 # info: Double quote to prevent globbing and word splitting.
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"MD013": false,
|
||||
"MD014": false
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
v14.20.1
|
|
@ -1,12 +0,0 @@
|
|||
# See: https://github.com/koalaman/shellcheck/blob/master/shellcheck.1.md#rc-files
|
||||
|
||||
source-path=SCRIPTDIR
|
||||
|
||||
# Allow opening any 'source'd file, even if not specified as input
|
||||
external-sources=true
|
||||
|
||||
# Turn on warnings for unquoted variables with safe values
|
||||
enable=quote-safe-variables
|
||||
|
||||
# Turn on warnings for unassigned uppercase variables
|
||||
enable=check-unassigned-uppercase
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"foxundermoon.shell-format",
|
||||
"timonwong.shellcheck",
|
||||
"jetmartin.bats",
|
||||
"aaron-bond.better-comments",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"editorconfig.editorconfig",
|
||||
"github.vscode-github-actions",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"redhat.vscode-yaml",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"shellformat.useEditorConfig": true,
|
||||
"[shellscript]": {
|
||||
"files.eol": "\n",
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-yaml"
|
||||
},
|
||||
"[dockercompose]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-yaml",
|
||||
"editor.autoIndent": "advanced",
|
||||
},
|
||||
"yaml.schemas": {
|
||||
"https://json.schemastore.org/composer": "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json"
|
||||
},
|
||||
"files.associations": {
|
||||
".env": "shellscript",
|
||||
".env.*": "shellscript"
|
||||
}
|
||||
}
|
797
CHANGELOG.md
797
CHANGELOG.md
|
@ -1,785 +1,6 @@
|
|||
# Release Notes
|
||||
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.13...dev)
|
||||
|
||||
### Updates
|
||||
|
||||
- Update SoftwareUpdateService, add command to refresh latest versions ([632f2cb6](https://github.com/pixelfed/pixelfed/commit/632f2cb6))
|
||||
- Update Post.vue, fix cache bug ([3a27e637](https://github.com/pixelfed/pixelfed/commit/3a27e637))
|
||||
- Update StatusHashtagService, use more efficient cached count ([592c8412](https://github.com/pixelfed/pixelfed/commit/592c8412))
|
||||
- Update DiscoverController, handle discover hashtag redirects ([18382e8a](https://github.com/pixelfed/pixelfed/commit/18382e8a))
|
||||
- Update ApiV1Controller, use admin filter service ([94503a1c](https://github.com/pixelfed/pixelfed/commit/94503a1c))
|
||||
- Update SearchApiV2Service, use more efficient query ([cee618e8](https://github.com/pixelfed/pixelfed/commit/cee618e8))
|
||||
- Update Curated Onboarding view, fix concierge form ([15ad69f7](https://github.com/pixelfed/pixelfed/commit/15ad69f7))
|
||||
- Update AP Profile Transformer, add `suspended` attribute ([25f3fa06](https://github.com/pixelfed/pixelfed/commit/25f3fa06))
|
||||
- Update AP Profile Transformer, fix movedTo attribute ([63100fe9](https://github.com/pixelfed/pixelfed/commit/63100fe9))
|
||||
- Update AP Profile Transformer, fix suspended attributes ([2e5e68e4](https://github.com/pixelfed/pixelfed/commit/2e5e68e4))
|
||||
- Update PrivacySettings controller, add cache invalidation ([e742d595](https://github.com/pixelfed/pixelfed/commit/e742d595))
|
||||
- Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup ([853a729f](https://github.com/pixelfed/pixelfed/commit/853a729f))
|
||||
- Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable ([95199843](https://github.com/pixelfed/pixelfed/commit/95199843))
|
||||
- Update AP transformers, add DeleteActor activity ([bcce1df6](https://github.com/pixelfed/pixelfed/commit/bcce1df6))
|
||||
- Update commands, add user account delete cli command to federate account deletion ([4aa0e25f](https://github.com/pixelfed/pixelfed/commit/4aa0e25f))
|
||||
- Update web-api popular accounts route to its own method to remove the breaking oauth scope bug ([a4bc5ce3](https://github.com/pixelfed/pixelfed/commit/a4bc5ce3))
|
||||
- Update config cache ([5e4d4eff](https://github.com/pixelfed/pixelfed/commit/5e4d4eff))
|
||||
- Update Config, use config_cache ([7785a2da](https://github.com/pixelfed/pixelfed/commit/7785a2da))
|
||||
- Update ApiV1Dot1Controller, use config_cache for in-app registration ([b0cb4456](https://github.com/pixelfed/pixelfed/commit/b0cb4456))
|
||||
- Update captcha, use config_cache helper ([8a89e3c9](https://github.com/pixelfed/pixelfed/commit/8a89e3c9))
|
||||
- Update custom emoji, add config_cache support ([481314cd](https://github.com/pixelfed/pixelfed/commit/481314cd))
|
||||
- Update ProfileController, fix permalink redirect bug ([75081e60](https://github.com/pixelfed/pixelfed/commit/75081e60))
|
||||
- Update admin css, use font-display:swap for nucleo icons ([8a0c456e](https://github.com/pixelfed/pixelfed/commit/8a0c456e))
|
||||
- Update PixelfedDirectoryController, fix boolean cast bug ([f08aab22](https://github.com/pixelfed/pixelfed/commit/f08aab22))
|
||||
- Update PixelfedDirectoryController, use cached stats ([f2f2a809](https://github.com/pixelfed/pixelfed/commit/f2f2a809))
|
||||
- Update AdminDirectoryController, fix type casting ([ad506e90](https://github.com/pixelfed/pixelfed/commit/ad506e90))
|
||||
- Update image pipeline, use config_cache ([a72188a7](https://github.com/pixelfed/pixelfed/commit/a72188a7))
|
||||
- Update cloud storage, use config_cache ([665581d8](https://github.com/pixelfed/pixelfed/commit/665581d8))
|
||||
- Update pixelfed.max_album_length, use config_cache ([fecbe189](https://github.com/pixelfed/pixelfed/commit/fecbe189))
|
||||
- Update media_types, use config_cache ([d670de17](https://github.com/pixelfed/pixelfed/commit/d670de17))
|
||||
- Update landing settings, use config_cache ([40478f25](https://github.com/pixelfed/pixelfed/commit/40478f25))
|
||||
- Update activitypub setting, use config_cache ([5071aaf4](https://github.com/pixelfed/pixelfed/commit/5071aaf4))
|
||||
- Update oauth setting, use config_cache ([ce228f7f](https://github.com/pixelfed/pixelfed/commit/ce228f7f))
|
||||
- Update stories config, use config_cache ([d1adb109](https://github.com/pixelfed/pixelfed/commit/d1adb109))
|
||||
- Update ig import, use config_cache ([da0e0ffa](https://github.com/pixelfed/pixelfed/commit/da0e0ffa))
|
||||
- Update autospam config, use config_cache ([a76cb5f4](https://github.com/pixelfed/pixelfed/commit/a76cb5f4))
|
||||
- Update app.name config, use config_cache ([911446c0](https://github.com/pixelfed/pixelfed/commit/911446c0))
|
||||
- Update UserObserver, fix type casting ([949e9979](https://github.com/pixelfed/pixelfed/commit/949e9979))
|
||||
- Update user_filters, use config_cache ([6ce513f8](https://github.com/pixelfed/pixelfed/commit/6ce513f8))
|
||||
- Update filesystems config, add to config_cache ([087b2791](https://github.com/pixelfed/pixelfed/commit/087b2791))
|
||||
- Update web-admin routes, add setting api routes ([828a456f](https://github.com/pixelfed/pixelfed/commit/828a456f))
|
||||
- Update hashtag component ([cee979ed](https://github.com/pixelfed/pixelfed/commit/cee979ed))
|
||||
- Update AdminReadMore component, add .prevent to click action ([704e7b12](https://github.com/pixelfed/pixelfed/commit/704e7b12))
|
||||
- Update admin dashboard, add admin settings partials ([eb487123](https://github.com/pixelfed/pixelfed/commit/eb487123))
|
||||
- Update admin settings, refactor to vue component ([674e560f](https://github.com/pixelfed/pixelfed/commit/674e560f))
|
||||
- Update ConfigCacheService, encrypt keys at rest ([3628b462](https://github.com/pixelfed/pixelfed/commit/3628b462))
|
||||
- Update RemoteFollowImportRecent, use MediaPathService ([5162c070](https://github.com/pixelfed/pixelfed/commit/5162c070))
|
||||
- Update AdminSettingsController, add user filter max limit settings ([ac1f0748](https://github.com/pixelfed/pixelfed/commit/ac1f0748))
|
||||
- Update AdminSettingsController, add AdminSettingsService ([dcc5f416](https://github.com/pixelfed/pixelfed/commit/dcc5f416))
|
||||
- Update AdminSettings component, fix user settings ([aba1e13d](https://github.com/pixelfed/pixelfed/commit/aba1e13d))
|
||||
- Update AdminInstances component ([ec2fdd61](https://github.com/pixelfed/pixelfed/commit/ec2fdd61))
|
||||
- Update AdminSettings, add max_account_size support ([2dcbc1d5](https://github.com/pixelfed/pixelfed/commit/2dcbc1d5))
|
||||
- Update AdminSettings, use better validation for user integer settings ([d946afcc](https://github.com/pixelfed/pixelfed/commit/d946afcc))
|
||||
- Update spa sass, fix timestamp dark mode bug ([4147f7c5](https://github.com/pixelfed/pixelfed/commit/4147f7c5))
|
||||
- Update relationships view, fix unfollow hashtag bug. Fixes #5008 ([8c693640](https://github.com/pixelfed/pixelfed/commit/8c693640))
|
||||
- Update PrivacySettings controller, refresh RelationshipService when unmute/unblocking ([b7322b68](https://github.com/pixelfed/pixelfed/commit/b7322b68))
|
||||
- Update ApiV1Controller, improve refresh relations logic when (un)muting or (un)blocking ([b8e96a5f](https://github.com/pixelfed/pixelfed/commit/b8e96a5f))
|
||||
- Update context menu, add mute/block/unfollow actions and update relationship store accordingly ([81d1e0fd](https://github.com/pixelfed/pixelfed/commit/81d1e0fd))
|
||||
- Update docker env, fix config_cache. Fixes #5033 ([858fcbf6](https://github.com/pixelfed/pixelfed/commit/858fcbf6))
|
||||
- Update UnfollowPipeline, fix follower count cache bug ([6bdf73de](https://github.com/pixelfed/pixelfed/commit/6bdf73de))
|
||||
- Update VideoPresenter component, add webkit-playsinline attribute to video element to prevent the full screen video player ([ad032916](https://github.com/pixelfed/pixelfed/commit/ad032916))
|
||||
- Update VideoPlayer component, add playsinline attribute to video element ([8af23607](https://github.com/pixelfed/pixelfed/commit/8af23607))
|
||||
- Update StatusController, refactor status embeds ([9a7acc12](https://github.com/pixelfed/pixelfed/commit/9a7acc12))
|
||||
- Update ProfileController, refactor profile embeds ([8b8b1ffc](https://github.com/pixelfed/pixelfed/commit/8b8b1ffc))
|
||||
- Update profile embed view, fix height bug ([65166570](https://github.com/pixelfed/pixelfed/commit/65166570))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13)
|
||||
|
||||
### Features
|
||||
|
||||
- Account Migrations ([#4968](https://github.com/pixelfed/pixelfed/pull/4968)) ([4a6be6212](https://github.com/pixelfed/pixelfed/pull/4968/commits/4a6be6212))
|
||||
- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
|
||||
- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4))
|
||||
- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e))
|
||||
- Improved Docker Support ([#4844](https://github.com/pixelfed/pixelfed/pull/4844)) ([d92cf7f](https://github.com/pixelfed/pixelfed/commit/d92cf7f))
|
||||
|
||||
### Updates
|
||||
|
||||
- Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad))
|
||||
- Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4))
|
||||
- Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623))
|
||||
- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9))
|
||||
- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e))
|
||||
- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d))
|
||||
- Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641))
|
||||
- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
|
||||
- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
|
||||
- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
|
||||
- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8))
|
||||
- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac))
|
||||
- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c))
|
||||
- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e))
|
||||
- Update Inbox, fix flag validation condition, allow profile reports ([402a4607](https://github.com/pixelfed/pixelfed/commit/402a4607))
|
||||
- Update AccountTransformer, fix follower/following count visibility bug ([542d1106](https://github.com/pixelfed/pixelfed/commit/542d1106))
|
||||
- Update ProfileMigration model, add target relation ([3f053997](https://github.com/pixelfed/pixelfed/commit/3f053997))
|
||||
- Update ApiV1Controller, update Notifications endpoint to filter notifications with missing activities ([a933615b](https://github.com/pixelfed/pixelfed/commit/a933615b))
|
||||
- Update ApiV1Controller, fix public timeline scope, properly support both local + remote parameters ([d6eac655](https://github.com/pixelfed/pixelfed/commit/d6eac655))
|
||||
- Update ApiV1Controller, handle public feed parameter bug to gracefully fallback to min_id=1 when max_id=0 ([e3826c58](https://github.com/pixelfed/pixelfed/commit/e3826c58))
|
||||
- Update ApiV1Controller, fix hashtag feed to include private posts from accounts you follow or your own, and your own unlisted posts ([3b5500b3](https://github.com/pixelfed/pixelfed/commit/3b5500b3))
|
||||
- Update checkpoint view, improve input autocomplete. Fixes ([#4959](https://github.com/pixelfed/pixelfed/pull/4959)) ([d18824e7](https://github.com/pixelfed/pixelfed/commit/d18824e7))
|
||||
- Update navbar.vue, removes the 50px limit ([#4969](https://github.com/pixelfed/pixelfed/pull/4969)) ([7fd5599](https://github.com/pixelfed/pixelfed/commit/7fd5599))
|
||||
- Update ComposeModal.vue, add an informative UI error message when trying to create a mixed media album ([#4886](https://github.com/pixelfed/pixelfed/pull/4886)) ([fd4f41a](https://github.com/pixelfed/pixelfed/commit/fd4f41a))
|
||||
|
||||
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)
|
||||
|
||||
### Features
|
||||
- Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a))
|
||||
- Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988))
|
||||
|
||||
### Updates
|
||||
|
||||
- Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3))
|
||||
- Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc))
|
||||
- Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3))
|
||||
- Update ActivityPubFetchService, enforce stricter Content-Type validation ([1232cfc8](https://github.com/pixelfed/pixelfed/commit/1232cfc8))
|
||||
- Update status view, fix unlisted/private scope bug ([0f3ca194](https://github.com/pixelfed/pixelfed/commit/0f3ca194))
|
||||
|
||||
## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11)
|
||||
|
||||
### Fixes
|
||||
- Fix api endpoints ([fd7f5dbb](https://github.com/pixelfed/pixelfed/commit/fd7f5dbb))
|
||||
|
||||
## [v0.11.10 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.9...v0.11.10)
|
||||
|
||||
### Added
|
||||
- Resilient Media Storage ([#4665](https://github.com/pixelfed/pixelfed/pull/4665)) ([fb1deb6](https://github.com/pixelfed/pixelfed/commit/fb1deb6))
|
||||
- Video WebP2P ([#4713](https://github.com/pixelfed/pixelfed/pull/4713)) ([0405ef12](https://github.com/pixelfed/pixelfed/commit/0405ef12))
|
||||
- Added user:2fa command to easily disable 2FA for given account ([c6408fd7](https://github.com/pixelfed/pixelfed/commit/c6408fd7))
|
||||
- Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde))
|
||||
- Added S3 command to rewrite media urls ([5b3a5610](https://github.com/pixelfed/pixelfed/commit/5b3a5610))
|
||||
- Experimental home feed ([#4752](https://github.com/pixelfed/pixelfed/pull/4752)) ([c39b9afb](https://github.com/pixelfed/pixelfed/commit/c39b9afb))
|
||||
- Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6))
|
||||
- Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7))
|
||||
- Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46))
|
||||
- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac))
|
||||
- Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59))
|
||||
- Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1))
|
||||
- Added S3 IG Import Media Storage support ([#4891](https://github.com/pixelfed/pixelfed/pull/4891)) ([081360b9](https://github.com/pixelfed/pixelfed/commit/081360b9))
|
||||
|
||||
### Federation
|
||||
- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))
|
||||
- Update AP Helpers, consume actor `indexable` attribute ([fbdcdd9d](https://github.com/pixelfed/pixelfed/commit/fbdcdd9d))
|
||||
|
||||
### Updates
|
||||
- Update FollowerService, add forget method to RelationshipService call to reduce load when mass purging ([347e4f59](https://github.com/pixelfed/pixelfed/commit/347e4f59))
|
||||
- Update FollowServiceWarmCache, improve handling larger following/follower lists ([61a6d904](https://github.com/pixelfed/pixelfed/commit/61a6d904))
|
||||
- Update StoryApiV1Controller, add viewers route to view story viewers ([941736ce](https://github.com/pixelfed/pixelfed/commit/941736ce))
|
||||
- Update NotificationService, improve cache warming query ([2496386d](https://github.com/pixelfed/pixelfed/commit/2496386d))
|
||||
- Update StatusService, hydrate accounts on request instead of caching them along with status objects ([223661ec](https://github.com/pixelfed/pixelfed/commit/223661ec))
|
||||
- Update profile embed, fix resize ([dc23c21d](https://github.com/pixelfed/pixelfed/commit/dc23c21d))
|
||||
- Update Status model, improve thumb logic ([d969a973](https://github.com/pixelfed/pixelfed/commit/d969a973))
|
||||
- Update Status model, allow unlisted thumbnails ([1f0a45b7](https://github.com/pixelfed/pixelfed/commit/1f0a45b7))
|
||||
- Update StatusTagsPipeline, fix object tags and slug normalization ([d295e605](https://github.com/pixelfed/pixelfed/commit/d295e605))
|
||||
- Update Note and CreateNote transformers, include attachment blurhash, width and height ([ce1afe27](https://github.com/pixelfed/pixelfed/commit/ce1afe27))
|
||||
- Update ap helpers, store media attachment width and height if present ([8c969191](https://github.com/pixelfed/pixelfed/commit/8c969191))
|
||||
- Update Sign-in with Mastodon, allow usage when registrations are closed ([895dc4fa](https://github.com/pixelfed/pixelfed/commit/895dc4fa))
|
||||
- Update profile embeds, filter sensitive posts ([ede5ec3b](https://github.com/pixelfed/pixelfed/commit/ede5ec3b))
|
||||
- Update ApiV1Controller, hydrate reblog interactions. Fixes ([#4686](https://github.com/pixelfed/pixelfed/issues/4686)) ([135798eb](https://github.com/pixelfed/pixelfed/commit/135798eb))
|
||||
- Update AdminReportController, add `profile_id` to group by. Fixes ([#4685](https://github.com/pixelfed/pixelfed/issues/4685)) ([e4d3b196](https://github.com/pixelfed/pixelfed/commit/e4d3b196))
|
||||
- Update user:admin command, improve logic. Fixes ([#2465](https://github.com/pixelfed/pixelfed/issues/2465)) ([01bac511](https://github.com/pixelfed/pixelfed/commit/01bac511))
|
||||
- Update AP helpers, adjust RemoteAvatarFetch ttl from 24h to 3 months ([36b23fe3](https://github.com/pixelfed/pixelfed/commit/36b23fe3))
|
||||
- Update AvatarPipeline, improve refresh logic and garbage collection to purge old avatars ([82798b5e](https://github.com/pixelfed/pixelfed/commit/82798b5e))
|
||||
- Update CreateAvatar job, add processing constraints and set `is_remote` attribute ([319ced40](https://github.com/pixelfed/pixelfed/commit/319ced40))
|
||||
- Update RemoteStatusDelete and DecrementPostCount pipelines ([edbcf3ed](https://github.com/pixelfed/pixelfed/commit/edbcf3ed))
|
||||
- Update lexer regex, fix mention regex and add more tests ([778e83d3](https://github.com/pixelfed/pixelfed/commit/778e83d3))
|
||||
- Update StatusTransformer, generate autolink on request ([dfe2379b](https://github.com/pixelfed/pixelfed/commit/dfe2379b))
|
||||
- Update ComposeModal component, fix multi filter bug and allow media re-ordering before upload/posting ([56e315f6](https://github.com/pixelfed/pixelfed/commit/56e315f6))
|
||||
- Update ApiV1Dot1Controller, allow iar rate limits to be configurable ([28a80803](https://github.com/pixelfed/pixelfed/commit/28a80803))
|
||||
- Update ApiV1Dot1Controller, add domain to iar redirect ([1f82d47c](https://github.com/pixelfed/pixelfed/commit/1f82d47c))
|
||||
- Update ApiV1Dot1Controller, add configurable app confirm rate limit ttl ([4c6a0719](https://github.com/pixelfed/pixelfed/commit/4c6a0719))
|
||||
- Update LikePipeline, dispatch to feed queue. Fixes ([#4723](https://github.com/pixelfed/pixelfed/issues/4723)) ([da510089](https://github.com/pixelfed/pixelfed/commit/da510089))
|
||||
- Update AccountImport ([5a2d7e3e](https://github.com/pixelfed/pixelfed/commit/5a2d7e3e))
|
||||
- Update ImportPostController, fix IG bug with missing spaces between hashtags ([9c24157a](https://github.com/pixelfed/pixelfed/commit/9c24157a))
|
||||
- Update ApiV1Controller, fix mutes in home feed ([ddc21714](https://github.com/pixelfed/pixelfed/commit/ddc21714))
|
||||
- Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79))
|
||||
- Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c))
|
||||
- Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271))
|
||||
- Update hashtag following ([015b1b80](https://github.com/pixelfed/pixelfed/commit/015b1b80))
|
||||
- Update IncrementPostCount job, prevent overlap ([b2c9cc23](https://github.com/pixelfed/pixelfed/commit/b2c9cc23))
|
||||
- Update HashtagFollowService, fix cache invalidation bug ([84f4e885](https://github.com/pixelfed/pixelfed/commit/84f4e885))
|
||||
- Update Experimental Home Feed, fix remote posts, shares and reblogs ([c6a6b3ae](https://github.com/pixelfed/pixelfed/commit/c6a6b3ae))
|
||||
- Update HashtagService, improve count perf ([3327a008](https://github.com/pixelfed/pixelfed/commit/3327a008))
|
||||
- Update StatusHashtagService, remove problematic cache layer ([e5401f85](https://github.com/pixelfed/pixelfed/commit/e5401f85))
|
||||
- Update HomeFeedPipeline, fix tag filtering ([f105f4e8](https://github.com/pixelfed/pixelfed/commit/f105f4e8))
|
||||
- Update HashtagService, reduce cached_count cache ttl ([15f29f7d](https://github.com/pixelfed/pixelfed/commit/15f29f7d))
|
||||
- Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic ([287f903b](https://github.com/pixelfed/pixelfed/commit/287f903b))
|
||||
- Update StoryApiV1Controller, add self-carousel endpoint. Fixes ([#4352](https://github.com/pixelfed/pixelfed/issues/4352)) ([bcb88d5b](https://github.com/pixelfed/pixelfed/commit/bcb88d5b))
|
||||
- Update FollowServiceWarmCache, use more efficient query ([fe9b4c5a](https://github.com/pixelfed/pixelfed/commit/fe9b4c5a))
|
||||
- Update HomeFeedPipeline, observe mutes/blocks during fanout ([8548294c](https://github.com/pixelfed/pixelfed/commit/8548294c))
|
||||
- Update FederationController, add proper following/follower counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96))
|
||||
- Update FederationController, add proper statuses counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96))
|
||||
- Update Inbox handler, fix missing object_url and uri fields for direct statuses ([a0157fce](https://github.com/pixelfed/pixelfed/commit/a0157fce))
|
||||
- Update DirectMessageController, deliver direct delete activities to user inbox instead of sharedInbox ([d848792a](https://github.com/pixelfed/pixelfed/commit/d848792a))
|
||||
- Update DirectMessageController, dispatch deliver and delete actions to the job queue ([7f462a80](https://github.com/pixelfed/pixelfed/commit/7f462a80))
|
||||
- Update Inbox, improve story attribute collection ([06bee36c](https://github.com/pixelfed/pixelfed/commit/06bee36c))
|
||||
- Update DirectMessageController, dispatch local deletes to pipeline ([98186564](https://github.com/pixelfed/pixelfed/commit/98186564))
|
||||
- Update StatusPipeline, fix Direct and Story notification deletion ([4c95306f](https://github.com/pixelfed/pixelfed/commit/4c95306f))
|
||||
- Update Notifications.vue, fix deprecated DM action links for story activities ([4c3823b0](https://github.com/pixelfed/pixelfed/commit/4c3823b0))
|
||||
- Update ComposeModal, fix missing alttext post state ([0a068119](https://github.com/pixelfed/pixelfed/commit/0a068119))
|
||||
- Update PhotoAlbumPresenter.vue, fix fullscreen mode ([822e9888](https://github.com/pixelfed/pixelfed/commit/822e9888))
|
||||
- Update Timeline.vue, improve CHT pagination ([9c43e7e2](https://github.com/pixelfed/pixelfed/commit/9c43e7e2))
|
||||
- Update HomeFeedPipeline, fix StatusService validation ([041c0135](https://github.com/pixelfed/pixelfed/commit/041c0135))
|
||||
- Update Inbox, improve tombstone query efficiency ([759a4393](https://github.com/pixelfed/pixelfed/commit/759a4393))
|
||||
- Update AccountService, add setLastActive method ([ebbd98e7](https://github.com/pixelfed/pixelfed/commit/ebbd98e7))
|
||||
- Update ApiV1Controller, set last_active_at ([b6419545](https://github.com/pixelfed/pixelfed/commit/b6419545))
|
||||
- Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a))
|
||||
- Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc))
|
||||
- Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1))
|
||||
- Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c))
|
||||
- Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168))
|
||||
- Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171))
|
||||
- Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a))
|
||||
- Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b))
|
||||
- Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04))
|
||||
- Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381))
|
||||
- Update AP helpers, refactor post count decrement logic ([b81ae577](https://github.com/pixelfed/pixelfed/commit/b81ae577))
|
||||
- Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c))
|
||||
- Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389))
|
||||
- Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3))
|
||||
- Update NodeinfoService, disable redirects ([240e6bbe](https://github.com/pixelfed/pixelfed/commit/240e6bbe))
|
||||
- Update Instance model, add entity casts ([289cad47](https://github.com/pixelfed/pixelfed/commit/289cad47))
|
||||
- Update FetchNodeinfoPipeline, use more efficient dispatch ([ac01f51a](https://github.com/pixelfed/pixelfed/commit/ac01f51a))
|
||||
- Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade))
|
||||
- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3))
|
||||
- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac))
|
||||
- Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! ([4d02d6f1](https://github.com/pixelfed/pixelfed/commit/4d02d6f1))
|
||||
|
||||
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)
|
||||
|
||||
### Added
|
||||
- Import from Instagram ([#4466](https://github.com/pixelfed/pixelfed/pull/4466)) ([cf3078c5](https://github.com/pixelfed/pixelfed/commit/cf3078c5))
|
||||
- Sign-in with Mastodon ([#4545](https://github.com/pixelfed/pixelfed/pull/4545)) ([45b9404e](https://github.com/pixelfed/pixelfed/commit/45b9404e))
|
||||
- Health check endpoint at /api/service/health-check ([ff58f970](https://github.com/pixelfed/pixelfed/commit/ff58f970))
|
||||
- Reblogs in home feed ([#4563](https://github.com/pixelfed/pixelfed/pull/4563)) ([b86d47bf](https://github.com/pixelfed/pixelfed/commit/b86d47bf))
|
||||
- Account Migrations ([#4578](https://github.com/pixelfed/pixelfed/pull/4578)) ([a9220e4e](https://github.com/pixelfed/pixelfed/commit/a9220e4e))
|
||||
|
||||
### Updates
|
||||
- Update Notifications.vue component, fix filtering logic to prevent endless spinner ([3df9b53f](https://github.com/pixelfed/pixelfed/commit/3df9b53f))
|
||||
- Update Direct Messages, fix api endpoint ([fe8728c0](https://github.com/pixelfed/pixelfed/commit/fe8728c0))
|
||||
- Update nginx config ([fbdc6358](https://github.com/pixelfed/pixelfed/commit/fbdc6358))
|
||||
- Update api routes, add DeprecatedEndpoint middleware. For more info, visit [pixelfed.org/kb/10404](https://pixelfed.org/kb/10404) ([a8453e77](https://github.com/pixelfed/pixelfed/commit/a8453e77))
|
||||
- Update admin dashboard, improve users section ([36b6bf48](https://github.com/pixelfed/pixelfed/commit/36b6bf48))
|
||||
- Update AdminApiController, add instance stats endpoint ([89c3710d](https://github.com/pixelfed/pixelfed/commit/89c3710d))
|
||||
- Update config, re-add `PF_MAX_USERS` .env variable to limit max users to 1000 by default ([a6d10f03](https://github.com/pixelfed/pixelfed/commit/a6d10f03))
|
||||
- Update AdminApiController, fix stats ([5c5541fc](https://github.com/pixelfed/pixelfed/commit/5c5541fc))
|
||||
- Update AdminApiController, include more data for getUser method ([4f850e54](https://github.com/pixelfed/pixelfed/commit/4f850e54))
|
||||
- Update AdminApiController, improve admin moderation tools ([763ce19a](https://github.com/pixelfed/pixelfed/commit/763ce19a))
|
||||
- Update ActivityPubFetchService, fix authorized_fetch compatibility. Closes #1850, #2713, #2935 ([63a7879c](https://github.com/pixelfed/pixelfed/commit/63a7879c))
|
||||
- Update IG Import commands, fix stalled import queue ([b18f3fba](https://github.com/pixelfed/pixelfed/commit/b18f3fba))
|
||||
- Update TransformImports command, improve handling of imported posts that already exist or are from deleted accounts ([892907d5](https://github.com/pixelfed/pixelfed/commit/892907d5))
|
||||
- Update console kernel, add import upload gc ([afe6948d](https://github.com/pixelfed/pixelfed/commit/afe6948d))
|
||||
- Update ImportService, filter deleted posts from getImportedPosts endpoint ([10dd348c](https://github.com/pixelfed/pixelfed/commit/10dd348c))
|
||||
- Update FixStatusCount, improve command and support remote count resync ([04f4f8ba](https://github.com/pixelfed/pixelfed/commit/04f4f8ba))
|
||||
- Update StatusRemoteUpdatePipeline, fix missing mime and size attributes that cause empty media previews on our mobile app ([ea54413e](https://github.com/pixelfed/pixelfed/commit/ea54413e))
|
||||
- Update ComposeModal.vue, fix scroll issue and dont hide scrollbar ([2d959fb3](https://github.com/pixelfed/pixelfed/commit/2d959fb3))
|
||||
- Update AccountImport, add select first 100 posts button ([625a76a5](https://github.com/pixelfed/pixelfed/commit/625a76a5))
|
||||
- Update ApiV1Controller, add include_reblogs attribute to home timeline ([37fd0342](https://github.com/pixelfed/pixelfed/commit/37fd0342))
|
||||
- Update rate limits, fixes #4537 ([1cc6274a](https://github.com/pixelfed/pixelfed/commit/1cc6274a))
|
||||
- Update Services, use zpopmin on predis ([4b2c66f5](https://github.com/pixelfed/pixelfed/commit/4b2c66f5))
|
||||
- Update Inbox, allow storing Create->Note activities without any local followers, disabled by default ([9fa6b3f7](https://github.com/pixelfed/pixelfed/commit/9fa6b3f7))
|
||||
- Update AP Helpers, preserve admin unlisted state before adding to NetworkTimelineService ([0704c7e0](https://github.com/pixelfed/pixelfed/commit/0704c7e0))
|
||||
- Update SearchApiV2Service, improve resolve query logic to better handle remote posts/profiles and local posts/profiles ([c61d0b91](https://github.com/pixelfed/pixelfed/commit/c61d0b91))
|
||||
- Update FollowPipeline, improve follower/following count calculation ([0b515767](https://github.com/pixelfed/pixelfed/commit/0b515767))
|
||||
- Update TransformImports command, increment status_count on profile model ([ba7551d8](https://github.com/pixelfed/pixelfed/commit/ba7551d8))
|
||||
- Update AP Helpers, improve url validation and add optional dns verification, disabled by default ([2bef3e41](https://github.com/pixelfed/pixelfed/commit/2bef3e41))
|
||||
- Update admin users blade view, show last_active_at and other info ([e0b48b29](https://github.com/pixelfed/pixelfed/commit/e0b48b29))
|
||||
- Update MediaStorageService, improve head header handling ([3590adbd](https://github.com/pixelfed/pixelfed/commit/3590adbd))
|
||||
- Update admin user view, improve previews ([ff2c16fe](https://github.com/pixelfed/pixelfed/commit/ff2c16fe))
|
||||
- Update FanoutDeletePipeline, fix AP object ([0d802c31](https://github.com/pixelfed/pixelfed/commit/0d802c31))
|
||||
- Update Remote Auth feature, fix custom domain bug and enforce banned domains ([acabf603](https://github.com/pixelfed/pixelfed/commit/acabf603))
|
||||
- Update StatusService, reduce cache ttl from 7 days to 6 hours ([59b64378](https://github.com/pixelfed/pixelfed/commit/59b64378))
|
||||
- Update ProfileController, allow albums in atom feed. Closes #4561. Fixes #4526 ([1c105a6c](https://github.com/pixelfed/pixelfed/commit/1c105a6c))
|
||||
- Update admin users view, fix website value. Closes #4557 ([c469d475](https://github.com/pixelfed/pixelfed/commit/c469d475))
|
||||
- Update StatusStatelessTransformer, allow unlisted reblogs ([1c13b518](https://github.com/pixelfed/pixelfed/commit/1c13b518))
|
||||
- Update ApiV1Controller, hydrate reblog state in home timeline ([13bdaa2e](https://github.com/pixelfed/pixelfed/commit/13bdaa2e))
|
||||
- Update Timeline component, improve reblog support ([29de91e5](https://github.com/pixelfed/pixelfed/commit/29de91e5))
|
||||
- Update timeline settings, add photo reblogs only option ([e2705b9a](https://github.com/pixelfed/pixelfed/commit/e2705b9a))
|
||||
- Update PostContent, add text cw warning ([911504fa](https://github.com/pixelfed/pixelfed/commit/911504fa))
|
||||
- Update ActivityPubFetchService, add validateUrl parameter to bypass url validation to fetch content from blocked instances ([3d1b6516](https://github.com/pixelfed/pixelfed/commit/3d1b6516))
|
||||
- Update RemoteStatusDelete pipeline ([71e92261](https://github.com/pixelfed/pixelfed/commit/71e92261))
|
||||
- Update RemoteStatusDelete pipeline ([fab8f25e](https://github.com/pixelfed/pixelfed/commit/fab8f25e))
|
||||
- Update RemoteStatusPipeline, fix reply check ([618b6727](https://github.com/pixelfed/pixelfed/commit/618b6727))
|
||||
- Update ApiV1Controller, add bookmarked to timeline entities ([ca746717](https://github.com/pixelfed/pixelfed/commit/ca746717))
|
||||
|
||||
## [v0.11.8 (2023-05-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.7...v0.11.8)
|
||||
|
||||
### API Changes
|
||||
- Added `following_since` attribute to `/api/v1/accounts/relationships` endpoint when `_pe=1` (pixelfed entity) parameter is present ([992d910b](https://github.com/pixelfed/pixelfed/commit/992d910b))
|
||||
- Added `/api/v1.1/accounts/app/settings` endpoint and UserAppSettings model to store app specific settings ([a2305d5f](https://github.com/pixelfed/pixelfed/commit/a2305d5f))
|
||||
|
||||
### Added
|
||||
- Post edits ([#4416](https://github.com/pixelfed/pixelfed/pull/4416)) ([98cf8f3](https://github.com/pixelfed/pixelfed/commit/98cf8f3))
|
||||
|
||||
### Updates
|
||||
- Update StatusService, fix bug in getFull method ([4d8b4dcf](https://github.com/pixelfed/pixelfed/commit/4d8b4dcf))
|
||||
- Update Config, bump version for post edit support without having to clear cache ([c0190d84](https://github.com/pixelfed/pixelfed/commit/c0190d84))
|
||||
- Update EditHistoryModal, fix caption rendering ([0f803446](https://github.com/pixelfed/pixelfed/commit/0f803446))
|
||||
- Update StatusRemoteUpdatePipeline, fix typo ([109d0419](https://github.com/pixelfed/pixelfed/commit/109d0419))
|
||||
- Update StatusActivityPubDeliver, fix delivery addressing ([1f2183ee](https://github.com/pixelfed/pixelfed/commit/1f2183ee))
|
||||
- Update UpdateStatusService, fix formatting issue. Fixes #4423 ([4479055e](https://github.com/pixelfed/pixelfed/commit/4479055e))
|
||||
- Update nginx config ([ee3b6e09](https://github.com/pixelfed/pixelfed/commit/ee3b6e09))
|
||||
- Update Status model, increase max mentions, hashtags and links ([1430f532](https://github.com/pixelfed/pixelfed/commit/1430f532))
|
||||
|
||||
## [v0.11.7 (2023-05-24)](https://github.com/pixelfed/pixelfed/compare/v0.11.6...v0.11.7)
|
||||
|
||||
### API Changes
|
||||
- Added [/api/v1/followed_tags](https://docs.joinmastodon.org/methods/followed_tags/) api endpoint ([175a8486](https://github.com/pixelfed/pixelfed/commit/175a8486))
|
||||
- Added [/api/v1/tags/:id/follow](https://docs.joinmastodon.org/methods/tags/#follow) and [/api/v1/tags/:id/unfollow](https://docs.joinmastodon.org/methods/tags/#unfollow) api endpoints ([4d997bb9](https://github.com/pixelfed/pixelfed/commit/4d997bb9))
|
||||
- Added [/api/v1/tags/:id](https://docs.joinmastodon.org/methods/tags/) api endpoint ([521b3b4c](https://github.com/pixelfed/pixelfed/commit/521b3b4c))
|
||||
- Added `only_media` support to /api/v1/timelines/tag/:id api endpoint ([b5fe956a](https://github.com/pixelfed/pixelfed/commit/b5fe956a))
|
||||
- Added /api/v2/instance api endpoint ([167dbcdd](https://github.com/pixelfed/pixelfed/commit/167dbcdd))
|
||||
- Removed api endpoint cloud ip block logic ([6a2daf1f](https://github.com/pixelfed/pixelfed/commit/6a2daf1f))
|
||||
- Added idempotency-key support to /api/v1/statuses endpoint ([c54cdd3e](https://github.com/pixelfed/pixelfed/commit/c54cdd3e))
|
||||
|
||||
### Added
|
||||
- Added store remote media on S3 config setting, disabled by default ([51768083](https://github.com/pixelfed/pixelfed/commit/51768083))
|
||||
- Added Autospam Advanced Detection ([132a58de](https://github.com/pixelfed/pixelfed/commit/132a58de))
|
||||
|
||||
### Updates
|
||||
- Update admin dashboard, fix search and dropdown menu ([dac0d083](https://github.com/pixelfed/pixelfed/commit/dac0d083))
|
||||
- Update sudo mode view, fix trusted device checkbox ([8ef900bf](https://github.com/pixelfed/pixelfed/commit/8ef900bf))
|
||||
- Update SearchApiV2Service, improve postgres support ([666e5732](https://github.com/pixelfed/pixelfed/commit/666e5732))
|
||||
- Update StoryController, show active self stories on home timeline ([633351f6](https://github.com/pixelfed/pixelfed/commit/633351f6))
|
||||
- Update ApiV1Controller, fix trending accounts format. Closes #4356 ([37bd2ee5](https://github.com/pixelfed/pixelfed/commit/37bd2ee5))
|
||||
- Update instance config, enable config cache by default ([970f77b0](https://github.com/pixelfed/pixelfed/commit/970f77b0))
|
||||
- Update Admin Dashboard, allow admins to designate an admin account for the landing page and instance api endpoint ([6ea2bdc7](https://github.com/pixelfed/pixelfed/commit/6ea2bdc7))
|
||||
- Update config, enable oauth by default ([6a2e9e8f](https://github.com/pixelfed/pixelfed/commit/6a2e9e8f))
|
||||
- Update StatusService, fix missing account condition ([f48daab3](https://github.com/pixelfed/pixelfed/commit/f48daab3))
|
||||
- Update ProfileService, add softFail param ([6bc20a37](https://github.com/pixelfed/pixelfed/commit/6bc20a37))
|
||||
- Update MediaTagService, fix ProfileService to soft fail on missing or deleted accounts ([df444851](https://github.com/pixelfed/pixelfed/commit/df444851))
|
||||
- Update LikeService, improve likedBy logic to soft fail on missing or deleted accounts ([91ba1398](https://github.com/pixelfed/pixelfed/commit/91ba1398))
|
||||
- Update StatusTransformers, fix ProfileService to soft fail on missing or deleted accounts ([43d3aa2b](https://github.com/pixelfed/pixelfed/commit/43d3aa2b))
|
||||
- Update ApiV1Controller, fix hashtag timeline ([fc1a385c](https://github.com/pixelfed/pixelfed/commit/fc1a385c))
|
||||
- Update settings view, add fallback avatar ([1a83c585](https://github.com/pixelfed/pixelfed/commit/1a83c585))
|
||||
- Update HashtagFollow model, add MAX_LIMIT of 250 tags per account ([ed352141](https://github.com/pixelfed/pixelfed/commit/ed352141))
|
||||
- Update Notification logic, remove message and rendered fields ([6cdb5bc6](https://github.com/pixelfed/pixelfed/commit/6cdb5bc6))
|
||||
- Update InstanceService, fix banner blurhash memory bug ([3aad75ab](https://github.com/pixelfed/pixelfed/commit/3aad75ab))
|
||||
- Update models, remove deprecated toText and toHtml method ([ea943333](https://github.com/pixelfed/pixelfed/commit/ea943333))
|
||||
- Update Notification components, add autospam notification support ([0d3b4bc2](https://github.com/pixelfed/pixelfed/commit/0d3b4bc2))
|
||||
- Update AutoSpam Bouncer, generate notification on positive detections ([d5f63f8a](https://github.com/pixelfed/pixelfed/commit/d5f63f8a))
|
||||
- Update admin autospam apis, remove autospam warning notifications when appropriate ([588ca653](https://github.com/pixelfed/pixelfed/commit/588ca653))
|
||||
- Update StatusEntityLexer, stop saving entities ([a91a5e48](https://github.com/pixelfed/pixelfed/commit/a91a5e48))
|
||||
- Update UserCreate command, fix is_admin flag ([ad25ed67](https://github.com/pixelfed/pixelfed/commit/ad25ed67))
|
||||
- Update Bouncer, adjust advanced Autospam logic ([18cddd43](https://github.com/pixelfed/pixelfed/commit/18cddd43))
|
||||
- Update atom view, fix atom feed bug ([63b72c42](https://github.com/pixelfed/pixelfed/commit/63b72c42))
|
||||
- Update StatusController, disable post embeds from spam accounts ([c167af43](https://github.com/pixelfed/pixelfed/commit/c167af43))
|
||||
- Update ProfileController, require login to view spam accounts, and disable profile embeds and atom feeds for spam accounts ([dd2f5bb9](https://github.com/pixelfed/pixelfed/commit/dd2f5bb9))
|
||||
- Update Settings, allow users to disable atom feeds ([3662d3de](https://github.com/pixelfed/pixelfed/commit/3662d3de))
|
||||
- Update ApiV1Controller, filter muted/blocked accounts from tag timeline ([f42c1140](https://github.com/pixelfed/pixelfed/commit/f42c1140))
|
||||
- Update admin moderation logic, only re-add top level posts ([c6ffda96](https://github.com/pixelfed/pixelfed/commit/c6ffda96))
|
||||
- Update admin dashboard, add mass account deletes ([b8426cce](https://github.com/pixelfed/pixelfed/commit/b8426cce))
|
||||
- Update scheduler, fix S3 media garbage collection not being executed when cloud storage is enabled via dashboard without .env/config being enabled ([adb070f1](https://github.com/pixelfed/pixelfed/commit/adb070f1))
|
||||
- Update MediaController, add fallback for local files that are later stored on S3 but still are referenced in cached objects remotely ([4973cb46](https://github.com/pixelfed/pixelfed/commit/4973cb46))
|
||||
- Update PublicTimelineService, improve warmCache query ([9f901d65](https://github.com/pixelfed/pixelfed/commit/9f901d65))
|
||||
- Update AP Inbox, fix delete handling ([2800c888](https://github.com/pixelfed/pixelfed/commit/2800c888))
|
||||
- Update login/register views and captcha config, enable login or register captchas or both ([c071c719](https://github.com/pixelfed/pixelfed/commit/c071c719))
|
||||
- Update login form, allow admins to enable captcha after X failed attempts. Admins can set the number of attempts before captcha is shown, default is 2 attempts before captcha is required ([221ddce0](https://github.com/pixelfed/pixelfed/commit/221ddce0))
|
||||
|
||||
## [v0.11.6 (2023-05-03)](https://github.com/pixelfed/pixelfed/compare/v0.11.5...v0.11.6)
|
||||
|
||||
### Added
|
||||
- Add php 8.2 support. Bump laravel version, v9 => v10 ([fb4ac4eb](https://github.com/pixelfed/pixelfed/commit/fb4ac4eb))
|
||||
- New media:fix-nonlocal-driver command. Fixes s3 media created with invalid FILESYSTEM_DRIVER=s3 configuration ([672cccd4](https://github.com/pixelfed/pixelfed/commit/672cccd4))
|
||||
- New landing page design ([09c0032b](https://github.com/pixelfed/pixelfed/commit/09c0032b))
|
||||
- Add cloud ip bans to BouncerService (disabled by default) ([50ab2e20](https://github.com/pixelfed/pixelfed/commit/50ab2e20))
|
||||
- Redesigned Admin Dashboard Reports/Moderation ([c6cc6327](https://github.com/pixelfed/pixelfed/commit/c6cc6327))
|
||||
|
||||
### Fixes
|
||||
- Fixed `violates check constraint "statuses_visibility_check"` bug affecting postgres instances + various api endpoints ([79b6a17e](https://github.com/pixelfed/pixelfed/commit/79b6a17e))
|
||||
- Fixed duplicate hashtags on postgres ([64059cb4](https://github.com/pixelfed/pixelfed/commit/64059cb4))
|
||||
- Fixed custom emoji domain search on postgres. Closes #4333 ([3dac45f3](https://github.com/pixelfed/pixelfed/commit/3dac45f3))
|
||||
|
||||
### Updates
|
||||
- Update ApiV1Controller, fix blocking remote accounts. Closes #4256 ([8e71e0c0](https://github.com/pixelfed/pixelfed/commit/8e71e0c0))
|
||||
- Update ComposeController, fix postgres location search. Closes #4242 and #4239 ([64a4a006](https://github.com/pixelfed/pixelfed/commit/64a4a006))
|
||||
- Update app.js, add title attribute to iframe embeds to comply with accessibility requirements ([4d72b9e3](https://github.com/pixelfed/pixelfed/commit/4d72b9e3))
|
||||
- Update MediaPathService, fix story path ([aebbad96](https://github.com/pixelfed/pixelfed/commit/aebbad96))
|
||||
- Update Story v1.1 api endpoints ([855e9626](https://github.com/pixelfed/pixelfed/commit/855e9626))
|
||||
- Update ApiV1Controller, filter mute/blocks on statuses/context and statuses/replies endpoints ([73aa01e8](https://github.com/pixelfed/pixelfed/commit/73aa01e8))
|
||||
- Update filesystems, store all files as public by default and add default permissions. Fixes #4273, #4275. Closes #3825 ([22da2647](https://github.com/pixelfed/pixelfed/commit/22da2647))
|
||||
- Update Profile model, fix avatar url path generation. Fixes #4041, Fixes #4031, Fixes #3523 ([28bf8649](https://github.com/pixelfed/pixelfed/commit/28bf8649))
|
||||
- Update filesystem config, change FILESYSTEM_DRIVER env variable to DANGEROUSLY_SET_FILESYSTEM_DRIVER and remove from default env configs. Changing the default filesystem should be avoided, use FILESYSTEM_CLOUD for s3 support, otherwise you can break things ([573c88d7](https://github.com/pixelfed/pixelfed/commit/573c88d7))
|
||||
- Update MediaS3GarbageCollector, fix handle ([2eee36cf](https://github.com/pixelfed/pixelfed/commit/2eee36cf))
|
||||
- Update StatusController, allow users to delete replies to posts ([738925c2](https://github.com/pixelfed/pixelfed/commit/738925c2))
|
||||
- Update admin autospam/report email templates, remove image previews ([76be49ac](https://github.com/pixelfed/pixelfed/commit/76be49ac))
|
||||
- Update LandingService, enable landing directory/explore feed by default and move configuration to config/instance.php file ([780f2507](https://github.com/pixelfed/pixelfed/commit/780f2507))
|
||||
- Update ImageOptimizePipeline, improve support for disabling image optimizations ([e76289e4](https://github.com/pixelfed/pixelfed/commit/e76289e4))
|
||||
- Update LandingController, fix config variable names ([b716926b](https://github.com/pixelfed/pixelfed/commit/b716926b))
|
||||
- Update Privacy Settings, add Directory setting ([634c15e4](https://github.com/pixelfed/pixelfed/commit/634c15e4))
|
||||
- Update site config ([6d59dc8e](https://github.com/pixelfed/pixelfed/commit/6d59dc8e))
|
||||
- Update db:raw queries to support laravel v10 ([849e5103](https://github.com/pixelfed/pixelfed/commit/849e5103))
|
||||
- Update RegisterController, store client ip during registration ([d4c967de](https://github.com/pixelfed/pixelfed/commit/d4c967de))
|
||||
- Update ApiV1Controller, fix account blocks. Closes #4304 ([98739139](https://github.com/pixelfed/pixelfed/commit/98739139))
|
||||
- Update RegisterController, improve max_users calculation and add kb page to redirect to if conditions are met ([1bbee6d0](https://github.com/pixelfed/pixelfed/commit/1bbee6d0))
|
||||
- Update SecuritySettings, remove imagick depdency for 2FA qr code generation image ([506f95c6](https://github.com/pixelfed/pixelfed/commit/506f95c6))
|
||||
- Update 2fa checkpoint view design ([86c472ac](https://github.com/pixelfed/pixelfed/commit/86c472ac))
|
||||
- Update sudo mode checkpoint view design ([091e0b2c](https://github.com/pixelfed/pixelfed/commit/091e0b2c))
|
||||
- Update ForgotPasswordController, add captcha support, improve security and a new redesigned view ([f6e7ff64](https://github.com/pixelfed/pixelfed/commit/f6e7ff64))
|
||||
- Update ResetPasswordController, add captcha support, improve security and a new redesigned view ([0ab5b96a](https://github.com/pixelfed/pixelfed/commit/0ab5b96a))
|
||||
- Update Inbox, remove handleCreateActivity logic that rejected posts from accounts without followers ([a93a3efd](https://github.com/pixelfed/pixelfed/commit/a93a3efd))
|
||||
- Update ApiV1Controller and DiscoverController, fix postgres hashtag search ([055aa6b3](https://github.com/pixelfed/pixelfed/commit/055aa6b3))
|
||||
- Update StatusTagsPipeline, deduplicate hashtags on postgres ([867cbc75](https://github.com/pixelfed/pixelfed/commit/867cbc75))
|
||||
- Update SearchApiV2Service, fix postgres hashtag search and prepend wildcard operator to improve results ([6e20d0a6](https://github.com/pixelfed/pixelfed/commit/6e20d0a6))
|
||||
|
||||
## [v0.11.5 (2023-03-25)](https://github.com/pixelfed/pixelfed/compare/v0.11.4...v0.11.5)
|
||||
|
||||
### New Features
|
||||
- Mobile App Registration ([#3829](https://github.com/pixelfed/pixelfed/pull/3829))
|
||||
- Portfolios ([#3705](https://github.com/pixelfed/pixelfed/pull/3705))
|
||||
- Server Directory ([#3762](https://github.com/pixelfed/pixelfed/pull/3762))
|
||||
- Manually verify email address (php artisan user:verifyemail) ([682f5f0f](https://github.com/pixelfed/pixelfed/commit/682f5f0f))
|
||||
- Manually generate in-app registration confirmation links (php artisan user:app-magic-link) ([73eb9e36](https://github.com/pixelfed/pixelfed/commit/73eb9e36))
|
||||
- Optional home feed caching ([3328b367](https://github.com/pixelfed/pixelfed/commit/3328b367))
|
||||
- Admin Invites ([b73ca9a1](https://github.com/pixelfed/pixelfed/commit/b73ca9a1))
|
||||
- Hashtag administration ([84872311](https://github.com/pixelfed/pixelfed/commit/84872311))
|
||||
- Admin report email notifications ([4e1d0ed5](https://github.com/pixelfed/pixelfed/commit/4e1d0ed5))
|
||||
- Add Licenses help page, fixes #4238 ([3c712a70](https://github.com/pixelfed/pixelfed/commit/3c712a70))
|
||||
|
||||
### Updates
|
||||
- Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2))
|
||||
- Update PublicApiController, remove expensive and unused relationships ([2ecc3144](https://github.com/pixelfed/pixelfed/commit/2ecc3144))
|
||||
- Update status deletion, fix database lock issues and side effects ([04e8c96a](https://github.com/pixelfed/pixelfed/commit/04e8c96a))
|
||||
- Fix remote profile avatar urls when storing locally ([b0422d4f](https://github.com/pixelfed/pixelfed/commit/b0422d4f))
|
||||
- Enable network timeline caching by default ([c990ac2a](https://github.com/pixelfed/pixelfed/commit/c990ac2a))
|
||||
- Redirect /home to / ([97032997](https://github.com/pixelfed/pixelfed/commit/97032997))
|
||||
- Fix 2FA backup code bug ([a231b3c5](https://github.com/pixelfed/pixelfed/commit/a231b3c5))
|
||||
- Update federation config, enable remote follows by default ([59702d40](https://github.com/pixelfed/pixelfed/commit/59702d40))
|
||||
- Update ApiV1Controller, fix followAccountById with firstOrCreate() ([1d52ad0b](https://github.com/pixelfed/pixelfed/commit/1d52ad0b))
|
||||
- Update AccountService, fix delete status ([8b7121f9](https://github.com/pixelfed/pixelfed/commit/8b7121f9))
|
||||
- Update ap helpers, fix duplicate entry bug ([85cfa1ba](https://github.com/pixelfed/pixelfed/commit/85cfa1ba))
|
||||
- Update Inbox, fix handleUndoActivity ([d660e46b](https://github.com/pixelfed/pixelfed/commit/d660e46b))
|
||||
- Update HomeSettings controller, bail earlier when attempting to update email that already exists ([399bf5f8](https://github.com/pixelfed/pixelfed/commit/399bf5f8))
|
||||
- Update ProfileController, cache actor object and atom feed ([8665eab1](https://github.com/pixelfed/pixelfed/commit/8665eab1))
|
||||
- Update NotificationTransformer, fix mediaTag and modLog types ([b6c06c4b](https://github.com/pixelfed/pixelfed/commit/b6c06c4b))
|
||||
- Update landing view, add `app.name` and `app.short_description` for better customizability ([bda9d16b](https://github.com/pixelfed/pixelfed/commit/bda9d16b))
|
||||
- Update Profile, fix avatarUrl paths. Fixes #3559 #3634 ([989e4249](https://github.com/pixelfed/pixelfed/commit/989e4249))
|
||||
- Update InboxPipeline, bump request timeout from 5s to 60s ([bb120019](https://github.com/pixelfed/pixelfed/commit/bb120019))
|
||||
- Update web routes, fix missing home route ([a9f4ddfc](https://github.com/pixelfed/pixelfed/commit/a9f4ddfc))
|
||||
- Allow forceHttps to be disabled, fixes #3710 ([a31bdec7](https://github.com/pixelfed/pixelfed/commit/a31bdec7))
|
||||
- Update MediaStorageService, fix size check bug ([319f0ba5](https://github.com/pixelfed/pixelfed/commit/319f0ba5))
|
||||
- Update AvatarSync, fix sync skipping recently fetched avatars by setting last_fetched_at to null before refetching ([a83fc798](https://github.com/pixelfed/pixelfed/commit/a83fc798))
|
||||
- Refactor AvatarStorage to support migrating avatars to cloud storage, fix remote avatar refetching and merge AvatarSync commands and add deprecation notice to avatar:sync command ([223aea47](https://github.com/pixelfed/pixelfed/commit/223aea47))
|
||||
- Update AvatarStorage, improve overview calculations ([733b9fd0](https://github.com/pixelfed/pixelfed/commit/733b9fd0))
|
||||
- Update filesystem config, fix DO Spaces root default ([720b6eb3](https://github.com/pixelfed/pixelfed/commit/720b6eb3))
|
||||
- Update Avatar pipeline, fix cloud storage media_path ([02edd19d](https://github.com/pixelfed/pixelfed/commit/02edd19d))
|
||||
- Update FederationController, add instance actor profile to webfinger ([6e3c8097](https://github.com/pixelfed/pixelfed/commit/6e3c8097))
|
||||
- Update MediaService, add summary attribute for better alt text federation ([a12712cc](https://github.com/pixelfed/pixelfed/commit/a12712cc))
|
||||
- Update AvatarObserver, fix cloud delete bug by checking if cloud storage is enabled ([9f7672f5](https://github.com/pixelfed/pixelfed/commit/9f7672f5))
|
||||
- Update DeleteAccountPipeline, dispatch on low queue ([6eabe07c](https://github.com/pixelfed/pixelfed/commit/6eabe07c))
|
||||
- Update DeleteAccountPipeline, handle flysystem v3 changes by checking files exist before attempting to delete ([23e2998f](https://github.com/pixelfed/pixelfed/commit/23e2998f))
|
||||
- Update FollowerService, use redis sorted sets for follower relations ([356cc277](https://github.com/pixelfed/pixelfed/commit/356cc277))
|
||||
- Update FollowerService, use redis sorted sets for following relations ([f46b01af](https://github.com/pixelfed/pixelfed/commit/f46b01af))
|
||||
- Update PublicApiController, refactor follower/following api endpoints to consume FollowerService instead of querying database ([b39f91b4](https://github.com/pixelfed/pixelfed/commit/b39f91b4))
|
||||
- Update follower/following profile layout, optimized for mobile devices and use FollowerService ([78a5575d](https://github.com/pixelfed/pixelfed/commit/78a5575d))
|
||||
- Update sidebar menu, when clicking on the active feed/timeline buttons force a reload and scroll to top of feed ([78a5575d](https://github.com/pixelfed/pixelfed/commit/78a5575d))
|
||||
- Update InboxPipeline, increase timeout from 60s to 300s ([d1b888b5](https://github.com/pixelfed/pixelfed/commit/d1b888b5))
|
||||
- Update backup config, fixes #3793, #3920, #3931 ([b0c4cc30](https://github.com/pixelfed/pixelfed/commit/b0c4cc30))
|
||||
- Update FederationController, add two new queues (follow, shared) to prioritize follow request handling ([8ba33864](https://github.com/pixelfed/pixelfed/commit/8ba33864))
|
||||
- Dispatch follow accept/reject pipeline jobs to follow queue ([aaed2bf6](https://github.com/pixelfed/pixelfed/commit/aaed2bf6))
|
||||
- Update MediaStorageService, improve support for pleroma .blob avatars ([66226658](https://github.com/pixelfed/pixelfed/commit/66226658))
|
||||
- Update ApiV1Controller, remove min avatar size limit, fixes #3715 ([2b0db812](https://github.com/pixelfed/pixelfed/commit/2b0db812))
|
||||
- Update InboxPipeline, add inbox job queue and separate http sig validation from activity handling ([e6c1604d](https://github.com/pixelfed/pixelfed/commit/e6c1604d))
|
||||
- Update InboxPipeline, dispatch Follow/Accept Follow jobs to follow queue ([f62d2494](https://github.com/pixelfed/pixelfed/commit/f62d2494))
|
||||
- Add MediaS3GarbageCollector command to clear local media after uploaded to S3 disks after 12 hours ([b8c3f153](https://github.com/pixelfed/pixelfed/commit/b8c3f153))
|
||||
- Update MediaS3GarbageCollector command, disable logging by default and optimize huge invocations ([a14af93b](https://github.com/pixelfed/pixelfed/commit/a14af93b))
|
||||
- Update MediaStorageService, clear MediaService and StatusService caches after localToCloud ([de56b0f0](https://github.com/pixelfed/pixelfed/commit/de56b0f0))
|
||||
- Add CloudMediaMigrate command to migrate older local media to cloud storage ([382d00d9](https://github.com/pixelfed/pixelfed/commit/382d00d9))
|
||||
- Update MediaS3GarbageCollector command, handle thumbnail deletion ([95bbcc38](https://github.com/pixelfed/pixelfed/commit/95bbcc38))
|
||||
- Update StatusReplyPipeline, remove expensive reply count re-calculation query ([a2f8aad1](https://github.com/pixelfed/pixelfed/commit/a2f8aad1))
|
||||
- Update CommentPipeline, remove expensive reply count re-calculation query ([b457a446](https://github.com/pixelfed/pixelfed/commit/b457a446))
|
||||
- Update FederationController, improve inbox/sharedInbox delete handling ([2180a2de](https://github.com/pixelfed/pixelfed/commit/2180a2de))
|
||||
- Update HashtagController, improve trending hashtag endpoint ([4873c7dd](https://github.com/pixelfed/pixelfed/commit/4873c7dd))
|
||||
- Fix CustomEmoji, properly handle shortcode updates and delete old copy in case the extension changes ([bc29073a](https://github.com/pixelfed/pixelfed/commit/bc29073a))
|
||||
- Update reply pipelines, restore reply_count logic ([0d780ffb](https://github.com/pixelfed/pixelfed/commit/0d780ffb))
|
||||
- Update StatusTagsPipeline, reject if `type` not set ([91085c45](https://github.com/pixelfed/pixelfed/commit/91085c45))
|
||||
- Update ReplyPipelines, use more efficent reply count calculation ([d4dfa95c](https://github.com/pixelfed/pixelfed/commit/d4dfa95c))
|
||||
- Update StatusDelete pipeline, dispatch async ([257c0949](https://github.com/pixelfed/pixelfed/commit/257c0949))
|
||||
- Update lexer/extractor to handle banned hashtags ([909a8a5a](https://github.com/pixelfed/pixelfed/commit/909a8a5a))
|
||||
- Update FederationController, fix double lock bug ([9fcccca9](https://github.com/pixelfed/pixelfed/commit/9fcccca9))
|
||||
- Update AdminInvite component, fix email regex ([2aea77d3](https://github.com/pixelfed/pixelfed/commit/2aea77d3))
|
||||
- Update database config, use single transaction and skip lock tables for mysql dump ([936f1e7a](https://github.com/pixelfed/pixelfed/commit/936f1e7a))
|
||||
- Update database config, add sticky flag https://laravel.com/docs/9.x/database#the-sticky-option ([10b65980](https://github.com/pixelfed/pixelfed/commit/10b65980))
|
||||
- Update profile audience to filter blocked instances ([e0c3dae3](https://github.com/pixelfed/pixelfed/commit/e0c3dae3))
|
||||
- Update SearchApiV2Service, improve query performance ([4d1f2811](https://github.com/pixelfed/pixelfed/commit/4d1f2811))
|
||||
- Update InstanceService, improve unlisted/banned network post filtering ([a0da6ec3](https://github.com/pixelfed/pixelfed/commit/a0da6ec3))
|
||||
- Update ApiV1DotController, fix inAppRegistrationConfirm logic ([6cfbedd9](https://github.com/pixelfed/pixelfed/commit/6cfbedd9))
|
||||
- Update ApiV1Controller, allow description (alt text) updates after status is published ([869c3ed1](https://github.com/pixelfed/pixelfed/commit/869c3ed1))
|
||||
- Update AdminApiController, fix postgres support ([84fb59d0](https://github.com/pixelfed/pixelfed/commit/84fb59d0))
|
||||
- Update StatusReplyPipeline, fix comment counts ([164aa577](https://github.com/pixelfed/pixelfed/commit/164aa577))
|
||||
- Update ComposeModal, add Alt Text button to caption screen ([4db48188](https://github.com/pixelfed/pixelfed/commit/4db48188))
|
||||
- Update AccountService, fix actor cache invalidation ([498b46f7](https://github.com/pixelfed/pixelfed/commit/498b46f7))
|
||||
- Update SharePipeline, fix share handling and notification generation ([83e1e203](https://github.com/pixelfed/pixelfed/commit/83e1e203))
|
||||
- Update SharePipeline, fix ReblogService and undo handling ([016c6e41](https://github.com/pixelfed/pixelfed/commit/016c6e41))
|
||||
- Update AP Helpers, fix media validation bug that would reject media with alttext/name longer than 255 chars and store remote alt text if set ([a7f58349](https://github.com/pixelfed/pixelfed/commit/a7f58349))
|
||||
- Update MentionPipeline, store non-local mentions ([17149230](https://github.com/pixelfed/pixelfed/commit/17149230))
|
||||
- Update Like model, increase rate limit to 500 likes per day ([ab7676f9](https://github.com/pixelfed/pixelfed/commit/ab7676f9))
|
||||
- Update ComposeController, fix validation issue ([80e6a5a9](https://github.com/pixelfed/pixelfed/commit/80e6a5a9))
|
||||
- Update reply view, fix visibility filtering ([d419af4b](https://github.com/pixelfed/pixelfed/commit/d419af4b))
|
||||
- Update AP helpers, ingest attachments in replies ([c504e643](https://github.com/pixelfed/pixelfed/commit/c504e643))
|
||||
- Update Media model, use cloud filesystem url if enabled instead of cdn_url to easily update S3 media urls ([e6bc57d7](https://github.com/pixelfed/pixelfed/commit/e6bc57d7))
|
||||
- Update ap helpers, fix unset media name bug ([083f506b](https://github.com/pixelfed/pixelfed/commit/083f506b))
|
||||
- Update MediaStorageService, fix improper path ([964c62da](https://github.com/pixelfed/pixelfed/commit/964c62da))
|
||||
- Update ApiV1Controller, fix account statuses and bookmark pagination ([9f66d6b6](https://github.com/pixelfed/pixelfed/commit/9f66d6b6))
|
||||
- Update SearchApiV2Service, improve account search results ([f6a588f9](https://github.com/pixelfed/pixelfed/commit/f6a588f9))
|
||||
- Update profile model, improve avatarUrl fallback ([620ee826](https://github.com/pixelfed/pixelfed/commit/620ee826))
|
||||
- Update ApiV1Controller, use cursor pagination for favourited_by and reblogged_by endpoints ([e1c7e701](https://github.com/pixelfed/pixelfed/commit/e1c7e701))
|
||||
- Update ApiV1Controller, fix favourited_by and reblogged_by follows attribute ([1a130f3e](https://github.com/pixelfed/pixelfed/commit/1a130f3e))
|
||||
- Update notifications component, improve UX with exponential retry and loading state ([937e6d07](https://github.com/pixelfed/pixelfed/commit/937e6d07))
|
||||
- Update likeModal and shareModal components, use new pagination logic and re-add Follow/Unfollow buttons ([b565ead6](https://github.com/pixelfed/pixelfed/commit/b565ead6))
|
||||
- Update profileFeed component, fix pagination ([7cf41628](https://github.com/pixelfed/pixelfed/commit/7cf41628))
|
||||
- Update ApiV1Controller, add BookmarkService logic to bookmark endpoints ([29b1af10](https://github.com/pixelfed/pixelfed/commit/29b1af10))
|
||||
- Update ApiV1Controller, filter conversations without last_status ([e8a6a8c7](https://github.com/pixelfed/pixelfed/commit/e8a6a8c7))
|
||||
- Update ApiV1Controller and BookmarkController, fix api differences and allow unbookmarking regardless of relationship ([e343061a](https://github.com/pixelfed/pixelfed/commit/e343061a))
|
||||
- Update ApiV1Controller, add pixelfed entity support to bookmarks endpoint ([94069db9](https://github.com/pixelfed/pixelfed/commit/94069db9))
|
||||
- Update PostReactions, reduce bookmark timeout to 2s from 5s ([a8094e6c](https://github.com/pixelfed/pixelfed/commit/a8094e6c))
|
||||
- Update CollectionController, fixes #3946 ([abd52f4d](https://github.com/pixelfed/pixelfed/commit/abd52f4d))
|
||||
- Update ComposeController, fix add to collection logic ([9f8957b9](https://github.com/pixelfed/pixelfed/commit/9f8957b9))
|
||||
- Update v1.1 api, add post moderation endpoint ([9bbd6dcd](https://github.com/pixelfed/pixelfed/commit/9bbd6dcd))
|
||||
- Update StatusService, on purge remove from NetworkTimelineService cache ([18940cb2](https://github.com/pixelfed/pixelfed/commit/18940cb2))
|
||||
- Update mute/block logic with admin defined limits and improved filtering to skip deleted accounts ([5b879f01](https://github.com/pixelfed/pixelfed/commit/5b879f01))
|
||||
- Update FollowPipeline, fix followers_count and following_count counters ([6153b620](https://github.com/pixelfed/pixelfed/commit/6153b620))
|
||||
- Update ApiV1Controller, fix media update. Fixes #4196 ([f3164650](https://github.com/pixelfed/pixelfed/commit/f3164650))
|
||||
- Update SearchApiV2Service, fix hashtag search. ([1992b5bc](https://github.com/pixelfed/pixelfed/commit/1992b5bc))
|
||||
- Update ApiV1Controller, allow optional mastodonMode on v2/search endpoint. ([f4a69631](https://github.com/pixelfed/pixelfed/commit/f4a69631))
|
||||
- Update ApiV1Controller, add cursor pagination and pagination link headers to account/{id}/followers and account/{id}/following endpoints with legacy support for `page=` simple pagination ([713aa5fd](https://github.com/pixelfed/pixelfed/commit/713aa5fd))
|
||||
- Update legacy Profile component to use new cursor pagination for following/follower modals ([7a1495e6](https://github.com/pixelfed/pixelfed/commit/7a1495e6))
|
||||
- Update ApiV1Controller, fix link header pagination in /api/v1/statuses/{id}/favourited_by ([adc82eca](https://github.com/pixelfed/pixelfed/commit/adc82eca))
|
||||
- Update ApiV1Controller, fix link header pagination in /api/v1/statuses/{id}/reblogged_by ([e346b675](https://github.com/pixelfed/pixelfed/commit/e346b675))
|
||||
- Update ApiV1Controller, fix following/follower entities, use masto schema by default and update components accordingly ([4716c280](https://github.com/pixelfed/pixelfed/commit/4716c280))
|
||||
- Update FollowerController, remove deprecated /i/follow endpoint ([4739d614](https://github.com/pixelfed/pixelfed/commit/4739d614))
|
||||
- Update queue config, set "after_commit" to true ([304ea956](https://github.com/pixelfed/pixelfed/commit/304ea956))
|
||||
- Update ApiV1Controller, fix home timeline bug ([a8ec8445](https://github.com/pixelfed/pixelfed/commit/a8ec8445))
|
||||
- Update ApiV1Controller, increase home timeline max limit to 100 to fix compatibility with mastoapi ([5cf9ba78](https://github.com/pixelfed/pixelfed/commit/5cf9ba78))
|
||||
- Update ApiV1Controller, preserve album order. Fixes #3708 ([deb26971](https://github.com/pixelfed/pixelfed/commit/deb26971))
|
||||
- Update site config endpoint ([f9be48d6](https://github.com/pixelfed/pixelfed/commit/f9be48d6))
|
||||
- Update Portfolios, add ActivityPub + RSS support, light mode, style customization and more ([5ad0d883](https://github.com/pixelfed/pixelfed/commit/5ad0d883))
|
||||
- Update atom feed, improve cache expiry and fix double encoding bug. Fixes #4121 ([467c9d75](https://github.com/pixelfed/pixelfed/commit/467c9d75))
|
||||
- Update email settings, add dangerzone middleware to prompt for password before you can change your email address. Fixes #4101 ([186ba7f0](https://github.com/pixelfed/pixelfed/commit/186ba7f0))
|
||||
- Update InboxPipelines, improve handling of missing signature validation headers ([419c0fb0](https://github.com/pixelfed/pixelfed/commit/419c0fb0))
|
||||
- Update admin instances dashboard ([ecfc0766](https://github.com/pixelfed/pixelfed/commit/ecfc0766))
|
||||
- Update ap helpers, fix album order bug by setting media order ([871f798c](https://github.com/pixelfed/pixelfed/commit/871f798c))
|
||||
- Update image pipeline, dispatch jobs to mmo queue and add "replace_id" param to v2/media endpoint to dispatch delayed MediaDeletePipeline job for original media id to improve media gc on supported clients ([5a67e9f9](https://github.com/pixelfed/pixelfed/commit/5a67e9f9))
|
||||
- Update admin instance management, improve filtering/sorting and add import/export support ([d5d9500d](https://github.com/pixelfed/pixelfed/commit/d5d9500d))
|
||||
- Update Post component, show state error when status account is null or missing ([e6dc6234](https://github.com/pixelfed/pixelfed/commit/e6dc6234))
|
||||
- Update private profile view, add rel=me support, hide avatar/bio when not logged in and add robots meta tag to block search engine indexing on private profiles ([ab4bb9a0](https://github.com/pixelfed/pixelfed/commit/ab4bb9a0))
|
||||
- Update settings, set maxlength on name and bio inputs. Fixes #4248 ([558700fc](https://github.com/pixelfed/pixelfed/commit/558700fc))
|
||||
- Update api routes, add post method support to /api/v1/accounts/update_credentials to properly handle binary form data (avatars). Fixes #4250 ([1ae19ea5](https://github.com/pixelfed/pixelfed/commit/1ae19ea5))
|
||||
- Update ApiV1Controller, improve timeline account hydration ([4e79c772](https://github.com/pixelfed/pixelfed/commit/4e79c772))
|
||||
|
||||
## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)
|
||||
|
||||
### New Features
|
||||
- Custom content warnings/spoiler text ([d4864213](https://github.com/pixelfed/pixelfed/commit/d4864213))
|
||||
- Add NetworkTimelineService cache ([1310d95c](https://github.com/pixelfed/pixelfed/commit/1310d95c))
|
||||
- Customizable Legal Notice page ([0b7d0a96](https://github.com/pixelfed/pixelfed/commit/0b7d0a96))
|
||||
|
||||
### Breaking
|
||||
- Replaced `predis` with `phpredis` as default redis driver due to predis being deprecated, install [phpredis](https://github.com/phpredis/phpredis/blob/develop/INSTALL.markdown) if you're still using predis.
|
||||
|
||||
### Updates
|
||||
- Improve S3 support by removing `ListObjects` call in media deletion ([#3438](https://github.com/pixelfed/pixelfed/pull/3438))
|
||||
- Enforce UTC in incoming activities ([18931a1f](https://github.com/pixelfed/pixelfed/commit/18931a1f))
|
||||
- Add storage flags to admin dashboard diagnostics ([#3444](https://github.com/pixelfed/pixelfed/pull/3444))
|
||||
- Hardcode UTC application timezone to prevent timezone issues ([b0d2c5e1](https://github.com/pixelfed/pixelfed/commit/b0d2c5e1))
|
||||
- Remove arbitrary metro url redirect timeout ([84209c24](https://github.com/pixelfed/pixelfed/commit/84209c24))
|
||||
- Fix JSON-LD contexts ([#3464](https://github.com/pixelfed/pixelfed/pull/3464))
|
||||
- Fix json-ld attributes, fixes #3423 ([95f902b1](https://github.com/pixelfed/pixelfed/commit/95f902b1))
|
||||
- Add trusted proxies flag to admin dashboard diagnostics ([#3450](https://github.com/pixelfed/pixelfed/pull/3450))
|
||||
- Fix json-ld attributes, fixes #3423 ([95f902b1](https://github.com/pixelfed/pixelfed/commit/95f902b1))
|
||||
- Update exp config, enforce mastoapi compatibility by default ([a160b233](https://github.com/pixelfed/pixelfed/commit/a160b233))
|
||||
- Update home timeline, redirect to /i/web unless force_old_ui is present ([5ff4730f](https://github.com/pixelfed/pixelfed/commit/5ff4730f))
|
||||
- Update adminReportController, fix mail verification request 500 bug by changing filter precedence to catch deleted users that may still be cached in AccountService ([3f322e29](https://github.com/pixelfed/pixelfed/commit/3f322e29))
|
||||
- Update AP Helpers, fix getSensitive and getScope missing parameters ([657c66c1](https://github.com/pixelfed/pixelfed/commit/657c66c1))
|
||||
- Fix mastodon api compatibility ([#3499](https://github.com/pixelfed/pixelfed/pull/3499))
|
||||
- Add ffmpeg config, disable logging by default ([108e3803](https://github.com/pixelfed/pixelfed/commit/108e3803))
|
||||
- Refactor AP profileFetch logic to fix race conditions and improve updating fields and avatars ([505261da](https://github.com/pixelfed/pixelfed/commit/505261da))
|
||||
- Update network timeline api, limit falloff to 2 days ([13a66303](https://github.com/pixelfed/pixelfed/commit/13a66303))
|
||||
- Update Inbox, store follow request activity ([c82f2085](https://github.com/pixelfed/pixelfed/commit/c82f2085))
|
||||
- Update UserFilterService, improve cache strategy by using in-memory state via UserFilterObserver for empty lists with a ttl of 90 days ([9c17def4](https://github.com/pixelfed/pixelfed/commit/9c17def4))
|
||||
- Update ApiV1Controller, add network timeline support via NetworkTimelineService ([f54fd6e9](https://github.com/pixelfed/pixelfed/commit/f54fd6e9))
|
||||
- Bump max_collection_length default to 100 from 18 ([65cf9cca](https://github.com/pixelfed/pixelfed/commit/65cf9cca))
|
||||
- Improve follow request flow, federate rejections and delete rejections from database to properly handle future follow requests from same actor ([4470981a](https://github.com/pixelfed/pixelfed/commit/4470981a))
|
||||
- Update follower counts on follow_request approval ([e97900a0](https://github.com/pixelfed/pixelfed/commit/e97900a0))
|
||||
- Update ApiV1Controller, improve local/remote logic in public timeline endpoint ([4ff179ad](https://github.com/pixelfed/pixelfed/commit/4ff179ad))
|
||||
- Update ApiV1Controller, fix network timeline ([11e99d78](https://github.com/pixelfed/pixelfed/commit/11e99d78))
|
||||
- Update ApiV1Controller, fix public timeline min/max id pagination ([a7613bae](https://github.com/pixelfed/pixelfed/commit/a7613bae))
|
||||
- Improve CollectionService cache invalidation, fixes [#3548](https://github.com/pixelfed/pixelfed/issues/3548) ([44f4a9ed](https://github.com/pixelfed/pixelfed/commit/44f4a9ed))
|
||||
- Improve inbox status deletion cache invalidation ([1eba7f81](https://github.com/pixelfed/pixelfed/commit/1eba7f81))
|
||||
- Update MediaDeletePipeline, fix async media deletion ([bb1cccbe](https://github.com/pixelfed/pixelfed/commit/bb1cccbe))
|
||||
- Fix timeline infinite scroll ([03a85460](https://github.com/pixelfed/pixelfed/commit/03a85460))
|
||||
- Fix remote avatar urls when not using cloud storage ([672f7c8c](https://github.com/pixelfed/pixelfed/commit/672f7c8c))
|
||||
- Update ResetPasswordController redirectTo path to /i/web as /home is deprecated ([8803c6de](https://github.com/pixelfed/pixelfed/commit/8803c6de))
|
||||
- Fix v1 api block/mute endpoints, refresh RelationshipService cache after relationship changes ([54a5c3be](https://github.com/pixelfed/pixelfed/commit/54a5c3be))
|
||||
- Fix NotificationService bug returning html response on /api/v1/notifications endpoint when a notification id belonging to a deleted account is rendered by checking AccountService before NotificationTransformer. ([734b30e5](https://github.com/pixelfed/pixelfed/commit/734b30e5))
|
||||
- Hydrate `favourited` and `reblogged` state on v1 context endpoint ([abb4f7e1](https://github.com/pixelfed/pixelfed/commit/abb4f7e1))
|
||||
- Improve admin dashboard by moving expensive stats to its page and loading stats and recent data async on the dashboard home page ([9d52b9c2](https://github.com/pixelfed/pixelfed/commit/9d52b9c2))
|
||||
- Update unfollow api endpoint to only decrement when appropriate, fixes #3539 ([44de1ad7](https://github.com/pixelfed/pixelfed/commit/44de1ad7))
|
||||
- Improve cache invalidation after processing VideoThumbnail to eliminate "No Preview Available" on grid feeds ([47571887](https://github.com/pixelfed/pixelfed/commit/47571887))
|
||||
- Use poster in VideoPresenter component ([a3cc90b0](https://github.com/pixelfed/pixelfed/commit/a3cc90b0))
|
||||
- Fix mastoapi notification type casting to include comment and share (mention and reblog) notifications ([eba84530](https://github.com/pixelfed/pixelfed/commit/eba84530))
|
||||
- Fix email verification requests filtering to gracefully handle deleted accounts and accounts already verified ([b57066d1](https://github.com/pixelfed/pixelfed/commit/b57066d1))
|
||||
- Add configuration to v1/instance endpoint. Fixes #3605 ([2fb18b7d](https://github.com/pixelfed/pixelfed/commit/2fb18b7d))
|
||||
- Fix remote account post counts ([149cf9dc](https://github.com/pixelfed/pixelfed/commit/149cf9dc))
|
||||
- Enforce blocks on incoming likes, shares, replies and follows on all endpoints ([1545e37c](https://github.com/pixelfed/pixelfed/commit/1545e37c))
|
||||
- Fix unlisted post web redirect and api response ([6033d837](https://github.com/pixelfed/pixelfed/commit/6033d837))
|
||||
- Remove quilljs from admin page editor, fixes #3616 ([75fbd373](https://github.com/pixelfed/pixelfed/commit/75fbd373))
|
||||
- Fix AdminStatService cache key, fixes #3612 ([d1dbed89](https://github.com/pixelfed/pixelfed/commit/d1dbed89))
|
||||
- Improve mute/block v1 api endpoints, fixes #3540 ([c3e8a0e4](https://github.com/pixelfed/pixelfed/commit/c3e8a0e4))
|
||||
- Set Last-Modified header for atom feeds, fixes #2988 ([c18dcde3](https://github.com/pixelfed/pixelfed/commit/c18dcde3))
|
||||
- Add instance post/profile embed config setting ([7734dc03](https://github.com/pixelfed/pixelfed/commit/7734dc03))
|
||||
- Remove remote posts from NetworkTimelineService when processing Tombstones ([2e4f2377](https://github.com/pixelfed/pixelfed/commit/2e4f2377))
|
||||
- Limit NotificationService to 400 items ([f6ed560e](https://github.com/pixelfed/pixelfed/commit/f6ed560e))
|
||||
- Refactor discover accounts endpoint, cache popular accounts and remove following check as most invocations are from new accounts ([016b11f3](https://github.com/pixelfed/pixelfed/commit/016b11f3))
|
||||
- Fix cache invalidation in AdminSettingsController when updating rules ([fe6787f7](https://github.com/pixelfed/pixelfed/commit/fe6787f7))
|
||||
- Update SearchApiService, improve account/webfinger results ([533f7165](https://github.com/pixelfed/pixelfed/commit/533f7165))
|
||||
- Update NotificationService, fix account attribute ([949b7bb6](https://github.com/pixelfed/pixelfed/commit/949b7bb6))
|
||||
- Update DeleteWorker, remove cache lock ([6d6a033a](https://github.com/pixelfed/pixelfed/commit/6d6a033a))
|
||||
- Fix SearchApiV2Service, improve webfinger condition ([9d31f73b](https://github.com/pixelfed/pixelfed/commit/9d31f73b))
|
||||
- Update inbox handler, upsert statuses to fix duplicate bug. Fixes #2670, #2961, #3556 ([2c20d9e3](https://github.com/pixelfed/pixelfed/commit/2c20d9e3))
|
||||
- Update AP helpers, remove cache lock from profileUpdateOrCreate method and move webfinger + key_id to unique constraints to fix sql duplicate errors ([bc2bbc14](https://github.com/pixelfed/pixelfed/commit/bc2bbc14))
|
||||
- Add migrations to fix webfinger profiles ([66aa8bf9](https://github.com/pixelfed/pixelfed/commit/66aa8bf9))
|
||||
- Update ap helpers, move remote_url constraint ([acd8f5bb](https://github.com/pixelfed/pixelfed/commit/acd8f5bb))
|
||||
- Update ApiV1Controller, fix typo in statavouriteById method ([c91a6a75](https://github.com/pixelfed/pixelfed/commit/c91a6a75))
|
||||
- Update InboxPipeline, fix peertube attributedTo parsing ([99fb80bf](https://github.com/pixelfed/pixelfed/commit/99fb80bf))
|
||||
- Update Collection components, fix addId bug #3230 ([62c05665](https://github.com/pixelfed/pixelfed/commit/62c05665))
|
||||
- Update DirectMessageController, include account entity in lookup endpoint ([9e223a6b](https://github.com/pixelfed/pixelfed/commit/9e223a6b))
|
||||
- Update ApiV1Controller update_credentials endpoint to support app response ([61d26e85](https://github.com/pixelfed/pixelfed/commit/61d26e85))
|
||||
- Update PronounService, fix json_decode null parameter ([d72cd819](https://github.com/pixelfed/pixelfed/commit/d72cd819))
|
||||
- Update ApiV1Controller, normalize profile id comparison ([374bfdae](https://github.com/pixelfed/pixelfed/commit/374bfdae))
|
||||
- Update ApiV1Controller, fix pagination header. Fixes #3354 ([4fe07e6f](https://github.com/pixelfed/pixelfed/commit/4fe07e6f))
|
||||
- Update ApiV1Controller, add optional place_id parameter to POST /api/v1/statuses endpoint ([ef0d1f84](https://github.com/pixelfed/pixelfed/commit/ef0d1f84))
|
||||
- Update SettingsController, fix double json encoding and cache settings for 7 days ([4514ab1d](https://github.com/pixelfed/pixelfed/commit/4514ab1d))
|
||||
- Update ApiV1Controller, fix mute/block entities ([364adb43](https://github.com/pixelfed/pixelfed/commit/364adb43))
|
||||
- Update atom feed, remove invalid entities ([e362ef9e](https://github.com/pixelfed/pixelfed/commit/e362ef9e))
|
||||
- Update StatusObserver, handle events after all transactions are committed ([805a014e](https://github.com/pixelfed/pixelfed/commit/805a014e))
|
||||
- Update ApiV1Controller, add collection_ids parameter to /api/v1/statuses endpoint ([7ae21fc3](https://github.com/pixelfed/pixelfed/commit/7ae21fc3))
|
||||
- Update ApiV1Controller, add comments_disabled param to /api/v1/statuses endpoint ([95b58610](https://github.com/pixelfed/pixelfed/commit/95b58610))
|
||||
- Update ap helpers to handle disabled comments ([92f56c9b](https://github.com/pixelfed/pixelfed/commit/92f56c9b))
|
||||
- Update CollectionController, limit max title and description length ([6e76cf4b](https://github.com/pixelfed/pixelfed/commit/6e76cf4b))
|
||||
- Update collection components, fix title/description padding/overflow bug and add title/description limit and input counter ([6e4272a8](https://github.com/pixelfed/pixelfed/commit/6e4272a8))
|
||||
- Update Media model, fix thumbnail cdn paths ([9888af12](https://github.com/pixelfed/pixelfed/commit/9888af12))
|
||||
|
||||
## [v0.11.3 (2022-05-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.2...v0.11.3)
|
||||
|
||||
### Added
|
||||
- Custom Emoji ([#3166](https://github.com/pixelfed/pixelfed/pull/3166))
|
||||
- LDAP Authentication ([#3296](https://github.com/pixelfed/pixelfed/pull/3296))
|
||||
|
||||
### Metro 2.0 UI
|
||||
- Dark Mode ([cb540373](https://github.com/pixelfed/pixelfed/commit/cb540373))
|
||||
- Added Hovercards ([16ced7b4](https://github.com/pixelfed/pixelfed/commit/16ced7b4))
|
||||
- Fix word-break on statuses ([16ced7b4](https://github.com/pixelfed/pixelfed/commit/16ced7b4))
|
||||
- Add pronouns to hovercards ([33f863e8](https://github.com/pixelfed/pixelfed/commit/33f863e8))
|
||||
- Improved onboarding ([042c5b6c](https://github.com/pixelfed/pixelfed/commit/042c5b6c))
|
||||
- Add Hide Counts & Stats setting ([01af7d80](https://github.com/pixelfed/pixelfed/commit/01af7d80))
|
||||
- Fix nsfw videos not displaying sensitive warning ([01af7d80](https://github.com/pixelfed/pixelfed/commit/01af7d80))
|
||||
- Easy Avatar updates - update from timelines with drag-n-drop support ([f37d3798](https://github.com/pixelfed/pixelfed/commit/f37d3798))
|
||||
- Comment hovercards ([f37d3798](https://github.com/pixelfed/pixelfed/commit/f37d3798))
|
||||
- Mod tools button on posts for admins ([f37d3798](https://github.com/pixelfed/pixelfed/commit/f37d3798))
|
||||
- Improved Media Previews - disable to restore original preview aspect ratios ([c55eeac8](https://github.com/pixelfed/pixelfed/commit/c55eeac8))
|
||||
- Moved media license to post header ([390f3ab0](https://github.com/pixelfed/pixelfed/commit/390f3ab0))
|
||||
- Mobile app drawer menu ([7b4318fd](https://github.com/pixelfed/pixelfed/commit/7b4318fd))
|
||||
- Add Preferred Profile Layout UI setting ([a816ea66](https://github.com/pixelfed/pixelfed/commit/a816ea66))
|
||||
- Fix profile masonry layout on mobile. Fixes #3203 ([fdf90f2d](https://github.com/pixelfed/pixelfed/commit/fdf90f2d))
|
||||
- Add search bar to mobile breakpoints and adjust avatar size when necessary ([77b9b6bd](https://github.com/pixelfed/pixelfed/commit/77b9b6bd))
|
||||
- Improved profile layout on mobile breakpoints ([77b9b6bd](https://github.com/pixelfed/pixelfed/commit/77b9b6bd))
|
||||
- New Discover layout with My Hashtags, My Memories, Account Insights, Find Friends and Server Timelines ([0b680099](https://github.com/pixelfed/pixelfed/commit/0b680099))
|
||||
- Fix private profile feed not loading for owner ([e950b3b2](https://github.com/pixelfed/pixelfed/commit/e950b3b2))
|
||||
- Add "Shared by" link to posts that opens a list of accounts that reblogged the post ([e4b4bfc1](https://github.com/pixelfed/pixelfed/commit/e4b4bfc1))
|
||||
- Notification filters ([537af6df](https://github.com/pixelfed/pixelfed/commit/537af6df))
|
||||
- Full screen preview on photo albums ([ac40fde1](https://github.com/pixelfed/pixelfed/commit/ac40fde1))
|
||||
|
||||
### Updated
|
||||
- Updated MediaStorageService, fix remote avatar bug. ([1c20d696](https://github.com/pixelfed/pixelfed/commit/1c20d696))
|
||||
- Updated WebfingerService. Fixes #3167. ([aff74566](https://github.com/pixelfed/pixelfed/commit/aff74566))
|
||||
- Updated ComposeModal, add max file size and allowed mime types. Fixes #3162. ([879281cc](https://github.com/pixelfed/pixelfed/commit/879281cc))
|
||||
- Updated profile embeds, fix NaN bug and improve performance. ([3bd211d7](https://github.com/pixelfed/pixelfed/commit/3bd211d7))
|
||||
- Updated ApiV1Controller, improve follow count cache invalidation. ([4b6effb9](https://github.com/pixelfed/pixelfed/commit/4b6effb9))
|
||||
- Updated web routes, fix atom feeds for account usernames containing a dot. ([8c54ab57](https://github.com/pixelfed/pixelfed/commit/8c54ab57))
|
||||
- Updated atom feeds, include media alt text. Fixes #3184. ([5d9b6863](https://github.com/pixelfed/pixelfed/commit/5d9b6863))
|
||||
- Updated ApiV1Controller, add custom_emoji endpoint. ([16e72518](https://github.com/pixelfed/pixelfed/commit/16e72518))
|
||||
- Updated InternalApiController, redirect remote post and profiles to Metro 2.0. ([3c35158e](https://github.com/pixelfed/pixelfed/commit/3c35158e))
|
||||
- Updated BaseApiController, improve favourites endpoint. ([f063cb01](https://github.com/pixelfed/pixelfed/commit/f063cb01))
|
||||
- Updated ApiV1Controller, invalidate status reply cache on new reply. ([3c261bbf](https://github.com/pixelfed/pixelfed/commit/3c261bbf))
|
||||
- Updated PublicApiController, add bookmark state to timeline endpoints. ([c0b1e042](https://github.com/pixelfed/pixelfed/commit/c0b1e042))
|
||||
- Updated ApiV1Controller, fix private status replies returning 404. ([73226360](https://github.com/pixelfed/pixelfed/commit/73226360))
|
||||
- Updated StatusService, use BookmarkService for bookmarked state. ([a7d71551](https://github.com/pixelfed/pixelfed/commit/a7d71551))
|
||||
- Updated Apis, added ReblogService to improve reblogged state for api entities ([6cfd6be5](https://github.com/pixelfed/pixelfed/commit/6cfd6be5))
|
||||
- Updated InstanceActorController, fix content-type header. ([21792246](https://github.com/pixelfed/pixelfed/commit/21792246))
|
||||
- Updated Exception handler to report validation message bag errors. ([74905ba1](https://github.com/pixelfed/pixelfed/commit/74905ba1))
|
||||
- Updated ApiV1Controller, add validation messages to update_credentials endpoint. ([cd785601](https://github.com/pixelfed/pixelfed/commit/cd785601))
|
||||
- Updated ComposeController, improve location search results ordering by use frequency. ([29c4bd25](https://github.com/pixelfed/pixelfed/commit/29c4bd25))
|
||||
- Updated AvatarController, fix mimetype bug. ([7fa9d4dc](https://github.com/pixelfed/pixelfed/commit/7fa9d4dc))
|
||||
- Updated PostComponent.vue, filter out non-text comments. ([a7346f21](https://github.com/pixelfed/pixelfed/commit/a7346f21))
|
||||
- Updated Profile.vue component, fix v-once bug. ([4d003d00](https://github.com/pixelfed/pixelfed/commit/4d003d00))
|
||||
- Updated filesystems config, set S3 visibility to public by default. Fixes #2913. ([49a53c27](https://github.com/pixelfed/pixelfed/commit/49a53c27))
|
||||
- Updated CommentPipeline, improve parent reply_count calculation. ([ccc94802](https://github.com/pixelfed/pixelfed/commit/ccc94802))
|
||||
- Updated StatusTagsPipeline, process federated hashtags and mentions ([a84b1736](https://github.com/pixelfed/pixelfed/commit/a84b1736))
|
||||
- Updated Inbox, fix undo announce. ([cf286fb0](https://github.com/pixelfed/pixelfed/commit/cf286fb0))
|
||||
- Updated ApiV1Controller, improve favourites endpoint. ([151dc17c](https://github.com/pixelfed/pixelfed/commit/151dc17c))
|
||||
- Updated StatusController, set missing reblog/share type. ([548a12a4](https://github.com/pixelfed/pixelfed/commit/548a12a4))
|
||||
- Updated index view, remove shortcut from favicon meta tag. Fixes #3196. ([6e2cb3cd](https://github.com/pixelfed/pixelfed/commit/6e2cb3cd))
|
||||
- Updated CollectionController, fix broken unauthenticated access. Fixes #3242. ([bd249f0c](https://github.com/pixelfed/pixelfed/commit/bd249f0c))
|
||||
- Updated ComposeController, add collection support to compose endpoint. ([ec2cfaf5](https://github.com/pixelfed/pixelfed/commit/ec2cfaf5))
|
||||
- Updated instance config, match default oauth settings in AuthServiceProvider. ([52f25ff1](https://github.com/pixelfed/pixelfed/commit/52f25ff1))
|
||||
- Updated ComposeModal.vue, fix redirect after posting. Fixes #3254. ([5db64e94](https://github.com/pixelfed/pixelfed/commit/5db64e94))
|
||||
- Updated StatusController, redirect status view for authed users to Metro 2.0 UI. ([71dff472](https://github.com/pixelfed/pixelfed/commit/71dff472))
|
||||
- Updated ProfileController, redirect profile view for authed users to Metro 2.0 UI. ([7f8129a7](https://github.com/pixelfed/pixelfed/commit/7f8129a7))
|
||||
- Updated SpaController, fix variable typo. Fixes #3268. ([8d1af1d6](https://github.com/pixelfed/pixelfed/commit/8d1af1d6))
|
||||
- Updated ComposeModal, fix post redirect on old UI. ([160e32a5](https://github.com/pixelfed/pixelfed/commit/160e32a5))
|
||||
- Updated LikeService, improve caching logic and add profile id to likedBy method to fix #3271. ([6af842eb](https://github.com/pixelfed/pixelfed/commit/6af842eb))
|
||||
- Updated admin diagnostics, add more configuration data to help diagnose potential issues. ([eab96fc3](https://github.com/pixelfed/pixelfed/commit/eab96fc3))
|
||||
- Updated ConfigCacheService, fix discover features. ([ad48521a](https://github.com/pixelfed/pixelfed/commit/ad48521a))
|
||||
- Updated MediaTransformer, fix type case bug. Fixes #3281. ([c1669253](https://github.com/pixelfed/pixelfed/commit/c1669253))
|
||||
- Updated SpaController, redirect web ui hashtags to legacy page for unauthenticated users. ([a44b812b](https://github.com/pixelfed/pixelfed/commit/a44b812b))
|
||||
- Updated ApiV1Controller, fixes #3288. ([3e670774](https://github.com/pixelfed/pixelfed/commit/3e670774))
|
||||
- Updated AP Helpers, fixes #3287. ([b78bff72](https://github.com/pixelfed/pixelfed/commit/b78bff72))
|
||||
- Updated AP Helpers, fixes #3290. ([53975206](https://github.com/pixelfed/pixelfed/commit/53975206))
|
||||
- Updated AccountController, refresh relationship after handling follow request. ([fe768785](https://github.com/pixelfed/pixelfed/commit/fe768785))
|
||||
- Updated CollectionController, fixes #3289. ([c7e1e473](https://github.com/pixelfed/pixelfed/commit/c7e1e473))
|
||||
- Updated SpaController, handle web redirects. ([b6c6c85b](https://github.com/pixelfed/pixelfed/commit/b6c6c85b))
|
||||
- Updated presenter components, remove video poster attribute. ([4d612dfa](https://github.com/pixelfed/pixelfed/commit/4d612dfa))
|
||||
- Improved reblog api performance ([3ef6c9fe](https://github.com/pixelfed/pixelfed/commit/3ef6c9fe))
|
||||
- Updated ApiV1Controller, fix unlisted replies. ([c13bca76](https://github.com/pixelfed/pixelfed/commit/c13bca76))
|
||||
- Updated SearchApiV2Service, filter banned instances. ([281443d7](https://github.com/pixelfed/pixelfed/commit/281443d7))
|
||||
- Updated DiscoverController, fix favourited state on memories. ([b91747b4](https://github.com/pixelfed/pixelfed/commit/b91747b4))
|
||||
- Updated InboxPipeline, fixes #3306. ([20710f4d](https://github.com/pixelfed/pixelfed/commit/20710f4d))
|
||||
- Updated inbox workers, fixes #3304. ([cd4f73be](https://github.com/pixelfed/pixelfed/commit/cd4f73be))
|
||||
- Updated Inbox, fixes #3305. ([14231632](https://github.com/pixelfed/pixelfed/commit/14231632))
|
||||
- Updated Inbox, fixes #3313. ([1c3e72c0](https://github.com/pixelfed/pixelfed/commit/1c3e72c0))
|
||||
- Updated Inbox, fixes #3314. ([dfcd2e6d](https://github.com/pixelfed/pixelfed/commit/dfcd2e6d))
|
||||
- Updated search service, fix banned instance edge case. ([74018e9c](https://github.com/pixelfed/pixelfed/commit/74018e9c))
|
||||
- Updated inbox, fixes #3315. ([c3c3ce18](https://github.com/pixelfed/pixelfed/commit/c3c3ce18))
|
||||
- Updated ApiV1Controller, fix instance endpoint. ([c383f100](https://github.com/pixelfed/pixelfed/commit/c383f100))
|
||||
- Updated ApiV1Controller, marshal json without escaped slashes. ([89303fa4](https://github.com/pixelfed/pixelfed/commit/89303fa4))
|
||||
- Updated ApiV1Controller, fix statusCreate validator. ([b6b15b0c](https://github.com/pixelfed/pixelfed/commit/b6b15b0c))
|
||||
- Updated ApiV1Controller, fix notification entities. ([afe903c3](https://github.com/pixelfed/pixelfed/commit/afe903c3))
|
||||
- Updated FederationController, fix webfinger endpoint. ([a0e15d89](https://github.com/pixelfed/pixelfed/commit/a0e15d89))
|
||||
- Updated ApiV1Controller, fix context entities. ([b1ab41e0](https://github.com/pixelfed/pixelfed/commit/b1ab41e0))
|
||||
- Updated ApiV1Controller, fix timeline default limit. ([a87f8301](https://github.com/pixelfed/pixelfed/commit/a87f8301))
|
||||
- Updated ApiV1Controller, fix search v2 entities. ([9dac861e](https://github.com/pixelfed/pixelfed/commit/9dac861e))
|
||||
- Updated ApiV1Controller, fix apps endpoint. ([50baae52](https://github.com/pixelfed/pixelfed/commit/50baae52))
|
||||
- Updated ApiV1Controller, add apps/verify_credentials endpoint. ([c4d38c20](https://github.com/pixelfed/pixelfed/commit/c4d38c20))
|
||||
- Updated ApiV1Controller, increase max limion timelines. ([df22f2e4](https://github.com/pixelfed/pixelfed/commit/df22f2e4))
|
||||
- Updated ApiV1Controller, add preferences endpoint. ([c3e56b87](https://github.com/pixelfed/pixelfed/commit/c3e56b87))
|
||||
- Updated ApiV1Controller, fix tag timeline limits and remove has(media) constraint. ([8c65d60b](https://github.com/pixelfed/pixelfed/commit/8c65d60b))
|
||||
- Updated ApiV1Controller, add trends endpoint. ([d40a8453](https://github.com/pixelfed/pixelfed/commit/d40a8453))
|
||||
- Updated ApiV1Controller, add announcements endpoint. ([fbe07c51](https://github.com/pixelfed/pixelfed/commit/fbe07c51))
|
||||
- Updated ApiV1Controller, add markers endpoint. ([93a9769e](https://github.com/pixelfed/pixelfed/commit/93a9769e))
|
||||
- Updated ApiV1Controller, increase limits from 80 to 100. ([15eccd44](https://github.com/pixelfed/pixelfed/commit/15eccd44))
|
||||
- Updated ApiV1Controller, fix accountStatusesById endpoint. ([db7b1af3](https://github.com/pixelfed/pixelfed/commit/db7b1af3))
|
||||
- Updated ApiV1Controller, update statusCreate entity. ([a84ab6ea](https://github.com/pixelfed/pixelfed/commit/a84ab6ea))
|
||||
- Updated ApiV1Controller, remove pinned attribute to match MastoAPI Status entity. ([6057de30](https://github.com/pixelfed/pixelfed/commit/6057de30))
|
||||
- Updated controller signatures, fix mysql 8 support. ([72e3d891](https://github.com/pixelfed/pixelfed/commit/72e3d891))
|
||||
- Updated ApiV1Controller, remove no-preview image from media urls. ([37dfb101](https://github.com/pixelfed/pixelfed/commit/37dfb101))
|
||||
- Updated DeleteAccountPipeline, fix perf issues. ([a9edd93f](https://github.com/pixelfed/pixelfed/commit/a9edd93f))
|
||||
- Updated DeleteAccountPipeline, improve coverage. ([4870cc3b](https://github.com/pixelfed/pixelfed/commit/4870cc3b))
|
||||
- Updated media model, use original photo url for non-existent thumbnails. ([9b04b9d8](https://github.com/pixelfed/pixelfed/commit/9b04b9d8))
|
||||
- Updated PlaceController, require authentication. ([e7783af6](https://github.com/pixelfed/pixelfed/commit/e7783af6))
|
||||
- Updated PublicApiController, disable legacy public access to local timeline. ([6ba7d433](https://github.com/pixelfed/pixelfed/commit/6ba7d433))
|
||||
- Updated DiscoverController, cache public tag feed and only include local posts for unauthenticated users. ([0541aed5](https://github.com/pixelfed/pixelfed/commit/0541aed5))
|
||||
- Updated DiscoverController, improve tag feed performance. ([d8ff40eb](https://github.com/pixelfed/pixelfed/commit/d8ff40eb))
|
||||
- Updated ApiV1Controller, fix timeline pagination. ([a5cdc28b](https://github.com/pixelfed/pixelfed/commit/a5cdc28b))
|
||||
- Updated ApiV1Controller, add missing pagination header. ([5649873a](https://github.com/pixelfed/pixelfed/commit/5649873a))
|
||||
- Updated CollectionController, limit unpublished collections to owner. ([a0061eb5](https://github.com/pixelfed/pixelfed/commit/a0061eb5))
|
||||
- Updated AP Inbox, fixes #3332. ([f8931dc7](https://github.com/pixelfed/pixelfed/commit/f8931dc7))
|
||||
- Updated AdminReportController, add account delete button. ([563817a9](https://github.com/pixelfed/pixelfed/commit/563817a9))
|
||||
- Updated ApiV1Controller, added /api/v2/media endpoint, fixes #3405. ([f07cc14c](https://github.com/pixelfed/pixelfed/commit/f07cc14c))
|
||||
- Updated AP fanout, added Content-Type and User-Agent for activity delivery. ([@noellabo](https://github.com/noellabo)) ([209c125](https://github.com/pixelfed/pixelfed/commit/209c125))
|
||||
- Updated DirectMessageController to support new Metro 2.0 UI DMs. ([a4659fd2](https://github.com/pixelfed/pixelfed/commit/a4659fd2))
|
||||
- Updated Like model, bump max likes per day from 100 to 200. ([71ba5fed](https://github.com/pixelfed/pixelfed/commit/71ba5fed))
|
||||
- Updated HashtagService, use sorted set for followed tags. ([153eb6ba](https://github.com/pixelfed/pixelfed/commit/153eb6ba))
|
||||
- Updated Discover component, fixed post side effects (fixes #3409). ([fe5a92b2](https://github.com/pixelfed/pixelfed/commit/fe5a92b2))
|
||||
|
||||
## [v0.11.2 (2022-01-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.1...v0.11.2)
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.1...dev)
|
||||
|
||||
### Breaking
|
||||
- Dropped support for PHP 7.3 [#3041](https://github.com/pixelfed/pixelfed/pull/3041)
|
||||
|
@ -788,8 +9,6 @@
|
|||
- Added UI Settings modal and fixed height media previews setting ([f2467e71](https://github.com/pixelfed/pixelfed/commit/f2467e71))
|
||||
- Set max-width of 1440px for larger screens ([af68872a](https://github.com/pixelfed/pixelfed/commit/af68872a))
|
||||
- Add link to sidebar profile card ([85964510](https://github.com/pixelfed/pixelfed/commit/85964510))
|
||||
- Improved search bar, now resolves (and imports) remote accounts and posts, including webfinger addresses ([c8a667f2](https://github.com/pixelfed/pixelfed/commit/c8a667f2))
|
||||
- Added user facing changelog at `/i/web/whats-new` ([e61dc66a](https://github.com/pixelfed/pixelfed/commit/e61dc66a))
|
||||
|
||||
### Configuration
|
||||
- Enable network timeline by default ([b95aec12](https://github.com/pixelfed/pixelfed/commit/b95aec12))
|
||||
|
@ -866,17 +85,7 @@
|
|||
- Updated ApiV1Controller, fix illegal operator bug by setting default min_id. ([415826f2](https://github.com/pixelfed/pixelfed/commit/415826f2))
|
||||
- Updated StatusService, add getMastodon method for mastoapi compatibility. ([36a129fe](https://github.com/pixelfed/pixelfed/commit/36a129fe))
|
||||
- Updated PublicApiController, fix accountStatuses pagination operator. ([85fc9dd0](https://github.com/pixelfed/pixelfed/commit/85fc9dd0))
|
||||
- Updated PublicApiController, enforce only_media on accountStatuses method. Fixes #3105. ([861a2d36](https://github.com/pixelfed/pixelfed/commit/861a2d36))
|
||||
- Updated ApiV1Controller, add mastoapi strict mode. ([46485426](https://github.com/pixelfed/pixelfed/commit/46485426))
|
||||
- Updated AccountController, refresh RelationshipService on mute/block. ([6f1b0245](https://github.com/pixelfed/pixelfed/commit/6f1b0245))
|
||||
- Updated ApiV1Controller, fix version on instance endpoint. ([a6261221](https://github.com/pixelfed/pixelfed/commit/a6261221))
|
||||
- Updated components, fix api endpoints. Fixes #3138. ([e724633e](https://github.com/pixelfed/pixelfed/commit/e724633e))
|
||||
- Updated ApiV1Controller, fix public timeline endpoint. ([80c7def3](https://github.com/pixelfed/pixelfed/commit/80c7def3))
|
||||
- Updated PublicApiController, fix public timeline endpoint. ([dcb7ba9c](https://github.com/pixelfed/pixelfed/commit/dcb7ba9c))
|
||||
- Updated ApiV1Controller, fix home timeline entities. ([6fc0dcb3](https://github.com/pixelfed/pixelfed/commit/6fc0dcb3))
|
||||
- Updated ApiV1Controller, fix favourites endpoints ([d6d99385](https://github.com/pixelfed/pixelfed/commit/d6d99385))
|
||||
- Updated ApiV1Controller, fix reblogs endpoints ([de42d84c](https://github.com/pixelfed/pixelfed/commit/de42d84c))
|
||||
- Updated SearchApiV2Service, resolve remote queries. ([c8a667f2](https://github.com/pixelfed/pixelfed/commit/c8a667f2))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.1 (2021-09-07)](https://github.com/pixelfed/pixelfed/compare/v0.11.0...v0.11.1)
|
||||
### Added
|
||||
|
@ -1391,7 +600,7 @@
|
|||
- Added post embeds ([1fecf717](https://github.com/pixelfed/pixelfed/commit/1fecf717))
|
||||
- Added profile embeds ([fb7a3cf0](https://github.com/pixelfed/pixelfed/commit/fb7a3cf0))
|
||||
- Added Force MetroUI labs experiment ([#1889](https://github.com/pixelfed/pixelfed/pull/1889))
|
||||
- Added Stories, to enable add ```STORIES_ENABLED=true``` to ```.env``` and run ```php artisan config:cache && php artisan cache:clear```. If opcache is enabled you may need to reload the web server.
|
||||
- Added Stories, to enable add ```STORIES_ENABLED=true``` to ```.env``` and run ```php artisan config:cache && php artisan cache:clear```. If opcache is enabled you may need to reload the web server.
|
||||
|
||||
### Fixed
|
||||
- Fixed like and share/reblog count on profiles ([86cb7d09](https://github.com/pixelfed/pixelfed/commit/86cb7d09))
|
||||
|
|
18
CODEOWNERS
18
CODEOWNERS
|
@ -1,18 +0,0 @@
|
|||
# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
* @dansup
|
||||
|
||||
# Docker related files
|
||||
.editorconfig @jippi @dansup
|
||||
.env @jippi @dansup
|
||||
.env.* @jippi @dansup
|
||||
.hadolint.yaml @jippi @dansup
|
||||
.shellcheckrc @jippi @dansup
|
||||
/.github/ @jippi @dansup
|
||||
/docker/ @jippi @dansup
|
||||
/tests/ @jippi @dansup
|
||||
docker-compose.migrate.yml @jippi @dansup
|
||||
docker-compose.yml @jippi @dansup
|
||||
goss.yaml @jippi @dansup
|
307
Dockerfile
307
Dockerfile
|
@ -1,307 +0,0 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# See https://hub.docker.com/r/docker/dockerfile
|
||||
|
||||
#######################################################
|
||||
# Configuration
|
||||
#######################################################
|
||||
|
||||
# See: https://github.com/mlocati/docker-php-extension-installer
|
||||
ARG DOCKER_PHP_EXTENSION_INSTALLER_VERSION="2.1.80"
|
||||
|
||||
# See: https://github.com/composer/composer
|
||||
ARG COMPOSER_VERSION="2.6"
|
||||
|
||||
# See: https://nginx.org/
|
||||
ARG NGINX_VERSION="1.25.3"
|
||||
|
||||
# See: https://github.com/ddollar/forego
|
||||
ARG FOREGO_VERSION="0.17.2"
|
||||
|
||||
# See: https://github.com/hairyhenderson/gomplate
|
||||
ARG GOMPLATE_VERSION="v3.11.6"
|
||||
|
||||
# See: https://github.com/jippi/dottie
|
||||
ARG DOTTIE_VERSION="v0.9.5"
|
||||
|
||||
###
|
||||
# PHP base configuration
|
||||
###
|
||||
|
||||
# See: https://hub.docker.com/_/php/tags
|
||||
ARG PHP_VERSION="8.1"
|
||||
|
||||
# See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants
|
||||
ARG PHP_BASE_TYPE="apache"
|
||||
ARG PHP_DEBIAN_RELEASE="bullseye"
|
||||
|
||||
ARG RUNTIME_UID=33 # often called 'www-data'
|
||||
ARG RUNTIME_GID=33 # often called 'www-data'
|
||||
|
||||
# APT extra packages
|
||||
ARG APT_PACKAGES_EXTRA=
|
||||
|
||||
# Extensions installed via [pecl install]
|
||||
# ! NOTE: imagick is installed from [master] branch on GitHub due to 8.3 bug on ARM that haven't
|
||||
# ! been released yet (after +10 months)!
|
||||
# ! See: https://github.com/Imagick/imagick/pull/641
|
||||
ARG PHP_PECL_EXTENSIONS="redis https://codeload.github.com/Imagick/imagick/tar.gz/28f27044e435a2b203e32675e942eb8de620ee58"
|
||||
ARG PHP_PECL_EXTENSIONS_EXTRA=
|
||||
|
||||
# Extensions installed via [docker-php-ext-install]
|
||||
ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd"
|
||||
ARG PHP_EXTENSIONS_EXTRA=""
|
||||
ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite"
|
||||
|
||||
# GPG key for nginx apt repository
|
||||
ARG NGINX_GPGKEY="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62"
|
||||
|
||||
# GPP key path for nginx apt repository
|
||||
ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg"
|
||||
|
||||
#######################################################
|
||||
# Docker "copy from" images
|
||||
#######################################################
|
||||
|
||||
# Composer docker image from Docker Hub
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM composer:${COMPOSER_VERSION} AS composer-image
|
||||
|
||||
# php-extension-installer image from Docker Hub
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM mlocati/php-extension-installer:${DOCKER_PHP_EXTENSION_INSTALLER_VERSION} AS php-extension-installer
|
||||
|
||||
# nginx webserver from Docker Hub.
|
||||
# Used to copy some docker-entrypoint files for [nginx-runtime]
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM nginx:${NGINX_VERSION} AS nginx-image
|
||||
|
||||
# Forego is a Procfile "runner" that makes it trival to run multiple
|
||||
# processes under a simple init / PID 1 process.
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
#
|
||||
# See: https://github.com/nginx-proxy/forego
|
||||
FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image
|
||||
|
||||
# Dottie makes working with .env files easier and safer
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
#
|
||||
# See: https://github.com/jippi/dottie
|
||||
FROM ghcr.io/jippi/dottie:${DOTTIE_VERSION} AS dottie-image
|
||||
|
||||
# gomplate-image grabs the gomplate binary from GitHub releases
|
||||
#
|
||||
# It's in its own layer so it can be fetched in parallel with other build steps
|
||||
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
ARG GOMPLATE_VERSION
|
||||
|
||||
RUN set -ex \
|
||||
&& curl \
|
||||
--silent \
|
||||
--show-error \
|
||||
--location \
|
||||
--output /usr/local/bin/gomplate \
|
||||
https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS}-${TARGETARCH} \
|
||||
&& chmod +x /usr/local/bin/gomplate \
|
||||
&& /usr/local/bin/gomplate --version
|
||||
|
||||
#######################################################
|
||||
# Base image
|
||||
#######################################################
|
||||
|
||||
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base
|
||||
|
||||
ARG BUILDKIT_SBOM_SCAN_STAGE="true"
|
||||
|
||||
ARG APT_PACKAGES_EXTRA
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_VERSION
|
||||
ARG RUNTIME_GID
|
||||
ARG RUNTIME_UID
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive"
|
||||
|
||||
# Ensure we run all scripts through 'bash' rather than 'sh'
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
RUN set -ex \
|
||||
&& mkdir -pv /var/www/ \
|
||||
&& chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www
|
||||
|
||||
WORKDIR /var/www/
|
||||
|
||||
ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA}
|
||||
|
||||
# Install and configure base layer
|
||||
COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh
|
||||
|
||||
RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
/docker/install/base.sh
|
||||
|
||||
#######################################################
|
||||
# PHP: extensions
|
||||
#######################################################
|
||||
|
||||
FROM base AS php-extensions
|
||||
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_EXTENSIONS
|
||||
ARG PHP_EXTENSIONS_DATABASE
|
||||
ARG PHP_EXTENSIONS_EXTRA
|
||||
ARG PHP_PECL_EXTENSIONS
|
||||
ARG PHP_PECL_EXTENSIONS_EXTRA
|
||||
ARG PHP_VERSION
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
|
||||
|
||||
COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh
|
||||
|
||||
RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/tmp/pear \
|
||||
--mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
PHP_EXTENSIONS=${PHP_EXTENSIONS} \
|
||||
PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} \
|
||||
PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} \
|
||||
PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} \
|
||||
PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} \
|
||||
/docker/install/php-extensions.sh
|
||||
|
||||
#######################################################
|
||||
# PHP: composer and source code
|
||||
#######################################################
|
||||
|
||||
FROM php-extensions AS composer-and-src
|
||||
|
||||
ARG PHP_VERSION
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG RUNTIME_UID
|
||||
ARG RUNTIME_GID
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Make sure composer cache is targeting our cache mount later
|
||||
ENV COMPOSER_CACHE_DIR="/cache/composer"
|
||||
|
||||
# Don't enforce any memory limits for composer
|
||||
ENV COMPOSER_MEMORY_LIMIT=-1
|
||||
|
||||
# Disable interactvitity from composer
|
||||
ENV COMPOSER_NO_INTERACTION=1
|
||||
|
||||
# Copy composer from https://hub.docker.com/_/composer
|
||||
COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer
|
||||
|
||||
#! Changing user to runtime user
|
||||
USER ${RUNTIME_UID}:${RUNTIME_GID}
|
||||
|
||||
# Install composer dependencies
|
||||
# NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet)
|
||||
RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION},sharing=locked,target=/cache/composer \
|
||||
--mount=type=bind,source=composer.json,target=/var/www/composer.json \
|
||||
--mount=type=bind,source=composer.lock,target=/var/www/composer.lock \
|
||||
set -ex \
|
||||
&& composer install --prefer-dist --no-autoloader --ignore-platform-reqs
|
||||
|
||||
# Copy all other files over
|
||||
COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/
|
||||
|
||||
#######################################################
|
||||
# Runtime: base
|
||||
#######################################################
|
||||
|
||||
FROM php-extensions AS shared-runtime
|
||||
|
||||
ARG RUNTIME_GID
|
||||
ARG RUNTIME_UID
|
||||
|
||||
ENV RUNTIME_UID=${RUNTIME_UID}
|
||||
ENV RUNTIME_GID=${RUNTIME_GID}
|
||||
|
||||
COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego
|
||||
COPY --link --from=dottie-image /dottie /usr/local/bin/dottie
|
||||
COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate
|
||||
COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer
|
||||
COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www
|
||||
|
||||
#! Changing user to runtime user
|
||||
USER ${RUNTIME_UID}:${RUNTIME_GID}
|
||||
|
||||
# Generate optimized autoloader now that we have all files around
|
||||
RUN set -ex \
|
||||
&& ENABLE_CONFIG_CACHE=false composer dump-autoload --optimize
|
||||
|
||||
USER root
|
||||
|
||||
# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862
|
||||
RUN set -ex \
|
||||
&& cp --recursive --link --preserve=all storage storage.skel \
|
||||
&& rm -rf html && ln -s public html
|
||||
|
||||
COPY docker/shared/root /
|
||||
|
||||
ENTRYPOINT ["/docker/entrypoint.sh"]
|
||||
|
||||
#######################################################
|
||||
# Runtime: apache
|
||||
#######################################################
|
||||
|
||||
FROM shared-runtime AS apache-runtime
|
||||
|
||||
COPY docker/apache/root /
|
||||
|
||||
RUN set -ex \
|
||||
&& a2enmod rewrite remoteip proxy proxy_http \
|
||||
&& a2enconf remoteip
|
||||
|
||||
CMD ["apache2-foreground"]
|
||||
|
||||
#######################################################
|
||||
# Runtime: fpm
|
||||
#######################################################
|
||||
|
||||
FROM shared-runtime AS fpm-runtime
|
||||
|
||||
COPY docker/fpm/root /
|
||||
|
||||
CMD ["php-fpm"]
|
||||
|
||||
#######################################################
|
||||
# Runtime: nginx
|
||||
#######################################################
|
||||
|
||||
FROM shared-runtime AS nginx-runtime
|
||||
|
||||
ARG NGINX_GPGKEY
|
||||
ARG NGINX_GPGKEY_PATH
|
||||
ARG NGINX_VERSION
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_VERSION
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Install nginx dependencies
|
||||
RUN --mount=type=cache,id=pixelfed-apt-lists-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt/lists \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
set -ex \
|
||||
&& gpg1 --keyserver "hkp://keyserver.ubuntu.com:80" --keyserver-options timeout=10 --recv-keys "${NGINX_GPGKEY}" \
|
||||
&& gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" \
|
||||
&& echo "deb [signed-by=${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ ${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nginx=${NGINX_VERSION}*
|
||||
|
||||
# copy docker entrypoints from the *real* nginx image directly
|
||||
COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/
|
||||
COPY docker/nginx/root /
|
||||
COPY docker/nginx/Procfile .
|
||||
|
||||
STOPSIGNAL SIGQUIT
|
||||
|
||||
CMD ["forego", "start", "-r"]
|
|
@ -11,10 +11,7 @@ class AccountInterstitial extends Model
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'read_at' => 'datetime',
|
||||
'appeal_requested_at' => 'datetime'
|
||||
];
|
||||
protected $dates = ['read_at', 'appeal_requested_at'];
|
||||
|
||||
public const JSON_MESSAGE = 'Please use web browser to proceed.';
|
||||
|
||||
|
|
|
@ -6,10 +6,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class Activity extends Model
|
||||
{
|
||||
protected $casts = [
|
||||
'processed_at' => 'datetime'
|
||||
];
|
||||
|
||||
protected $dates = ['processed_at'];
|
||||
protected $fillable = ['data', 'to_id', 'from_id', 'object_type'];
|
||||
|
||||
public function toProfile()
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Auth;
|
||||
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
|
||||
class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerTokenResponse
|
||||
{
|
||||
/**
|
||||
* Add custom fields to your Bearer Token response here, then override
|
||||
* AuthorizationServer::getResponseType() to pull in your version of
|
||||
* this class rather than the default.
|
||||
*
|
||||
* @param AccessTokenEntityInterface $accessToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getExtraParams(AccessTokenEntityInterface $accessToken)
|
||||
{
|
||||
return [
|
||||
'created_at' => time(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -14,13 +14,13 @@ class Avatar extends Model
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'deleted_at' => 'datetime',
|
||||
'last_fetched_at' => 'datetime',
|
||||
'last_processed_at' => 'datetime'
|
||||
protected $dates = [
|
||||
'deleted_at',
|
||||
'last_fetched_at',
|
||||
'last_processed_at'
|
||||
];
|
||||
|
||||
protected $guarded = [];
|
||||
protected $fillable = ['profile_id'];
|
||||
|
||||
protected $visible = [
|
||||
'id',
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\Account\AccountStatService;
|
||||
use App\Status;
|
||||
use App\Profile;
|
||||
|
||||
class AccountPostCountStatUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:account-post-count-stat-update';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update post counts from recent activities';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$ids = AccountStatService::getAllPostCountIncr();
|
||||
if(!$ids || !count($ids)) {
|
||||
return;
|
||||
}
|
||||
foreach($ids as $id) {
|
||||
$acct = AccountService::get($id, true);
|
||||
if(!$acct) {
|
||||
AccountStatService::removeFromPostCount($id);
|
||||
continue;
|
||||
}
|
||||
$statusCount = Status::whereProfileId($id)->count();
|
||||
if($statusCount != $acct['statuses_count']) {
|
||||
$profile = Profile::find($id);
|
||||
if(!$profile) {
|
||||
AccountStatService::removeFromPostCount($id);
|
||||
continue;
|
||||
}
|
||||
$profile->status_count = $statusCount;
|
||||
$profile->save();
|
||||
AccountService::del($id);
|
||||
}
|
||||
AccountStatService::removeFromPostCount($id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\User;
|
||||
use App\Models\DefaultDomainBlock;
|
||||
use App\Models\UserDomainBlock;
|
||||
use function Laravel\Prompts\text;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class AddUserDomainBlock extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:add-user-domain-block';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Apply a domain block to all users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$domain = text('Enter domain you want to block');
|
||||
$domain = strtolower($domain);
|
||||
$domain = $this->validateDomain($domain);
|
||||
if(!$domain || empty($domain)) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
$this->processBlocks($domain);
|
||||
return;
|
||||
}
|
||||
|
||||
protected function validateDomain($domain)
|
||||
{
|
||||
if(!strpos($domain, '.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'https://')) {
|
||||
$domain = str_replace('https://', '', $domain);
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'http://')) {
|
||||
$domain = str_replace('http://', '', $domain);
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
|
||||
|
||||
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
|
||||
if(!$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($domain === config('pixelfed.domain.app')) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
|
||||
$confirmed = confirm('Are you sure you want to block ' . $domain . '?');
|
||||
if(!$confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
protected function processBlocks($domain)
|
||||
{
|
||||
DefaultDomainBlock::updateOrCreate([
|
||||
'domain' => $domain
|
||||
]);
|
||||
progress(
|
||||
label: 'Updating user domain blocks...',
|
||||
steps: User::lazyById(500),
|
||||
callback: fn ($user) => $this->performTask($user, $domain),
|
||||
);
|
||||
}
|
||||
|
||||
protected function performTask($user, $domain)
|
||||
{
|
||||
if(!$user->profile_id || $user->delete_after) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($user->status != null && $user->status != 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
UserDomainBlock::updateOrCreate([
|
||||
'profile_id' => $user->profile_id,
|
||||
'domain' => $domain
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\AdminInvite;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdminInviteCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'admin:invite';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Create an invite link';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' Pixelfed Admin Inviter');
|
||||
$this->line(' ');
|
||||
$this->info(' Manage user registration invite links');
|
||||
$this->line(' ');
|
||||
|
||||
$action = $this->choice(
|
||||
'Select an action',
|
||||
[
|
||||
'Create invite',
|
||||
'View invites',
|
||||
'Expire invite',
|
||||
'Cancel'
|
||||
],
|
||||
3
|
||||
);
|
||||
|
||||
switch($action) {
|
||||
case 'Create invite':
|
||||
return $this->create();
|
||||
break;
|
||||
|
||||
case 'View invites':
|
||||
return $this->view();
|
||||
break;
|
||||
|
||||
case 'Expire invite':
|
||||
return $this->expire();
|
||||
break;
|
||||
|
||||
case 'Cancel':
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function create()
|
||||
{
|
||||
$this->info('Create Invite');
|
||||
$this->line('=============');
|
||||
$this->info('Set an optional invite name (only visible to admins)');
|
||||
$name = $this->ask('Invite Name (optional)', 'Untitled Invite');
|
||||
|
||||
$this->info('Set an optional invite description (only visible to admins)');
|
||||
$description = $this->ask('Invite Description (optional)');
|
||||
|
||||
$this->info('Set an optional message to invitees (visible to all)');
|
||||
$message = $this->ask('Invite Message (optional)', 'You\'ve been invited to join');
|
||||
|
||||
$this->info('Set maximum # of invite uses, use 0 for unlimited');
|
||||
$max_uses = $this->ask('Max uses', 1);
|
||||
|
||||
$shouldExpire = $this->choice(
|
||||
'Set an invite expiry date?',
|
||||
[
|
||||
'No - invite never expires',
|
||||
'Yes - expire after 24 hours',
|
||||
'Custom - let me pick an expiry date'
|
||||
],
|
||||
0
|
||||
);
|
||||
switch($shouldExpire) {
|
||||
case 'No - invite never expires':
|
||||
$expires = null;
|
||||
break;
|
||||
|
||||
case 'Yes - expire after 24 hours':
|
||||
$expires = now()->addHours(24);
|
||||
break;
|
||||
|
||||
case 'Custom - let me pick an expiry date':
|
||||
$this->info('Set custom expiry date in days');
|
||||
$customExpiry = $this->ask('Custom Expiry', 14);
|
||||
$expires = now()->addDays($customExpiry);
|
||||
break;
|
||||
}
|
||||
|
||||
$this->info('Skip email verification for invitees?');
|
||||
$skipEmailVerification = $this->choice('Skip email verification', ['No', 'Yes'], 0);
|
||||
|
||||
$invite = new AdminInvite;
|
||||
$invite->name = $name;
|
||||
$invite->description = $description;
|
||||
$invite->message = $message;
|
||||
$invite->max_uses = $max_uses;
|
||||
$invite->skip_email_verification = $skipEmailVerification === 'Yes';
|
||||
$invite->expires_at = $expires;
|
||||
$invite->invite_code = Str::uuid() . Str::random(random_int(1,6));
|
||||
$invite->save();
|
||||
|
||||
$this->info('####################');
|
||||
$this->info('# Invite Generated!');
|
||||
$this->line(' ');
|
||||
$this->info($invite->url());
|
||||
$this->line(' ');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function view()
|
||||
{
|
||||
$this->info('View Invites');
|
||||
$this->line('=============');
|
||||
if(AdminInvite::count() == 0) {
|
||||
$this->line(' ');
|
||||
$this->error('No invites found!');
|
||||
return;
|
||||
}
|
||||
$this->table(
|
||||
['Invite Code', 'Uses Left', 'Expires'],
|
||||
AdminInvite::all(['invite_code', 'max_uses', 'uses', 'expires_at'])->map(function($invite) {
|
||||
return [
|
||||
'invite_code' => $invite->invite_code,
|
||||
'uses_left' => $invite->max_uses ? ($invite->max_uses - $invite->uses) : '∞',
|
||||
'expires_at' => $invite->expires_at ? $invite->expires_at->diffForHumans() : 'never'
|
||||
];
|
||||
})->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
protected function expire()
|
||||
{
|
||||
$token = $this->anticipate('Enter invite code to expire', function($val) {
|
||||
if(!$val || empty($val)) {
|
||||
return [];
|
||||
}
|
||||
return AdminInvite::where('invite_code', 'like', '%' . $val . '%')->pluck('invite_code')->toArray();
|
||||
});
|
||||
|
||||
if(!$token || empty($token)) {
|
||||
$this->error('Invalid invite code');
|
||||
return;
|
||||
}
|
||||
$invite = AdminInvite::whereInviteCode($token)->first();
|
||||
if(!$invite) {
|
||||
$this->error('Invalid invite code');
|
||||
return;
|
||||
}
|
||||
$invite->max_uses = 1;
|
||||
$invite->expires_at = now()->subHours(2);
|
||||
$invite->save();
|
||||
$this->info('Expired the following invite: ' . $invite->url());
|
||||
}
|
||||
}
|
|
@ -1,293 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Avatar;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use Cache;
|
||||
use Storage;
|
||||
use App\Services\AccountService;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
|
||||
|
||||
class AvatarStorage extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'avatar:storage';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Manage avatar storage';
|
||||
|
||||
public $found = 0;
|
||||
public $notFetched = 0;
|
||||
public $fixed = 0;
|
||||
public $missing = 0;
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Pixelfed Avatar Storage Manager');
|
||||
$this->line(' ');
|
||||
$segments = [
|
||||
[
|
||||
'Local',
|
||||
Avatar::whereNull('is_remote')->count(),
|
||||
PrettyNumber::size(Avatar::whereNull('is_remote')->sum('size'))
|
||||
],
|
||||
[
|
||||
'Remote',
|
||||
Avatar::whereIsRemote(true)->count(),
|
||||
PrettyNumber::size(Avatar::whereIsRemote(true)->sum('size'))
|
||||
],
|
||||
[
|
||||
'Cached (CDN)',
|
||||
Avatar::whereNotNull('cdn_url')->count(),
|
||||
PrettyNumber::size(Avatar::whereNotNull('cdn_url')->sum('size'))
|
||||
],
|
||||
[
|
||||
'Uncached',
|
||||
Avatar::whereNull('cdn_url')->count(),
|
||||
PrettyNumber::size(Avatar::whereNull('cdn_url')->sum('size'))
|
||||
],
|
||||
[
|
||||
'------------',
|
||||
'----------',
|
||||
'-----'
|
||||
],
|
||||
[
|
||||
'Total',
|
||||
Avatar::count(),
|
||||
PrettyNumber::size(Avatar::sum('size'))
|
||||
],
|
||||
];
|
||||
$this->table(
|
||||
['Segment', 'Count', 'Space Used'],
|
||||
$segments
|
||||
);
|
||||
|
||||
$this->line(' ');
|
||||
|
||||
if((bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$this->info('✅ - Cloud storage configured');
|
||||
$this->line(' ');
|
||||
}
|
||||
|
||||
if(config('instance.avatar.local_to_cloud')) {
|
||||
$this->info('✅ - Store avatars on cloud filesystem');
|
||||
$this->line(' ');
|
||||
}
|
||||
|
||||
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 ? '✅' : '❌';
|
||||
$msg = $state . ' - Cloud default avatar exists';
|
||||
$this->info($msg);
|
||||
}
|
||||
|
||||
$options = (bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
|
||||
[
|
||||
'Cancel',
|
||||
'Upload default avatar to cloud',
|
||||
'Move local avatars to cloud',
|
||||
'Re-fetch remote avatars'
|
||||
] : [
|
||||
'Cancel',
|
||||
'Re-fetch remote avatars'
|
||||
];
|
||||
|
||||
$this->missing = Profile::where('created_at', '<', now()->subDays(1))->doesntHave('avatar')->count();
|
||||
if($this->missing != 0) {
|
||||
$options[] = 'Fix missing avatars';
|
||||
}
|
||||
|
||||
$choice = $this->choice(
|
||||
'Select action:',
|
||||
$options,
|
||||
0
|
||||
);
|
||||
|
||||
return $this->handleChoice($choice);
|
||||
}
|
||||
|
||||
protected function handleChoice($id)
|
||||
{
|
||||
switch ($id) {
|
||||
case 'Cancel':
|
||||
return;
|
||||
break;
|
||||
|
||||
case 'Upload default avatar to cloud':
|
||||
return $this->uploadDefaultAvatar();
|
||||
break;
|
||||
|
||||
case 'Move local avatars to cloud':
|
||||
return $this->uploadAvatarsToCloud();
|
||||
break;
|
||||
|
||||
case 'Re-fetch remote avatars':
|
||||
return $this->refetchRemoteAvatars();
|
||||
break;
|
||||
|
||||
case 'Fix missing avatars':
|
||||
return $this->fixMissingAvatars();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function uploadDefaultAvatar()
|
||||
{
|
||||
if(!$this->confirm('Are you sure you want to upload the default avatar to the cloud storage disk?')) {
|
||||
return;
|
||||
}
|
||||
$disk = Storage::disk(config_cache('filesystems.cloud'));
|
||||
$disk->put('cache/avatars/default.jpg', Storage::get('public/avatars/default.jpg'));
|
||||
Avatar::whereMediaPath('public/avatars/default.jpg')->update(['cdn_url' => $disk->url('cache/avatars/default.jpg')]);
|
||||
$this->info('Successfully uploaded default avatar to cloud storage!');
|
||||
$this->info($disk->url('cache/avatars/default.jpg'));
|
||||
}
|
||||
|
||||
protected function uploadAvatarsToCloud()
|
||||
{
|
||||
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;
|
||||
}
|
||||
$confirm = $this->confirm('Are you sure you want to move local avatars to cloud storage?');
|
||||
if(!$confirm) {
|
||||
$this->error('Aborted action');
|
||||
return;
|
||||
}
|
||||
|
||||
$disk = Storage::disk(config_cache('filesystems.cloud'));
|
||||
|
||||
if($disk->missing('cache/avatars/default.jpg')) {
|
||||
$disk->put('cache/avatars/default.jpg', Storage::get('public/avatars/default.jpg'));
|
||||
}
|
||||
|
||||
Avatar::whereNull('is_remote')->chunk(5, function($avatars) use($disk) {
|
||||
foreach($avatars as $avatar) {
|
||||
if($avatar->media_path === 'public/avatars/default.jpg') {
|
||||
$avatar->cdn_url = $disk->url('cache/avatars/default.jpg');
|
||||
$avatar->save();
|
||||
} else {
|
||||
if(!$avatar->media_path || !Str::of($avatar->media_path)->startsWith('public/avatars/')) {
|
||||
continue;
|
||||
}
|
||||
$ext = pathinfo($avatar->media_path, PATHINFO_EXTENSION);
|
||||
$newPath = 'cache/avatars/' . $avatar->profile_id . '/avatar_' . strtolower(Str::random(6)) . '.' . $ext;
|
||||
$existing = Storage::disk('local')->get($avatar->media_path);
|
||||
if(!$existing) {
|
||||
continue;
|
||||
}
|
||||
$newMediaPath = $disk->put($newPath, $existing);
|
||||
$avatar->media_path = $newPath;
|
||||
$avatar->cdn_url = $disk->url($newPath);
|
||||
$avatar->save();
|
||||
}
|
||||
|
||||
Cache::forget('avatar:' . $avatar->profile_id);
|
||||
Cache::forget(AccountService::CACHE_KEY . $avatar->profile_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function refetchRemoteAvatars()
|
||||
{
|
||||
if(!$this->confirm('Are you sure you want to refetch all remote avatars? This could take a while.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$count = Profile::has('avatar')
|
||||
->with('avatar')
|
||||
->whereNull('user_id')
|
||||
->count();
|
||||
|
||||
$this->info('Found ' . $count . ' remote avatars to re-fetch');
|
||||
$this->line(' ');
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
|
||||
Profile::has('avatar')
|
||||
->with('avatar')
|
||||
->whereNull('user_id')
|
||||
->chunk(50, function($profiles) use($bar) {
|
||||
foreach($profiles as $profile) {
|
||||
$avatar = $profile->avatar;
|
||||
$avatar->last_fetched_at = null;
|
||||
$avatar->save();
|
||||
RemoteAvatarFetch::dispatch($profile)->onQueue('low');
|
||||
$bar->advance();
|
||||
}
|
||||
});
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
$this->info('Finished dispatching avatar refetch jobs!');
|
||||
$this->line(' ');
|
||||
$this->info('This may take a few minutes to complete, you may need to run "php artisan cache:clear" after the jobs are processed.');
|
||||
$this->line(' ');
|
||||
}
|
||||
|
||||
protected function incr($name)
|
||||
{
|
||||
switch($name) {
|
||||
case 'found':
|
||||
$this->found = $this->found + 1;
|
||||
break;
|
||||
|
||||
case 'notFetched':
|
||||
$this->notFetched = $this->notFetched + 1;
|
||||
break;
|
||||
|
||||
case 'fixed':
|
||||
$this->fixed++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function fixMissingAvatars()
|
||||
{
|
||||
if(!$this->confirm('Are you sure you want to fix missing avatars?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info('Found ' . $this->missing . ' accounts with missing profiles');
|
||||
|
||||
Profile::where('created_at', '<', now()->subDays(1))
|
||||
->doesntHave('avatar')
|
||||
->chunk(50, function($profiles) {
|
||||
foreach($profiles as $profile) {
|
||||
Avatar::updateOrCreate([
|
||||
'profile_id' => $profile->id
|
||||
], [
|
||||
'media_path' => 'public/avatars/default.jpg',
|
||||
'is_remote' => $profile->domain == null && $profile->private_key == null
|
||||
]);
|
||||
$this->incr('fixed');
|
||||
}
|
||||
});
|
||||
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
$this->info('Fixed ' . $this->fixed . ' accounts with a blank avatar');
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Cache;
|
||||
use Storage;
|
||||
use App\Avatar;
|
||||
use App\Jobs\AvatarPipeline\AvatarStorageCleanup;
|
||||
|
||||
class AvatarStorageDeepClean extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'avatar:storage-deep-clean';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Cleanup avatar storage';
|
||||
|
||||
protected $shouldKeepRunning = true;
|
||||
protected $counter = 0;
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' Pixelfed Avatar Deep Cleaner');
|
||||
$this->line(' ');
|
||||
$this->info(' Purge/delete old and outdated avatars from remote accounts');
|
||||
$this->line(' ');
|
||||
|
||||
$storage = [
|
||||
'cloud' => (bool) config_cache('pixelfed.cloud_storage'),
|
||||
'local' => boolval(config_cache('federation.avatars.store_local'))
|
||||
];
|
||||
|
||||
if(!$storage['cloud'] && !$storage['local']) {
|
||||
$this->error('Remote avatars are not cached locally, there is nothing to purge. Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$start = 0;
|
||||
|
||||
if(!$this->confirm('Are you sure you want to proceed?')) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
if(!$this->activeCheck()) {
|
||||
$this->info('Found existing deep cleaning job');
|
||||
if(!$this->confirm('Do you want to continue where you left off?')) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
} else {
|
||||
$start = Cache::has('cmd:asdp') ? (int) Cache::get('cmd:asdp') : (int) Storage::get('avatar-deep-clean.json');
|
||||
|
||||
if($start && $start < 1 || $start > PHP_INT_MAX) {
|
||||
$this->error('Error fetching cached value');
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$count = Avatar::whereNotNull('cdn_url')->where('is_remote', true)->where('id', '>', $start)->count();
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
|
||||
foreach(Avatar::whereNotNull('cdn_url')->where('is_remote', true)->where('id', '>', $start)->lazyById(10, 'id') as $avatar) {
|
||||
usleep(random_int(50, 1000));
|
||||
$this->counter++;
|
||||
$this->handleAvatar($avatar);
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
}
|
||||
|
||||
protected function updateCache($id)
|
||||
{
|
||||
Cache::put('cmd:asdp', $id);
|
||||
if($this->counter % 5 === 0) {
|
||||
Storage::put('avatar-deep-clean.json', $id);
|
||||
}
|
||||
}
|
||||
|
||||
protected function activeCheck()
|
||||
{
|
||||
if(Storage::exists('avatar-deep-clean.json') || Cache::has('cmd:asdp')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function handleAvatar($avatar)
|
||||
{
|
||||
$this->updateCache($avatar->id);
|
||||
$queues = ['feed', 'mmo', 'feed', 'mmo', 'feed', 'feed', 'mmo', 'low'];
|
||||
$queue = $queues[random_int(0, 7)];
|
||||
AvatarStorageCleanup::dispatch($avatar)->onQueue($queue);
|
||||
}
|
||||
}
|
|
@ -48,18 +48,6 @@ class AvatarSync extends Command
|
|||
public function handle()
|
||||
{
|
||||
$this->info('Welcome to the avatar sync manager');
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
$this->error('This command is deprecated and will be removed in a future version');
|
||||
$this->error('You should use the following command instead: ');
|
||||
$this->line(' ');
|
||||
$this->info('php artisan avatar:storage');
|
||||
$this->line(' ');
|
||||
|
||||
$confirm = $this->confirm('Are you sure you want to use this deprecated command even though it is no longer supported?');
|
||||
if(!$confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actions = [
|
||||
'Analyze',
|
||||
|
@ -135,7 +123,7 @@ class AvatarSync extends Command
|
|||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
Profile::chunk(50, function($profiles) use ($bar) {
|
||||
Profile::chunk(5000, function($profiles) use ($bar) {
|
||||
foreach($profiles as $profile) {
|
||||
if($profile->domain == null) {
|
||||
$bar->advance();
|
||||
|
@ -158,11 +146,41 @@ class AvatarSync extends Command
|
|||
|
||||
protected function fetch()
|
||||
{
|
||||
$this->error('This action has been deprecated, please run the following command instead:');
|
||||
$this->line(' ');
|
||||
$this->info('php artisan avatar:storage');
|
||||
$this->line(' ');
|
||||
return;
|
||||
$this->info('Fetching ....');
|
||||
Avatar::whereIsRemote(true)
|
||||
->whereNull('cdn_url')
|
||||
// ->with('profile')
|
||||
->chunk(10, function($avatars) {
|
||||
foreach($avatars as $avatar) {
|
||||
if(!$avatar || !$avatar->profile) {
|
||||
continue;
|
||||
}
|
||||
$url = $avatar->profile->remote_url;
|
||||
if(!$url || !Helpers::validateUrl($url)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$res = Helpers::fetchFromUrl($url);
|
||||
if(
|
||||
!is_array($res) ||
|
||||
!isset($res['@context']) ||
|
||||
!isset($res['icon']) ||
|
||||
!isset($res['icon']['type']) ||
|
||||
!isset($res['icon']['url']) ||
|
||||
!Str::endsWith($res['icon']['url'], ['.png', '.jpg', '.jpeg'])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||
continue;
|
||||
} catch(\Illuminate\Http\Client\ConnectionException $e) {
|
||||
continue;
|
||||
}
|
||||
$avatar->remote_url = $res['icon']['url'];
|
||||
$avatar->save();
|
||||
RemoteAvatarFetch::dispatch($avatar->profile);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function fix()
|
||||
|
@ -190,10 +208,12 @@ class AvatarSync extends Command
|
|||
|
||||
protected function sync()
|
||||
{
|
||||
$this->error('This action has been deprecated, please run the following command instead:');
|
||||
$this->line(' ');
|
||||
$this->info('php artisan avatar:storage');
|
||||
$this->line(' ');
|
||||
return;
|
||||
Avatar::whereIsRemote(true)
|
||||
->with('profile')
|
||||
->chunk(10, function($avatars) {
|
||||
foreach($avatars as $avatar) {
|
||||
RemoteAvatarFetch::dispatch($avatar->profile);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,20 +40,22 @@ class CatchUnoptimizedMedia extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
Media::whereNull('processed_at')
|
||||
->where('created_at', '>', now()->subHours(1))
|
||||
->where('skip_optimize', '!=', true)
|
||||
->whereNull('remote_url')
|
||||
->whereNotNull('status_id')
|
||||
->whereNotNull('media_path')
|
||||
->whereIn('mime', [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
])
|
||||
->chunk(50, function($medias) {
|
||||
foreach ($medias as $media) {
|
||||
ImageOptimize::dispatch($media);
|
||||
}
|
||||
});
|
||||
DB::transaction(function() {
|
||||
Media::whereNull('processed_at')
|
||||
->where('skip_optimize', '!=', true)
|
||||
->whereNull('remote_url')
|
||||
->whereNotNull('status_id')
|
||||
->whereNotNull('media_path')
|
||||
->where('created_at', '>', now()->subHours(1))
|
||||
->whereIn('mime', [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
])
|
||||
->chunk(50, function($medias) {
|
||||
foreach ($medias as $media) {
|
||||
ImageOptimize::dispatch($media);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Media;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class CloudMediaMigrate extends Command
|
||||
{
|
||||
public $totalSize = 0;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:migrate2cloud {--limit=200} {--huge}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Move older media to cloud storage';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$enabled = (bool) config_cache('pixelfed.cloud_storage');
|
||||
if(!$enabled) {
|
||||
$this->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');
|
||||
|
||||
if($limit > 500 && !$hugeMode) {
|
||||
$this->error('Max limit exceeded, use a limit lower than 500 or run again with the --huge flag');
|
||||
return;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar($limit);
|
||||
$bar->start();
|
||||
|
||||
Media::whereNot('version', '4')
|
||||
->where('created_at', '<', now()->subDays(2))
|
||||
->whereRemoteMedia(false)
|
||||
->whereNotNull(['status_id', 'profile_id'])
|
||||
->whereNull(['cdn_url', 'replicated_at'])
|
||||
->orderByDesc('size')
|
||||
->take($limit)
|
||||
->get()
|
||||
->each(function($media) use($bar) {
|
||||
if(Storage::disk('local')->exists($media->media_path)) {
|
||||
$this->totalSize = $this->totalSize + $media->size;
|
||||
try {
|
||||
MediaStorageService::store($media);
|
||||
} catch (FileNotFoundException $e) {
|
||||
$this->error('Error migrating media ' . $media->id . ' to cloud storage: ' . $e->getMessage());
|
||||
return;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
$this->error('Error migrating media ' . $media->id . ' to cloud storage: ' . $e->getMessage());
|
||||
return;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Error migrating media ' . $media->id . ' to cloud storage: ' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
$bar->advance();
|
||||
});
|
||||
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
$this->info('Finished!');
|
||||
if($this->totalSize) {
|
||||
$this->info('Uploaded ' . PrettyNumber::size($this->totalSize) . ' of media to cloud storage!');
|
||||
$this->line(' ');
|
||||
$this->info('These files are still stored locally, and will be automatically removed.');
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\User;
|
||||
use App\Models\DefaultDomainBlock;
|
||||
use App\Models\UserDomainBlock;
|
||||
use function Laravel\Prompts\text;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class DeleteUserDomainBlock extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:delete-user-domain-block';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Remove a domain block for all users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$domain = text('Enter domain you want to unblock');
|
||||
$domain = strtolower($domain);
|
||||
$domain = $this->validateDomain($domain);
|
||||
if(!$domain || empty($domain)) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
$this->processUnblocks($domain);
|
||||
return;
|
||||
}
|
||||
|
||||
protected function validateDomain($domain)
|
||||
{
|
||||
if(!strpos($domain, '.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'https://')) {
|
||||
$domain = str_replace('https://', '', $domain);
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'http://')) {
|
||||
$domain = str_replace('http://', '', $domain);
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
|
||||
|
||||
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
|
||||
if(!$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($domain === config('pixelfed.domain.app')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$confirmed = confirm('Are you sure you want to unblock ' . $domain . '?');
|
||||
if(!$confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
protected function processUnblocks($domain)
|
||||
{
|
||||
DefaultDomainBlock::whereDomain($domain)->delete();
|
||||
if(!UserDomainBlock::whereDomain($domain)->count()) {
|
||||
$this->info('No results found!');
|
||||
return;
|
||||
}
|
||||
progress(
|
||||
label: 'Updating user domain blocks...',
|
||||
steps: UserDomainBlock::whereDomain($domain)->lazyById(500),
|
||||
callback: fn ($domainBlock) => $this->performTask($domainBlock),
|
||||
);
|
||||
}
|
||||
|
||||
protected function performTask($domainBlock)
|
||||
{
|
||||
$domainBlock->deleteQuietly();
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Media;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\StatusService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class FetchMissingMediaMimeType extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:fetch-missing-media-mime-type';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
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()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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')) {
|
||||
$media->size = $res->header('content-length');
|
||||
}
|
||||
|
||||
$media->save();
|
||||
|
||||
MediaService::del($media->status_id);
|
||||
StatusService::del($media->status_id);
|
||||
$this->info('mid:'.$media->id.' ('.$res->header('content-type').':'.$res->header('content-length').' bytes)');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,36 +5,12 @@ namespace App\Console\Commands;
|
|||
use Illuminate\Console\Command;
|
||||
|
||||
use App\{
|
||||
Avatar,
|
||||
Bookmark,
|
||||
Collection,
|
||||
DirectMessage,
|
||||
FollowRequest,
|
||||
Follower,
|
||||
HashtagFollow,
|
||||
Like,
|
||||
Media,
|
||||
MediaTag,
|
||||
Mention,
|
||||
Profile,
|
||||
Report,
|
||||
ReportComment,
|
||||
ReportLog,
|
||||
StatusArchived,
|
||||
StatusHashtag,
|
||||
StatusView,
|
||||
Status,
|
||||
Story,
|
||||
StoryView,
|
||||
User,
|
||||
UserFilter
|
||||
User
|
||||
};
|
||||
use App\Models\{
|
||||
Conversation,
|
||||
Portfolio,
|
||||
UserPronoun
|
||||
};
|
||||
use DB, Cache;
|
||||
|
||||
class FixDuplicateProfiles extends Command
|
||||
{
|
||||
|
@ -69,186 +45,31 @@ class FixDuplicateProfiles extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$duplicates = DB::table('profiles')
|
||||
->whereNull('domain')
|
||||
->select('username', DB::raw('COUNT(*) as "count"'))
|
||||
->groupBy('username')
|
||||
->havingRaw('COUNT(*) > 1')
|
||||
->pluck('username');
|
||||
$profiles = Profile::selectRaw('count(user_id) as count,user_id')->whereNotNull('user_id')->groupBy('user_id')->orderBy('user_id', 'desc')->get()->where('count', '>', 1);
|
||||
$count = $profiles->count();
|
||||
if($count == 0) {
|
||||
$this->info("No duplicate profiles found!");
|
||||
return;
|
||||
}
|
||||
$this->info("Found {$count} accounts with duplicate profiles...");
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
foreach($duplicates as $dupe) {
|
||||
$ids = Profile::whereNull('domain')->whereUsername($dupe)->pluck('id');
|
||||
if(!$ids || $ids->count() != 2) {
|
||||
continue;
|
||||
foreach ($profiles as $profile) {
|
||||
$dup = Profile::whereUserId($profile->user_id)->get();
|
||||
|
||||
if(
|
||||
$dup->first()->username === $dup->last()->username &&
|
||||
$dup->last()->statuses()->count() == 0 &&
|
||||
$dup->last()->followers()->count() == 0 &&
|
||||
$dup->last()->likes()->count() == 0 &&
|
||||
$dup->last()->media()->count() == 0
|
||||
) {
|
||||
$dup->last()->avatar->forceDelete();
|
||||
$dup->last()->forceDelete();
|
||||
}
|
||||
$id = $ids->first();
|
||||
$oid = $ids->last();
|
||||
|
||||
$user = User::whereUsername($dupe)->first();
|
||||
if($user) {
|
||||
$user->profile_id = $id;
|
||||
$user->save();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->checkAvatar($id, $oid);
|
||||
$this->checkBookmarks($id, $oid);
|
||||
$this->checkCollections($id, $oid);
|
||||
$this->checkConversations($id, $oid);
|
||||
$this->checkDirectMessages($id, $oid);
|
||||
$this->checkFollowRequest($id, $oid);
|
||||
$this->checkFollowers($id, $oid);
|
||||
$this->checkHashtagFollow($id, $oid);
|
||||
$this->checkLikes($id, $oid);
|
||||
$this->checkMedia($id, $oid);
|
||||
$this->checkMediaTag($id, $oid);
|
||||
$this->checkMention($id, $oid);
|
||||
$this->checkPortfolio($id, $oid);
|
||||
$this->checkReport($id, $oid);
|
||||
$this->checkStatusArchived($id, $oid);
|
||||
$this->checkStatusHashtag($id, $oid);
|
||||
$this->checkStatusView($id, $oid);
|
||||
$this->checkStatus($id, $oid);
|
||||
$this->checkStory($id, $oid);
|
||||
$this->checkStoryView($id, $oid);
|
||||
$this->checkUserFilter($id, $oid);
|
||||
$this->checkUserPronoun($id, $oid);
|
||||
Profile::find($oid)->forceDelete();
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
Cache::clear();
|
||||
$bar->finish();
|
||||
}
|
||||
|
||||
protected function checkAvatar($id, $oid)
|
||||
{
|
||||
Avatar::whereProfileId($oid)->forceDelete();
|
||||
}
|
||||
|
||||
protected function checkBookmarks($id, $oid)
|
||||
{
|
||||
Bookmark::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkCollections($id, $oid)
|
||||
{
|
||||
Collection::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkConversations($id, $oid)
|
||||
{
|
||||
Conversation::whereToId($oid)->update(['to_id' => $id]);
|
||||
Conversation::whereFromId($oid)->update(['from_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkDirectMessages($id, $oid)
|
||||
{
|
||||
DirectMessage::whereToId($oid)->update(['to_id' => $id]);
|
||||
DirectMessage::whereFromId($oid)->update(['from_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkFollowRequest($id, $oid)
|
||||
{
|
||||
FollowRequest::whereFollowerId($oid)->update(['follower_id' => $id]);
|
||||
FollowRequest::whereFollowingId($oid)->update(['following_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkFollowers($id, $oid)
|
||||
{
|
||||
$f = Follower::whereProfileId($oid)->pluck('following_id');
|
||||
foreach($f as $fo) {
|
||||
Follower::updateOrCreate([
|
||||
'profile_id' => $id,
|
||||
'following_id' => $fo
|
||||
]);
|
||||
}
|
||||
$f = Follower::whereFollowingId($oid)->pluck('profile_id');
|
||||
foreach($f as $fo) {
|
||||
Follower::updateOrCreate([
|
||||
'profile_id' => $fo,
|
||||
'following_id' => $id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkHashtagFollow($id, $oid)
|
||||
{
|
||||
HashtagFollow::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkLikes($id, $oid)
|
||||
{
|
||||
Like::whereStatusProfileId($oid)->update(['status_profile_id' => $id]);
|
||||
Like::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkMedia($id, $oid)
|
||||
{
|
||||
Media::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkMediaTag($id, $oid)
|
||||
{
|
||||
MediaTag::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkMention($id, $oid)
|
||||
{
|
||||
Mention::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkPortfolio($id, $oid)
|
||||
{
|
||||
Portfolio::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkReport($id, $oid)
|
||||
{
|
||||
ReportComment::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
ReportLog::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
Report::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStatusArchived($id, $oid)
|
||||
{
|
||||
StatusArchived::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStatusHashtag($id, $oid)
|
||||
{
|
||||
StatusHashtag::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStatusView($id, $oid)
|
||||
{
|
||||
StatusView::whereStatusProfileId($oid)->update(['profile_id' => $id]);
|
||||
StatusView::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStatus($id, $oid)
|
||||
{
|
||||
Status::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStory($id, $oid)
|
||||
{
|
||||
Story::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStoryView($id, $oid)
|
||||
{
|
||||
StoryView::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkUserFilter($id, $oid)
|
||||
{
|
||||
UserFilter::whereUserId($oid)->update(['user_id' => $id]);
|
||||
UserFilter::whereFilterableType('App\Profile')->whereFilterableId($oid)->update(['filterable_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkUserPronoun($id, $oid)
|
||||
{
|
||||
UserPronoun::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Media;
|
||||
use League\Flysystem\MountManager;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\MediaPipeline\MediaFixLocalFilesystemCleanupPipeline;
|
||||
|
||||
class FixMediaDriver extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:fix-nonlocal-driver';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Fix filesystem when FILESYSTEM_DRIVER not set to local';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(config('filesystems.default') !== 'local') {
|
||||
$this->error('Invalid default filesystem, set FILESYSTEM_DRIVER=local to proceed');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if((bool) config_cache('pixelfed.cloud_storage') == false) {
|
||||
$this->error('Cloud storage not enabled, exiting...');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' Media Filesystem Fix');
|
||||
$this->info(' =====================');
|
||||
$this->info(' Fix media that was created when FILESYSTEM_DRIVER=local');
|
||||
$this->info(' was not properly set. This command will fix media urls');
|
||||
$this->info(' and optionally optimize/generate thumbnails when applicable,');
|
||||
$this->info(' clean up temporary local media files and clear the app cache');
|
||||
$this->info(' to fix media paths/urls.');
|
||||
$this->info(' ');
|
||||
$this->error(' Remember, FILESYSTEM_DRIVER=local must remain set or you will break things!');
|
||||
|
||||
if(!$this->confirm('Are you sure you want to perform this command?')) {
|
||||
$this->info('Exiting...');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$optimize = $this->choice(
|
||||
'Do you want to optimize media and generate thumbnails? This will store s3 locally and re-upload optimized versions.',
|
||||
['no', 'yes'],
|
||||
1
|
||||
);
|
||||
|
||||
$cloud = Storage::disk(config('filesystems.cloud'));
|
||||
$mountManager = new MountManager([
|
||||
's3' => $cloud->getDriver(),
|
||||
'local' => Storage::disk('local')->getDriver(),
|
||||
]);
|
||||
|
||||
$this->info('Fixing media, this may take a while...');
|
||||
$this->line(' ');
|
||||
$bar = $this->output->createProgressBar(Media::whereNotNull('status_id')->whereNull('cdn_url')->count());
|
||||
$bar->start();
|
||||
|
||||
foreach(Media::whereNotNull('status_id')->whereNull('cdn_url')->lazyById(20) as $media) {
|
||||
if($cloud->exists($media->media_path)) {
|
||||
if($optimize === 'yes') {
|
||||
$mountManager->copy(
|
||||
's3://' . $media->media_path,
|
||||
'local://' . $media->media_path
|
||||
);
|
||||
sleep(1);
|
||||
if(empty($media->original_sha256)) {
|
||||
$hash = \hash_file('sha256', Storage::disk('local')->path($media->media_path));
|
||||
$media->original_sha256 = $hash;
|
||||
$media->save();
|
||||
sleep(1);
|
||||
}
|
||||
if(
|
||||
$media->mime &&
|
||||
in_array($media->mime, [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp'
|
||||
])
|
||||
) {
|
||||
ImageOptimize::dispatch($media);
|
||||
sleep(3);
|
||||
}
|
||||
} else {
|
||||
$media->cdn_url = $cloud->url($media->media_path);
|
||||
$media->save();
|
||||
}
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
|
||||
$this->callSilently('cache:clear');
|
||||
|
||||
$this->info('Successfully fixed media paths and cleared cached!');
|
||||
|
||||
if($optimize === 'yes') {
|
||||
MediaFixLocalFilesystemCleanupPipeline::dispatch()->delay(now()->addMinutes(15))->onQueue('default');
|
||||
$this->line(' ');
|
||||
$this->info('A cleanup job has been dispatched to delete media stored locally, it may take a few minutes to process!');
|
||||
}
|
||||
|
||||
$this->line(' ');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
|
||||
class FixRemotePostCount extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'fix:rpc';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Fix remote accounts post count';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Profile::whereNotNull('domain')->chunk(50, function($profiles) {
|
||||
foreach($profiles as $profile) {
|
||||
$count = Status::whereNull(['in_reply_to_id', 'reblog_of_id'])->whereProfileId($profile->id)->count();
|
||||
$this->info("Checking {$profile->id} {$profile->username} - found {$count} statuses");
|
||||
$profile->status_count = $count;
|
||||
$profile->save();
|
||||
}
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ namespace App\Console\Commands;
|
|||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class FixStatusCount extends Command
|
||||
{
|
||||
|
@ -13,7 +12,7 @@ class FixStatusCount extends Command
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'fix:statuscount {--remote} {--resync} {--remote-only} {--dlog}';
|
||||
protected $signature = 'fix:statuscount';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
|
@ -39,100 +38,18 @@ class FixStatusCount extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(!$this->confirm('Are you sure you want to run the fix status command?')) {
|
||||
return;
|
||||
}
|
||||
$this->line(' ');
|
||||
$this->info('Running fix status command...');
|
||||
$now = now();
|
||||
|
||||
$nulls = ['domain', 'status', 'last_fetched_at'];
|
||||
|
||||
$resync = $this->option('resync');
|
||||
$resync24hours = false;
|
||||
|
||||
if($resync) {
|
||||
$resyncChoices = ['Only resync accounts that havent been synced in 24 hours', 'Resync all accounts'];
|
||||
$rsc = $this->choice(
|
||||
'Do you want to resync all accounts, or just accounts that havent been resynced for 24 hours?',
|
||||
$resyncChoices,
|
||||
0
|
||||
);
|
||||
$rsci = array_search($rsc, $resyncChoices);
|
||||
if($rsci === 0) {
|
||||
$resync24hours = true;
|
||||
$nulls = ['status', 'domain', 'last_fetched_at'];
|
||||
} else {
|
||||
$resync24hours = false;
|
||||
$nulls = ['status', 'domain'];
|
||||
}
|
||||
}
|
||||
|
||||
$remote = $this->option('remote');
|
||||
|
||||
if($remote) {
|
||||
$ni = array_search('domain', $nulls);
|
||||
unset($nulls[$ni]);
|
||||
$ni = array_search('last_fetched_at', $nulls);
|
||||
unset($nulls[$ni]);
|
||||
}
|
||||
|
||||
$remoteOnly = $this->option('remote-only');
|
||||
|
||||
if($remoteOnly) {
|
||||
$ni = array_search('domain', $nulls);
|
||||
unset($nulls[$ni]);
|
||||
$ni = array_search('last_fetched_at', $nulls);
|
||||
unset($nulls[$ni]);
|
||||
$nulls[] = 'user_id';
|
||||
}
|
||||
|
||||
$dlog = $this->option('dlog');
|
||||
|
||||
$nulls = array_values($nulls);
|
||||
|
||||
foreach(
|
||||
Profile::when($resync24hours, function($query, $resync24hours) use($nulls) {
|
||||
if(in_array('domain', $nulls)) {
|
||||
return $query->whereNull('domain')
|
||||
->whereNull('last_fetched_at')
|
||||
->orWhere('last_fetched_at', '<', now()->subHours(24));
|
||||
} else {
|
||||
return $query->whereNull('last_fetched_at')
|
||||
->orWhere('last_fetched_at', '<', now()->subHours(24));
|
||||
}
|
||||
})
|
||||
->when($remoteOnly, function($query, $remoteOnly) {
|
||||
return $query->whereNull('last_fetched_at')
|
||||
->orWhere('last_fetched_at', '<', now()->subHours(24));
|
||||
})
|
||||
->whereNull($nulls)
|
||||
->lazyById(50, 'id') as $profile
|
||||
) {
|
||||
$ogc = $profile->status_count;
|
||||
$upc = $profile->statuses()
|
||||
->getQuery()
|
||||
->whereIn('scope', ['public', 'private', 'unlisted'])
|
||||
->count();
|
||||
if($ogc != $upc) {
|
||||
$profile->status_count = $upc;
|
||||
$profile->last_fetched_at = $now;
|
||||
Profile::whereNull('domain')
|
||||
->chunk(50, function($profiles) {
|
||||
foreach($profiles as $profile) {
|
||||
$profile->status_count = $profile->statuses()
|
||||
->getQuery()
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->count();
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
if($dlog) {
|
||||
$this->info($profile->id . ':' . $profile->username . ' : ' . $upc);
|
||||
}
|
||||
} else {
|
||||
$profile->last_fetched_at = $now;
|
||||
$profile->save();
|
||||
if($dlog) {
|
||||
$this->info($profile->id . ':' . $profile->username . ' : ' . $upc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->line(' ');
|
||||
$this->info('Finished fix status count command!');
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,9 @@ class GenerateInstanceActor extends Command
|
|||
}
|
||||
|
||||
if(InstanceActor::exists()) {
|
||||
$this->line(' ');
|
||||
$this->error('Instance actor already exists!');
|
||||
$this->line(' ');
|
||||
$actor = InstanceActor::whereNotNull('public_key')
|
||||
->whereNotNull('private_key')
|
||||
->firstOrFail();
|
||||
|
@ -39,8 +42,7 @@ class GenerateInstanceActor extends Command
|
|||
Cache::rememberForever(InstanceActor::PKI_PRIVATE, function() use($actor) {
|
||||
return $actor->private_key;
|
||||
});
|
||||
$this->info('Instance actor succesfully generated. You do not need to run this command again.');
|
||||
return;
|
||||
exit;
|
||||
}
|
||||
|
||||
$pkiConfig = [
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Hashtag;
|
||||
use App\StatusHashtag;
|
||||
use DB;
|
||||
|
||||
class HashtagCachedCountUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:hashtag-cached-count-update {--limit=100}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update cached counter of hashtags';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$limit = $this->option('limit');
|
||||
$tags = Hashtag::whereNull('cached_count')->limit($limit)->get();
|
||||
$count = count($tags);
|
||||
if(!$count) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
foreach($tags as $tag) {
|
||||
$count = DB::table('status_hashtags')->whereHashtagId($tag->id)->count();
|
||||
if(!$count) {
|
||||
$tag->cached_count = 0;
|
||||
$tag->saveQuietly();
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
$tag->cached_count = $count;
|
||||
$tag->saveQuietly();
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Hashtag;
|
||||
use App\StatusHashtag;
|
||||
use App\Models\HashtagRelated;
|
||||
use App\Services\HashtagRelatedService;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
use function Laravel\Prompts\multiselect;
|
||||
use function Laravel\Prompts\confirm;
|
||||
|
||||
class HashtagRelatedGenerate extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:hashtag-related-generate {tag}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Prompt for missing input arguments using the returned questions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function promptForMissingArgumentsUsing()
|
||||
{
|
||||
return [
|
||||
'tag' => 'Which hashtag should we generate related tags for?',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$tag = $this->argument('tag');
|
||||
$hashtag = Hashtag::whereName($tag)->orWhere('slug', $tag)->first();
|
||||
if(!$hashtag) {
|
||||
$this->error('Hashtag not found, aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$exists = HashtagRelated::whereHashtagId($hashtag->id)->exists();
|
||||
|
||||
if($exists) {
|
||||
$confirmed = confirm('Found existing related tags, do you want to regenerate them?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Looking up #' . $tag . '...');
|
||||
|
||||
$tags = StatusHashtag::whereHashtagId($hashtag->id)->count();
|
||||
if(!$tags || $tags < 100) {
|
||||
$this->error('Not enough posts found to generate related hashtags!');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->info('Found ' . $tags . ' posts that use that hashtag');
|
||||
$related = collect(HashtagRelatedService::fetchRelatedTags($tag));
|
||||
|
||||
$selected = multiselect(
|
||||
label: 'Which tags do you want to generate?',
|
||||
options: $related->pluck('name'),
|
||||
required: true,
|
||||
);
|
||||
|
||||
$filtered = $related->filter(fn($i) => in_array($i['name'], $selected))->all();
|
||||
$agg_score = $related->filter(fn($i) => in_array($i['name'], $selected))->sum('related_count');
|
||||
|
||||
HashtagRelated::updateOrCreate([
|
||||
'hashtag_id' => $hashtag->id,
|
||||
], [
|
||||
'related_tags' => array_values($filtered),
|
||||
'agg_score' => $agg_score,
|
||||
'last_calculated_at' => now()
|
||||
]);
|
||||
|
||||
$this->info('Finished!');
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\CustomEmoji;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ImportEmojis extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'import:emojis
|
||||
{path : Path to a tar.gz archive with the emojis}
|
||||
{--prefix : Define a prefix for the emjoi shortcode}
|
||||
{--suffix : Define a suffix for the emjoi shortcode}
|
||||
{--overwrite : Overwrite existing emojis}
|
||||
{--disabled : Import all emojis as disabled}';
|
||||
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Import emojis to the database';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$path = $this->argument('path');
|
||||
|
||||
if (!file_exists($path) || !mime_content_type($path) == 'application/x-tar') {
|
||||
$this->error('Path does not exist or is not a tarfile');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
|
||||
$tar = new \PharData($path);
|
||||
$tar->decompress();
|
||||
|
||||
foreach (new \RecursiveIteratorIterator($tar) as $entry) {
|
||||
$this->line("Processing {$entry->getFilename()}");
|
||||
if (!$entry->isFile() || !$this->isImage($entry) || !$this->isEmoji($entry->getPathname())) {
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = pathinfo($entry->getFilename(), PATHINFO_FILENAME);
|
||||
$extension = pathinfo($entry->getFilename(), PATHINFO_EXTENSION);
|
||||
|
||||
// Skip macOS shadow files
|
||||
if (str_starts_with($filename, '._')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$shortcode = implode('', [
|
||||
$this->option('prefix'),
|
||||
$filename,
|
||||
$this->option('suffix'),
|
||||
]);
|
||||
|
||||
$customEmoji = CustomEmoji::whereShortcode($shortcode)->first();
|
||||
|
||||
if ($customEmoji && !$this->option('overwrite')) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$emoji = $customEmoji ?? new CustomEmoji();
|
||||
$emoji->shortcode = $shortcode;
|
||||
$emoji->domain = config('pixelfed.domain.app');
|
||||
$emoji->disabled = $this->option('disabled');
|
||||
$emoji->save();
|
||||
|
||||
$fileName = $emoji->id . '.' . $extension;
|
||||
Storage::putFileAs('public/emoji', $entry->getPathname(), $fileName);
|
||||
$emoji->media_path = 'emoji/' . $fileName;
|
||||
$emoji->save();
|
||||
$imported++;
|
||||
Cache::forget('pf:custom_emoji');
|
||||
}
|
||||
|
||||
$this->line("Imported: {$imported}");
|
||||
$this->line("Skipped: {$skipped}");
|
||||
$this->line("Failed: {$failed}");
|
||||
|
||||
//delete file
|
||||
unlink(str_replace('.tar.gz', '.tar', $path));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function isImage($file)
|
||||
{
|
||||
$image = getimagesize($file->getPathname());
|
||||
return $image !== false;
|
||||
}
|
||||
|
||||
private function isEmoji($filename)
|
||||
{
|
||||
$allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp'];
|
||||
$mimeType = mime_content_type($filename);
|
||||
|
||||
return in_array($mimeType, $allowedMimeTypes);
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\User;
|
||||
use App\Models\ImportPost;
|
||||
use App\Services\ImportService;
|
||||
|
||||
class ImportRemoveDeletedAccounts extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:import-remove-deleted-accounts';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
const CACHE_KEY = 'pf:services:import:gc-accounts:skip_min_id';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$skipMinId = Cache::remember(self::CACHE_KEY, 864000, function() {
|
||||
return 1;
|
||||
});
|
||||
|
||||
$deletedIds = User::withTrashed()
|
||||
->whereNotNull('status')
|
||||
->whereIn('status', ['deleted', 'delete'])
|
||||
->where('id', '>', $skipMinId)
|
||||
->limit(500)
|
||||
->pluck('id');
|
||||
|
||||
if(!$deletedIds || !$deletedIds->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($deletedIds as $did) {
|
||||
if(Storage::exists('imports/' . $did)) {
|
||||
Storage::deleteDirectory('imports/' . $did);
|
||||
}
|
||||
|
||||
ImportPost::where('user_id', $did)->delete();
|
||||
$skipMinId = $did;
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY, $skipMinId, 864000);
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ImportPost;
|
||||
use Storage;
|
||||
use App\Services\ImportService;
|
||||
use App\User;
|
||||
|
||||
class ImportUploadCleanStorage extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:import-upload-clean-storage';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$dirs = Storage::allDirectories('imports');
|
||||
|
||||
foreach($dirs as $dir) {
|
||||
$uid = last(explode('/', $dir));
|
||||
$skip = User::whereNull('status')->find($uid);
|
||||
if(!$skip) {
|
||||
Storage::deleteDirectory($dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ImportPost;
|
||||
use Storage;
|
||||
use App\Services\ImportService;
|
||||
|
||||
class ImportUploadGarbageCollection extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:import-upload-garbage-collection';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(!config('import.instagram.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ips = ImportPost::whereNull('status_id')->where('skip_missing_media', true)->take(100)->get();
|
||||
|
||||
if(!$ips->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($ips as $ip) {
|
||||
$pid = $ip->profile_id;
|
||||
$ip->delete();
|
||||
ImportService::getPostCount($pid, true);
|
||||
ImportService::clearAttempts($pid);
|
||||
ImportService::getImportedFiles($pid, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ImportPost;
|
||||
use App\Jobs\ImportPipeline\ImportMediaToCloudPipeline;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class ImportUploadMediaToCloudStorage extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:import-upload-media-to-cloud-storage {--limit=500}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Migrate media imported from Instagram to S3 cloud storage.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(
|
||||
(bool) config('import.instagram.storage.cloud.enabled') === false ||
|
||||
(bool) config_cache('pixelfed.cloud_storage') === false
|
||||
) {
|
||||
$this->error('Aborted. Cloud storage is not enabled for IG imports.');
|
||||
return;
|
||||
}
|
||||
|
||||
$limit = $this->option('limit');
|
||||
|
||||
$progress = progress(label: 'Migrating import media', steps: $limit);
|
||||
|
||||
$progress->start();
|
||||
|
||||
$posts = ImportPost::whereUploadedToS3(false)->take($limit)->get();
|
||||
|
||||
foreach($posts as $post) {
|
||||
ImportMediaToCloudPipeline::dispatch($post)->onQueue('low');
|
||||
$progress->advance();
|
||||
}
|
||||
|
||||
$progress->finish();
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ namespace App\Console\Commands;
|
|||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use \PDO;
|
||||
|
||||
class Installer extends Command
|
||||
{
|
||||
|
@ -13,7 +12,7 @@ class Installer extends Command
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'install {--dangerously-overwrite-env : Re-run installation and overwrite current .env }';
|
||||
protected $signature = 'install';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
|
@ -22,8 +21,6 @@ class Installer extends Command
|
|||
*/
|
||||
protected $description = 'CLI Installer';
|
||||
|
||||
public $installType = 'Simple';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
|
@ -57,161 +54,67 @@ class Installer extends Command
|
|||
$this->info(' ');
|
||||
$this->info('Pixelfed version: ' . config('pixelfed.version'));
|
||||
$this->line(' ');
|
||||
$this->installerSteps();
|
||||
$this->info('Scanning system...');
|
||||
$this->preflightCheck();
|
||||
}
|
||||
|
||||
protected function installerSteps()
|
||||
{
|
||||
$this->envCheck();
|
||||
$this->envCreate();
|
||||
$this->installType();
|
||||
|
||||
if ($this->installType === 'Advanced') {
|
||||
$this->info('Installer: Advanced...');
|
||||
$this->checkPHPRequiredDependencies();
|
||||
$this->checkFFmpegDependencies();
|
||||
$this->checkOptimiseDependencies();
|
||||
$this->checkDiskPermissions();
|
||||
$this->envProd();
|
||||
$this->instanceDB();
|
||||
$this->instanceRedis();
|
||||
$this->instanceURL();
|
||||
$this->activityPubSettings();
|
||||
$this->laravelSettings();
|
||||
$this->instanceSettings();
|
||||
$this->mediaSettings();
|
||||
$this->dbMigrations();
|
||||
$this->validateEnv();
|
||||
$this->resetArtisanCache();
|
||||
} else {
|
||||
$this->info('Installer: Simple...');
|
||||
$this->checkDiskPermissions();
|
||||
$this->envProd();
|
||||
$this->instanceDB();
|
||||
$this->instanceRedis();
|
||||
$this->instanceURL();
|
||||
$this->activityPubSettings();
|
||||
$this->instanceSettings();
|
||||
$this->dbMigrations();
|
||||
$this->validateEnv();
|
||||
$this->resetArtisanCache();
|
||||
}
|
||||
}
|
||||
|
||||
protected function envCheck()
|
||||
{
|
||||
if (file_exists(base_path('.env')) &&
|
||||
filesize(base_path('.env')) !== 0 &&
|
||||
!$this->option('dangerously-overwrite-env')
|
||||
) {
|
||||
$this->line('');
|
||||
$this->error('Existing .env File Found - Installation Aborted');
|
||||
$this->line('Run the following command to re-run the installer: php artisan install --dangerously-overwrite-env');
|
||||
$this->line('');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
protected function envCreate()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Creating .env if required');
|
||||
if (!file_exists(app()->environmentFilePath())) {
|
||||
exec('cp .env.example .env');
|
||||
}
|
||||
}
|
||||
|
||||
protected function installType()
|
||||
{
|
||||
$type = $this->choice('Select installation type', ['Simple', 'Advanced'], 1);
|
||||
$this->installType = $type;
|
||||
}
|
||||
|
||||
protected function checkPHPRequiredDependencies()
|
||||
protected function preflightCheck()
|
||||
{
|
||||
$this->line(' ');
|
||||
$this->info('Checking for Required PHP Extensions...');
|
||||
$this->info('Checking for installed dependencies...');
|
||||
$redis = Redis::connection();
|
||||
if($redis->ping()) {
|
||||
$this->info('- Found redis!');
|
||||
} else {
|
||||
$this->error('- Redis not found, aborting installation');
|
||||
exit;
|
||||
}
|
||||
$this->checkPhpDependencies();
|
||||
$this->checkPermissions();
|
||||
$this->envCheck();
|
||||
}
|
||||
|
||||
protected function checkPhpDependencies()
|
||||
{
|
||||
$extensions = [
|
||||
'bcmath',
|
||||
'ctype',
|
||||
'curl',
|
||||
'json',
|
||||
'mbstring',
|
||||
'openssl',
|
||||
'gd',
|
||||
'intl',
|
||||
'xml',
|
||||
'zip',
|
||||
'redis',
|
||||
'openssl'
|
||||
];
|
||||
|
||||
foreach ($extensions as $ext) {
|
||||
if (extension_loaded($ext) == false) {
|
||||
$this->error("- \"{$ext}\" not found");
|
||||
} else {
|
||||
$this->info("- \"{$ext}\" found");
|
||||
}
|
||||
}
|
||||
|
||||
$continue = $this->choice('Do you wish to continue?', ['yes', 'no'], 0);
|
||||
$this->continue = $continue;
|
||||
if ($this->continue === 'no') {
|
||||
$this->info('Exiting Installer.');
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function checkFFmpegDependencies()
|
||||
{
|
||||
$this->line(' ');
|
||||
$this->info('Checking for Required FFmpeg dependencies...');
|
||||
|
||||
$ffmpeg = exec('which ffmpeg');
|
||||
if (empty($ffmpeg)) {
|
||||
$this->error("- \"{$ext}\" FFmpeg not found, aborting installation");
|
||||
if(empty($ffmpeg)) {
|
||||
$this->error('FFmpeg not found, please install it.');
|
||||
$this->error('Cancelling installation.');
|
||||
exit;
|
||||
} else {
|
||||
$this->info('- Found FFmpeg!');
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkOptimiseDependencies()
|
||||
{
|
||||
$this->line(' ');
|
||||
$this->info('Checking for Optional Media Optimisation dependencies...');
|
||||
|
||||
$dependencies = [
|
||||
'jpegoptim',
|
||||
'optipng',
|
||||
'pngquant',
|
||||
'gifsicle',
|
||||
];
|
||||
|
||||
foreach ($dependencies as $dep) {
|
||||
$which = exec("which $dep");
|
||||
if (empty($which)) {
|
||||
$this->error("- \"{$dep}\" not found");
|
||||
$this->line('');
|
||||
$this->info('Checking for required php extensions...');
|
||||
foreach($extensions as $ext) {
|
||||
if(extension_loaded($ext) == false) {
|
||||
$this->error("- {$ext} extension not found, aborting installation");
|
||||
exit;
|
||||
} else {
|
||||
$this->info("- \"{$dep}\" found");
|
||||
}
|
||||
}
|
||||
$this->info("- Required PHP extensions found!");
|
||||
}
|
||||
|
||||
protected function checkDiskPermissions()
|
||||
protected function checkPermissions()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Checking for proper filesystem permissions...');
|
||||
$this->callSilently('storage:link');
|
||||
|
||||
$paths = [
|
||||
base_path('bootstrap'),
|
||||
base_path('storage'),
|
||||
base_path('storage')
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (is_writeable($path) == false) {
|
||||
foreach($paths as $path) {
|
||||
if(is_writeable($path) == false) {
|
||||
$this->error("- Invalid permission found! Aborting installation.");
|
||||
$this->error(" Please make the following path writeable by the web server:");
|
||||
$this->error(" $path");
|
||||
|
@ -222,244 +125,95 @@ class Installer extends Command
|
|||
}
|
||||
}
|
||||
|
||||
protected function envProd()
|
||||
protected function envCheck()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Enabling production');
|
||||
|
||||
$this->updateEnvFile('APP_ENV', 'production');
|
||||
$this->updateEnvFile('APP_DEBUG', 'false');
|
||||
$this->call('key:generate', ['--force' => true]);
|
||||
}
|
||||
|
||||
protected function instanceDB()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Database Settings:');
|
||||
$database = $this->choice('Select database driver', ['mysql', 'pgsql'], 0);
|
||||
$database_host = $this->ask('Select database host', '127.0.0.1');
|
||||
$database_port_default = $database === 'mysql' ? 3306 : 5432;
|
||||
$database_port = $this->ask('Select database port', $database_port_default);
|
||||
|
||||
$database_db = $this->ask('Select database', 'pixelfed');
|
||||
$database_username = $this->ask('Select database username', 'pixelfed');
|
||||
$database_password = $this->secret('Select database password');
|
||||
|
||||
$this->updateEnvFile('DB_CONNECTION', $database);
|
||||
$this->updateEnvFile('DB_HOST', $database_host);
|
||||
$this->updateEnvFile('DB_PORT', $database_port);
|
||||
$this->updateEnvFile('DB_DATABASE', $database_db);
|
||||
$this->updateEnvFile('DB_USERNAME', $database_username);
|
||||
$this->updateEnvFile('DB_PASSWORD', $database_password);
|
||||
|
||||
$this->info('Testing Database...');
|
||||
$dsn = "{$database}:dbname={$database_db};host={$database_host};port={$database_port};";
|
||||
try {
|
||||
$dbh = new PDO($dsn, $database_username, $database_password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
} catch (\PDOException $e) {
|
||||
$this->error('Cannot connect to database, check your details and try again');
|
||||
exit;
|
||||
}
|
||||
$this->info('- Connected to DB Successfully');
|
||||
}
|
||||
|
||||
protected function instanceRedis()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Redis Settings:');
|
||||
$redis_client = $this->choice('Set redis client (PHP extension)', ['phpredis', 'predis'], 0);
|
||||
$redis_host = $this->ask('Set redis host', 'localhost');
|
||||
$redis_password = $this->ask('Set redis password', 'null');
|
||||
$redis_port = $this->ask('Set redis port', 6379);
|
||||
|
||||
$this->updateEnvFile('REDIS_CLIENT', $redis_client);
|
||||
$this->updateEnvFile('REDIS_SCHEME', 'tcp');
|
||||
$this->updateEnvFile('REDIS_HOST', $redis_host);
|
||||
$this->updateEnvFile('REDIS_PASSWORD', $redis_password);
|
||||
$this->updateEnvFile('REDIS_PORT', $redis_port);
|
||||
|
||||
$this->info('Testing Redis...');
|
||||
$redis = Redis::connection();
|
||||
if ($redis->ping()) {
|
||||
$this->info('- Connected to Redis Successfully!');
|
||||
if(!file_exists(base_path('.env')) || filesize(base_path('.env')) == 0) {
|
||||
$this->line('');
|
||||
$this->info('No .env configuration file found. We will create one now!');
|
||||
$this->createEnv();
|
||||
} else {
|
||||
$this->error('Cannot connect to Redis, check your details and try again');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
protected function instanceURL()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Instance URL Settings:');
|
||||
$name = $this->ask('Site name [ex: Pixelfed]', 'Pixelfed');
|
||||
|
||||
$domain = $this->ask('Site Domain [ex: pixelfed.com]');
|
||||
$domain = strtolower($domain);
|
||||
if (empty($domain)) {
|
||||
$this->error('You must set the site domain');
|
||||
exit;
|
||||
}
|
||||
if (starts_with($domain, 'http')) {
|
||||
$this->error('The site domain cannot start with https://, you must use the FQDN (eg: example.org)');
|
||||
exit;
|
||||
}
|
||||
if (strpos($domain, '.') == false) {
|
||||
$this->error('You must enter a valid site domain');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->updateEnvFile('APP_NAME', $name);
|
||||
$this->updateEnvFile('APP_URL', 'https://' . $domain);
|
||||
$this->updateEnvFile('APP_DOMAIN', $domain);
|
||||
$this->updateEnvFile('ADMIN_DOMAIN', $domain);
|
||||
$this->updateEnvFile('SESSION_DOMAIN', $domain);
|
||||
}
|
||||
|
||||
protected function laravelSettings()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Laravel Settings (Defaults are recommended):');
|
||||
$session = $this->choice('Select session driver', ["database", "file", "cookie", "redis", "memcached", "array"], 0);
|
||||
$cache = $this->choice('Select cache driver', ["redis", "apc", "array", "database", "file", "memcached"], 0);
|
||||
$queue = $this->choice('Select queue driver', ["redis", "database", "sync", "beanstalkd", "sqs", "null"], 0);
|
||||
$broadcast = $this->choice('Select broadcast driver', ["log", "redis", "pusher", "null"], 0);
|
||||
$log = $this->choice('Select Log Channel', ["stack", "single", "daily", "stderr", "syslog", "null"], 0);
|
||||
$horizon = $this->ask('Set Horizon Prefix [ex: horizon-]', 'horizon-');
|
||||
|
||||
$this->updateEnvFile('SESSION_DRIVER', $session);
|
||||
$this->updateEnvFile('CACHE_DRIVER', $cache);
|
||||
$this->updateEnvFile('QUEUE_DRIVER', $queue);
|
||||
$this->updateEnvFile('BROADCAST_DRIVER', $broadcast);
|
||||
$this->updateEnvFile('LOG_CHANNEL', $log);
|
||||
$this->updateEnvFile('HORIZON_PREFIX', $horizon);
|
||||
}
|
||||
|
||||
protected function instanceSettings()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Instance Settings:');
|
||||
$max_registration = $this->ask('Set Maximum users on this instance.', '1000');
|
||||
$open_registration = $this->choice('Allow new registrations?', ['false', 'true'], 0);
|
||||
$enforce_email_verification = $this->choice('Enforce email verification?', ['false', 'true'], 0);
|
||||
$enable_mobile_apis = $this->choice('Enable mobile app/apis support?', ['false', 'true'], 1);
|
||||
|
||||
$this->updateEnvFile('PF_MAX_USERS', $max_registration);
|
||||
$this->updateEnvFile('OPEN_REGISTRATION', $open_registration);
|
||||
$this->updateEnvFile('ENFORCE_EMAIL_VERIFICATION', $enforce_email_verification);
|
||||
$this->updateEnvFile('OAUTH_ENABLED', $enable_mobile_apis);
|
||||
$this->updateEnvFile('EXP_EMC', $enable_mobile_apis);
|
||||
}
|
||||
|
||||
protected function activityPubSettings()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Federation Settings:');
|
||||
$activitypub_federation = $this->choice('Enable ActivityPub federation?', ['false', 'true'], 1);
|
||||
|
||||
$this->updateEnvFile('ACTIVITY_PUB', $activitypub_federation);
|
||||
$this->updateEnvFile('AP_REMOTE_FOLLOW', $activitypub_federation);
|
||||
$this->updateEnvFile('AP_INBOX', $activitypub_federation);
|
||||
$this->updateEnvFile('AP_OUTBOX', $activitypub_federation);
|
||||
$this->updateEnvFile('AP_SHAREDINBOX', $activitypub_federation);
|
||||
}
|
||||
|
||||
protected function mediaSettings()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Media Settings:');
|
||||
$optimize_media = $this->choice('Optimize media uploads? Requires jpegoptim and other dependencies!', ['false', 'true'], 1);
|
||||
$image_quality = $this->ask('Set image optimization quality between 1-100. Default is 80%, lower values use less disk space at the expense of image quality.', '80');
|
||||
if ($image_quality < 1) {
|
||||
$this->error('Min image quality is 1. You should avoid such a low value, 60 at minimum is recommended.');
|
||||
exit;
|
||||
}
|
||||
if ($image_quality > 100) {
|
||||
$this->error('Max image quality is 100');
|
||||
exit;
|
||||
}
|
||||
$this->info('Note: Max photo size cannot exceed `post_max_size` in php.ini.');
|
||||
$max_photo_size = $this->ask('Max photo upload size in kilobytes. Default 15000 which is equal to 15MB', '15000');
|
||||
|
||||
$max_caption_length = $this->ask('Max caption limit. Default to 500, max 5000.', '500');
|
||||
if ($max_caption_length > 5000) {
|
||||
$this->error('Max caption length is 5000 characters.');
|
||||
exit;
|
||||
}
|
||||
|
||||
$max_album_length = $this->ask('Max photos allowed per album. Choose a value between 1 and 10.', '4');
|
||||
if ($max_album_length < 1) {
|
||||
$this->error('Min album length is 1 photos per album.');
|
||||
exit;
|
||||
}
|
||||
if ($max_album_length > 10) {
|
||||
$this->error('Max album length is 10 photos per album.');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->updateEnvFile('PF_OPTIMIZE_IMAGES', $optimize_media);
|
||||
$this->updateEnvFile('IMAGE_QUALITY', $image_quality);
|
||||
$this->updateEnvFile('MAX_PHOTO_SIZE', $max_photo_size);
|
||||
$this->updateEnvFile('MAX_CAPTION_LENGTH', $max_caption_length);
|
||||
$this->updateEnvFile('MAX_ALBUM_LENGTH', $max_album_length);
|
||||
}
|
||||
|
||||
protected function dbMigrations()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Note: We recommend running database migrations now!');
|
||||
$confirm = $this->choice('Do you want to run the database migrations?', ['Yes', 'No'], 0);
|
||||
|
||||
if ($confirm === 'Yes') {
|
||||
sleep(3);
|
||||
$this->call('config:cache');
|
||||
$this->line('');
|
||||
$this->info('Migrating DB:');
|
||||
$this->call('migrate', ['--force' => true]);
|
||||
$this->line('');
|
||||
$this->info('Importing Cities:');
|
||||
$this->call('import:cities');
|
||||
$this->line('');
|
||||
$this->info('Creating Federation Instance Actor:');
|
||||
$this->call('instance:actor');
|
||||
$this->line('');
|
||||
$this->info('Creating Password Keys for API:');
|
||||
$this->call('passport:keys', ['--force' => true]);
|
||||
|
||||
$confirm = $this->choice('Do you want to create an admin account?', ['Yes', 'No'], 0);
|
||||
if ($confirm === 'Yes') {
|
||||
$this->call('user:create');
|
||||
$confirm = $this->confirm('Found .env file, do you want to overwrite it?');
|
||||
if(!$confirm) {
|
||||
$this->info('Cancelling installation.');
|
||||
exit;
|
||||
}
|
||||
$confirm = $this->confirm('Are you really sure you want to overwrite it?');
|
||||
if(!$confirm) {
|
||||
$this->info('Cancelling installation.');
|
||||
exit;
|
||||
}
|
||||
$this->error('Warning ... if you did not backup your .env before its overwritten it will be permanently deleted.');
|
||||
$confirm = $this->confirm('The application may be installed already, are you really sure you want to overwrite it?');
|
||||
if(!$confirm) {
|
||||
$this->info('Cancelling installation.');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$this->postInstall();
|
||||
}
|
||||
|
||||
protected function resetArtisanCache()
|
||||
protected function createEnv()
|
||||
{
|
||||
$this->call('config:cache');
|
||||
$this->call('route:cache');
|
||||
$this->call('view:cache');
|
||||
}
|
||||
|
||||
protected function validateEnv()
|
||||
{
|
||||
$this->checkEnvKeys('APP_KEY', "key:generate failed?");
|
||||
$this->checkEnvKeys('APP_ENV', "APP_ENV value should be production");
|
||||
$this->checkEnvKeys('APP_DEBUG', "APP_DEBUG value should be false");
|
||||
}
|
||||
|
||||
#####
|
||||
# Installer Functions
|
||||
#####
|
||||
|
||||
protected function checkEnvKeys($key, $error)
|
||||
{
|
||||
$envPath = app()->environmentFilePath();
|
||||
$payload = file_get_contents($envPath);
|
||||
|
||||
if ($existing = $this->existingEnv($key, $payload)) {
|
||||
} else {
|
||||
$this->error("$key empty - $error");
|
||||
$this->line('');
|
||||
// copy env
|
||||
if(!file_exists(app()->environmentFilePath())) {
|
||||
exec('cp .env.example .env');
|
||||
$this->call('key:generate');
|
||||
}
|
||||
|
||||
$name = $this->ask('Site name [ex: Pixelfed]');
|
||||
$this->updateEnvFile('APP_NAME', $name ?? 'pixelfed');
|
||||
|
||||
$domain = $this->ask('Site Domain [ex: pixelfed.com]');
|
||||
$this->updateEnvFile('APP_DOMAIN', $domain ?? 'example.org');
|
||||
$this->updateEnvFile('ADMIN_DOMAIN', $domain ?? 'example.org');
|
||||
$this->updateEnvFile('SESSION_DOMAIN', $domain ?? 'example.org');
|
||||
$this->updateEnvFile('APP_URL', 'https://' . $domain ?? 'https://example.org');
|
||||
|
||||
$database = $this->choice('Select database driver', ['mysql', 'pgsql'], 0);
|
||||
$this->updateEnvFile('DB_CONNECTION', $database ?? 'mysql');
|
||||
switch ($database) {
|
||||
case 'mysql':
|
||||
$database_host = $this->ask('Select database host', '127.0.0.1');
|
||||
$this->updateEnvFile('DB_HOST', $database_host ?? 'mysql');
|
||||
|
||||
$database_port = $this->ask('Select database port', 3306);
|
||||
$this->updateEnvFile('DB_PORT', $database_port ?? 3306);
|
||||
|
||||
$database_db = $this->ask('Select database', 'pixelfed');
|
||||
$this->updateEnvFile('DB_DATABASE', $database_db ?? 'pixelfed');
|
||||
|
||||
$database_username = $this->ask('Select database username', 'pixelfed');
|
||||
$this->updateEnvFile('DB_USERNAME', $database_username ?? 'pixelfed');
|
||||
|
||||
$db_pass = str_random(64);
|
||||
$database_password = $this->secret('Select database password', $db_pass);
|
||||
$this->updateEnvFile('DB_PASSWORD', $database_password);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
$cache = $this->choice('Select cache driver', ["redis", "apc", "array", "database", "file", "memcached"], 0);
|
||||
$this->updateEnvFile('CACHE_DRIVER', $cache ?? 'redis');
|
||||
|
||||
$session = $this->choice('Select session driver', ["redis", "file", "cookie", "database", "apc", "memcached", "array"], 0);
|
||||
$this->updateEnvFile('SESSION_DRIVER', $session ?? 'redis');
|
||||
|
||||
$redis_host = $this->ask('Set redis host', 'localhost');
|
||||
$this->updateEnvFile('REDIS_HOST', $redis_host);
|
||||
|
||||
$redis_password = $this->ask('Set redis password', 'null');
|
||||
$this->updateEnvFile('REDIS_PASSWORD', $redis_password);
|
||||
|
||||
$redis_port = $this->ask('Set redis port', 6379);
|
||||
$this->updateEnvFile('REDIS_PORT', $redis_port);
|
||||
|
||||
$open_registration = $this->choice('Allow new registrations?', ['true', 'false'], 1);
|
||||
$this->updateEnvFile('OPEN_REGISTRATION', $open_registration);
|
||||
|
||||
$enforce_email_verification = $this->choice('Enforce email verification?', ['true', 'false'], 0);
|
||||
$this->updateEnvFile('ENFORCE_EMAIL_VERIFICATION', $enforce_email_verification);
|
||||
|
||||
}
|
||||
|
||||
protected function updateEnvFile($key, $value)
|
||||
|
@ -492,14 +246,10 @@ class Installer extends Command
|
|||
fclose($file);
|
||||
}
|
||||
|
||||
protected function parseSize($size)
|
||||
protected function postInstall()
|
||||
{
|
||||
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
|
||||
$size = preg_replace('/[^0-9\.]/', '', $size);
|
||||
if ($unit) {
|
||||
return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
|
||||
} else {
|
||||
return round($size);
|
||||
}
|
||||
$this->callSilent('config:cache');
|
||||
//$this->callSilent('route:cache');
|
||||
$this->info('Pixelfed has been successfully installed!');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,298 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Services\InstanceService;
|
||||
use App\Jobs\InstancePipeline\FetchNodeinfoPipeline;
|
||||
use function Laravel\Prompts\select;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
use function Laravel\Prompts\search;
|
||||
use function Laravel\Prompts\table;
|
||||
|
||||
class InstanceManager extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:instance-manager';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Manage Instances';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$action = select(
|
||||
'What action do you want to perform?',
|
||||
[
|
||||
'Recalculate Stats',
|
||||
'Ban Instance',
|
||||
'Unlist Instance',
|
||||
'Unlisted Instances',
|
||||
'Banned Instances',
|
||||
'Unban Instance',
|
||||
'Relist Instance',
|
||||
],
|
||||
);
|
||||
|
||||
switch($action) {
|
||||
case 'Recalculate Stats':
|
||||
return $this->recalculateStats();
|
||||
break;
|
||||
|
||||
case 'Unlisted Instances':
|
||||
return $this->viewUnlistedInstances();
|
||||
break;
|
||||
|
||||
case 'Banned Instances':
|
||||
return $this->viewBannedInstances();
|
||||
break;
|
||||
|
||||
case 'Unlist Instance':
|
||||
return $this->unlistInstance();
|
||||
break;
|
||||
|
||||
case 'Ban Instance':
|
||||
return $this->banInstance();
|
||||
break;
|
||||
|
||||
case 'Unban Instance':
|
||||
return $this->unbanInstance();
|
||||
break;
|
||||
|
||||
case 'Relist Instance':
|
||||
return $this->relistInstance();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function recalculateStats()
|
||||
{
|
||||
$instanceCount = Instance::count();
|
||||
$confirmed = confirm('Do you want to recalculate stats for all ' . $instanceCount . ' instances?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$users = progress(
|
||||
label: 'Updating instance stats...',
|
||||
steps: Instance::all(),
|
||||
callback: fn ($instance) => $this->updateInstanceStats($instance),
|
||||
);
|
||||
}
|
||||
|
||||
protected function updateInstanceStats($instance)
|
||||
{
|
||||
FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg');
|
||||
}
|
||||
|
||||
protected function unlistInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereUnlisted(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to unlist this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance unlisting');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->unlisted = true;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully unlisted ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function relistInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereUnlisted(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to re-list this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance re-listing');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->unlisted = false;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully re-listed ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function banInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereBanned(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to ban this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance ban');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->banned = true;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully banned ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function unbanInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereBanned(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to unban this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance unban');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->banned = false;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully un-banned ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function viewBannedInstances()
|
||||
{
|
||||
$data = Instance::whereBanned(true)
|
||||
->get(['domain', 'user_count', 'status_count'])
|
||||
->map(function($d) {
|
||||
return [
|
||||
'domain' => $d->domain,
|
||||
'user_count' => number_format($d->user_count),
|
||||
'status_count' => number_format($d->status_count),
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
table(
|
||||
['Domain', 'User Count', 'Status Count'],
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
protected function viewUnlistedInstances()
|
||||
{
|
||||
$data = Instance::whereUnlisted(true)
|
||||
->get(['domain', 'user_count', 'status_count', 'banned'])
|
||||
->map(function($d) {
|
||||
return [
|
||||
'domain' => $d->domain,
|
||||
'user_count' => number_format($d->user_count),
|
||||
'status_count' => number_format($d->status_count),
|
||||
'banned' => $d->banned ? '✅' : null
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
table(
|
||||
['Domain', 'User Count', 'Status Count', 'Banned'],
|
||||
$data
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Media;
|
||||
use Cache, Storage;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
|
||||
class MediaCloudUrlRewrite extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:cloud-url-rewrite {oldDomain} {newDomain}';
|
||||
|
||||
/**
|
||||
* Prompt for missing input arguments using the returned questions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function promptForMissingArgumentsUsing()
|
||||
{
|
||||
return [
|
||||
'oldDomain' => 'The old S3 domain',
|
||||
'newDomain' => 'The new S3 domain'
|
||||
];
|
||||
}
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Rewrite S3 media urls from local users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->preflightCheck();
|
||||
$this->bootMessage();
|
||||
$this->confirmCloudUrl();
|
||||
}
|
||||
|
||||
protected function preflightCheck()
|
||||
{
|
||||
if(!(bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$this->info('Error: Cloud storage is not enabled!');
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
protected function bootMessage()
|
||||
{
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' Media Cloud Url Rewrite Tool');
|
||||
$this->info(' ===');
|
||||
$this->info(' Old S3: ' . trim($this->argument('oldDomain')));
|
||||
$this->info(' New S3: ' . trim($this->argument('newDomain')));
|
||||
$this->info(' ');
|
||||
}
|
||||
|
||||
protected function confirmCloudUrl()
|
||||
{
|
||||
$disk = Storage::disk(config('filesystems.cloud'))->url('test');
|
||||
$domain = parse_url($disk, PHP_URL_HOST);
|
||||
if(trim($this->argument('newDomain')) !== $domain) {
|
||||
$this->error('Error: The new S3 domain you entered is not currently configured');
|
||||
exit;
|
||||
}
|
||||
|
||||
if(!$this->confirm('Confirm this is correct')) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->updateUrls();
|
||||
}
|
||||
|
||||
protected function updateUrls()
|
||||
{
|
||||
$this->info('Updating urls...');
|
||||
$oldDomain = trim($this->argument('oldDomain'));
|
||||
$newDomain = trim($this->argument('newDomain'));
|
||||
$disk = Storage::disk(config('filesystems.cloud'));
|
||||
$count = Media::whereNotNull('cdn_url')->count();
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$counter = 0;
|
||||
$bar->start();
|
||||
foreach(Media::whereNotNull('cdn_url')->lazyById(1000, 'id') as $media) {
|
||||
if(strncmp($media->media_path, 'http', 4) === 0) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
$cdnHost = parse_url($media->cdn_url, PHP_URL_HOST);
|
||||
if($oldDomain != $cdnHost || $newDomain == $cdnHost) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
$media->cdn_url = str_replace($oldDomain, $newDomain, $media->cdn_url);
|
||||
|
||||
if($media->thumbnail_url != null) {
|
||||
$thumbHost = parse_url($media->thumbnail_url, PHP_URL_HOST);
|
||||
if($thumbHost == $oldDomain) {
|
||||
$thumbUrl = $disk->url($media->thumbnail_path);
|
||||
$media->thumbnail_url = $thumbUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if($media->optimized_url != null) {
|
||||
$optiHost = parse_url($media->optimized_url, PHP_URL_HOST);
|
||||
if($optiHost == $oldDomain) {
|
||||
$optiUrl = str_replace($oldDomain, $newDomain, $media->optimized_url);
|
||||
$media->optimized_url = $optiUrl;
|
||||
}
|
||||
}
|
||||
|
||||
$media->save();
|
||||
$counter++;
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
|
||||
$this->line(' ');
|
||||
$this->info('Finished! Updated ' . $counter . ' total records!');
|
||||
$this->line(' ');
|
||||
$this->info('Tip: Run `php artisan cache:clear` to purge cached urls');
|
||||
}
|
||||
}
|
|
@ -4,55 +4,63 @@ namespace App\Console\Commands;
|
|||
|
||||
use Illuminate\Console\Command;
|
||||
use App\{Media, Status};
|
||||
use App\Services\MediaStorageService;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class MediaGarbageCollector extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:gc';
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:gc';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete media uploads not attached to any active statuses';
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete media uploads not attached to any active statuses';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$limit = 500;
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$limit = 20000;
|
||||
|
||||
$gc = Media::doesntHave('status')
|
||||
->where('created_at', '<', Carbon::now()->subHours(1)->toDateTimeString())
|
||||
->orderBy('created_at','asc')
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
$gc = Media::whereNull('status_id')
|
||||
->where('created_at', '<', now()->subHours(2)->toDateTimeString())
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
$bar = $this->output->createProgressBar($gc->count());
|
||||
$bar->start();
|
||||
foreach($gc as $media) {
|
||||
MediaStorageService::delete($media, true);
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->line('');
|
||||
}
|
||||
$bar = $this->output->createProgressBar($gc->count());
|
||||
$bar->start();
|
||||
foreach($gc as $media) {
|
||||
$path = storage_path("app/$media->media_path");
|
||||
$thumb = storage_path("app/$media->thumbnail_path");
|
||||
if(is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
if(is_file($thumb)) {
|
||||
unlink($thumb);
|
||||
}
|
||||
$media->forceDelete();
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Media;
|
||||
use App\Status;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\StatusService;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class MediaS3GarbageCollector extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:s3gc {--limit=200} {--huge} {--log-errors}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete (local) media uploads that exist on S3';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$enabled = (bool) config_cache('pixelfed.cloud_storage');
|
||||
if(!$enabled) {
|
||||
$this->error('Cloud storage not enabled. Exiting...');
|
||||
return;
|
||||
}
|
||||
|
||||
$deleteEnabled = config('media.delete_local_after_cloud');
|
||||
if(!$deleteEnabled) {
|
||||
$this->error('Delete local storage after cloud upload is not enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
$limit = $this->option('limit');
|
||||
$hugeMode = $this->option('huge');
|
||||
$log = $this->option('log-errors');
|
||||
|
||||
if($limit > 2000 && !$hugeMode) {
|
||||
$this->error('Limit exceeded, please use a limit under 2000 or run again with the --huge flag');
|
||||
return;
|
||||
}
|
||||
|
||||
$minId = Media::orderByDesc('id')->where('created_at', '<', now()->subHours(12))->first();
|
||||
|
||||
if(!$minId) {
|
||||
return;
|
||||
} else {
|
||||
$minId = $minId->id;
|
||||
}
|
||||
|
||||
return $hugeMode ?
|
||||
$this->hugeMode($minId, $limit, $log) :
|
||||
$this->regularMode($minId, $limit, $log);
|
||||
}
|
||||
|
||||
protected function regularMode($minId, $limit, $log)
|
||||
{
|
||||
$gc = Media::whereRemoteMedia(false)
|
||||
->whereNotNull(['status_id', 'cdn_url', 'replicated_at'])
|
||||
->whereNot('version', '4')
|
||||
->where('id', '<', $minId)
|
||||
->inRandomOrder()
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
$totalSize = 0;
|
||||
$bar = $this->output->createProgressBar($gc->count());
|
||||
$bar->start();
|
||||
$cloudDisk = Storage::disk(config('filesystems.cloud'));
|
||||
$localDisk = Storage::disk('local');
|
||||
|
||||
foreach($gc as $media) {
|
||||
try {
|
||||
if(
|
||||
$cloudDisk->exists($media->media_path)
|
||||
) {
|
||||
if( $localDisk->exists($media->media_path)) {
|
||||
$localDisk->delete($media->media_path);
|
||||
$media->version = 4;
|
||||
$media->save();
|
||||
$totalSize = $totalSize + $media->size;
|
||||
MediaService::del($media->status_id);
|
||||
StatusService::del($media->status_id, false);
|
||||
if($localDisk->exists($media->thumbnail_path)) {
|
||||
$localDisk->delete($media->thumbnail_path);
|
||||
}
|
||||
} else {
|
||||
$media->version = 4;
|
||||
$media->save();
|
||||
}
|
||||
} else {
|
||||
if($log) {
|
||||
Log::channel('media')->info('[GC] Local media not properly persisted to cloud storage', ['media_id' => $media->id]);
|
||||
}
|
||||
}
|
||||
$bar->advance();
|
||||
} catch (FileNotFoundException $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
} catch (\Exception $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
$this->info('Finished!');
|
||||
if($totalSize) {
|
||||
$this->info('Cleared ' . $totalSize . ' bytes of media from local disk!');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function hugeMode($minId, $limit, $log)
|
||||
{
|
||||
$cloudDisk = Storage::disk(config('filesystems.cloud'));
|
||||
$localDisk = Storage::disk('local');
|
||||
|
||||
$bar = $this->output->createProgressBar($limit);
|
||||
$bar->start();
|
||||
|
||||
Media::whereRemoteMedia(false)
|
||||
->whereNotNull(['status_id', 'cdn_url', 'replicated_at'])
|
||||
->whereNot('version', '4')
|
||||
->where('id', '<', $minId)
|
||||
->chunk(50, function($medias) use($cloudDisk, $localDisk, $bar, $log) {
|
||||
foreach($medias as $media) {
|
||||
try {
|
||||
if($cloudDisk->exists($media->media_path)) {
|
||||
if( $localDisk->exists($media->media_path)) {
|
||||
$localDisk->delete($media->media_path);
|
||||
$media->version = 4;
|
||||
$media->save();
|
||||
MediaService::del($media->status_id);
|
||||
StatusService::del($media->status_id, false);
|
||||
if($localDisk->exists($media->thumbnail_path)) {
|
||||
$localDisk->delete($media->thumbnail_path);
|
||||
}
|
||||
} else {
|
||||
$media->version = 4;
|
||||
$media->save();
|
||||
}
|
||||
} else {
|
||||
if($log) {
|
||||
Log::channel('media')->info('[GC] Local media not properly persisted to cloud storage', ['media_id' => $media->id]);
|
||||
}
|
||||
}
|
||||
$bar->advance();
|
||||
} catch (FileNotFoundException $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
} catch (\Exception $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
$this->info('Finished!');
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline;
|
||||
|
||||
class NotificationEpochUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:notification-epoch-update';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update notification epoch';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
NotificationEpochUpdatePipeline::dispatch();
|
||||
}
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Storage;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use App\Instance;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class SendUpdateActor extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ap:update-actors {--force}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Send Update Actor activities to known remote servers to force updates';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$totalUserCount = Profile::whereNotNull('user_id')->count();
|
||||
$totalInstanceCount = Instance::count();
|
||||
$this->info('Found ' . $totalUserCount . ' local accounts and ' . $totalInstanceCount . ' remote instances');
|
||||
|
||||
$task = $this->choice(
|
||||
'What do you want to do?',
|
||||
[
|
||||
'View top instances',
|
||||
'Send updates to an instance'
|
||||
],
|
||||
0
|
||||
);
|
||||
|
||||
if($task === 'View top instances') {
|
||||
$this->table(
|
||||
['domain', 'user_count', 'last_synced'],
|
||||
Instance::orderByDesc('user_count')->take(20)->get(['domain', 'user_count', 'actors_last_synced_at'])->toArray()
|
||||
);
|
||||
return Command::SUCCESS;
|
||||
} else {
|
||||
$domain = $this->anticipate('Enter the instance domain', function ($input) {
|
||||
return Instance::where('domain', 'like', '%' . $input . '%')->pluck('domain')->toArray();
|
||||
});
|
||||
if(!$this->confirm('Are you sure you want to send actor updates to ' . $domain . '?')) {
|
||||
return;
|
||||
}
|
||||
if($cur = Instance::whereDomain($domain)->whereNotNull('actors_last_synced_at')->first()) {
|
||||
if(!$this->option('force')) {
|
||||
$this->error('ERROR: Cannot re-sync this instance, it was already synced on ' . $cur->actors_last_synced_at);
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->touchStorageCache($domain);
|
||||
$this->line(' ');
|
||||
$this->error('Keep this window open during this process or it will not complete!');
|
||||
$sharedInbox = Profile::whereDomain($domain)->whereNotNull('sharedInbox')->first();
|
||||
if(!$sharedInbox) {
|
||||
$this->error('ERROR: Cannot find the sharedInbox of ' . $domain);
|
||||
return;
|
||||
}
|
||||
$url = $sharedInbox->sharedInbox;
|
||||
$this->line(' ');
|
||||
$this->info('Found sharedInbox: ' . $url);
|
||||
$bar = $this->output->createProgressBar($totalUserCount);
|
||||
$bar->start();
|
||||
|
||||
$startCache = $this->getStorageCache($domain);
|
||||
User::whereNull('status')->when($startCache, function($query, $startCache) use($bar) {
|
||||
$bar->advance($startCache);
|
||||
return $query->where('id', '>', $startCache);
|
||||
})->chunk(50, function($users) use($bar, $url, $domain) {
|
||||
foreach($users as $user) {
|
||||
$this->updateStorageCache($domain, $user->id);
|
||||
$profile = Profile::find($user->profile_id);
|
||||
if(!$profile) {
|
||||
continue;
|
||||
}
|
||||
$body = $this->updateObject($profile);
|
||||
try {
|
||||
Helpers::sendSignedObject($profile, $url, $body);
|
||||
} catch (HttpException $e) {
|
||||
continue;
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
});
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
$instance = Instance::whereDomain($domain)->firstOrFail();
|
||||
$instance->actors_last_synced_at = now();
|
||||
$instance->save();
|
||||
$this->info('Finished!');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function updateObject($profile)
|
||||
{
|
||||
return [
|
||||
'@context' => [
|
||||
'https://w3id.org/security/v1',
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
[
|
||||
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
|
||||
],
|
||||
],
|
||||
'id' => $profile->permalink('#updates/' . time()),
|
||||
'actor' => $profile->permalink(),
|
||||
'type' => 'Update',
|
||||
'object' => $this->actorObject($profile)
|
||||
];
|
||||
}
|
||||
|
||||
protected function touchStorageCache($domain)
|
||||
{
|
||||
$path = 'actor-update-cache/' . $domain;
|
||||
if(!Storage::exists($path)) {
|
||||
Storage::put($path, "");
|
||||
}
|
||||
}
|
||||
|
||||
protected function getStorageCache($domain)
|
||||
{
|
||||
$path = 'actor-update-cache/' . $domain;
|
||||
return Storage::get($path);
|
||||
}
|
||||
|
||||
protected function updateStorageCache($domain, $value)
|
||||
{
|
||||
$path = 'actor-update-cache/' . $domain;
|
||||
Storage::put($path, $value);
|
||||
}
|
||||
|
||||
protected function actorObject($profile)
|
||||
{
|
||||
$permalink = $profile->permalink();
|
||||
return [
|
||||
'id' => $permalink,
|
||||
'type' => 'Person',
|
||||
'following' => $permalink . '/following',
|
||||
'followers' => $permalink . '/followers',
|
||||
'inbox' => $permalink . '/inbox',
|
||||
'outbox' => $permalink . '/outbox',
|
||||
'preferredUsername' => $profile->username,
|
||||
'name' => $profile->name,
|
||||
'summary' => $profile->bio,
|
||||
'url' => $profile->url(),
|
||||
'manuallyApprovesFollowers' => (bool) $profile->is_private,
|
||||
'publicKey' => [
|
||||
'id' => $permalink . '#main-key',
|
||||
'owner' => $permalink,
|
||||
'publicKeyPem' => $profile->public_key,
|
||||
],
|
||||
'icon' => [
|
||||
'type' => 'Image',
|
||||
'mediaType' => 'image/jpeg',
|
||||
'url' => $profile->avatarUrl(),
|
||||
],
|
||||
'endpoints' => [
|
||||
'sharedInbox' => config('app.url') . '/f/inbox'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\Internal\SoftwareUpdateService;
|
||||
use Cache;
|
||||
|
||||
class SoftwareUpdateRefresh extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:software-update-refresh';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Refresh latest software version data';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$key = SoftwareUpdateService::cacheKey();
|
||||
Cache::forget($key);
|
||||
Cache::remember($key, 1209600, function() {
|
||||
return SoftwareUpdateService::fetchLatest();
|
||||
});
|
||||
$this->info('Succesfully updated software versions!');
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Story;
|
||||
use App\StoryView;
|
||||
|
@ -50,7 +51,7 @@ class StoryGC extends Command
|
|||
protected function archiveExpiredStories()
|
||||
{
|
||||
$stories = Story::whereActive(true)
|
||||
->where('expires_at', '<', now())
|
||||
->where('created_at', '<', now()->subHours(24))
|
||||
->get();
|
||||
|
||||
foreach($stories as $story) {
|
||||
|
@ -78,7 +79,6 @@ class StoryGC extends Command
|
|||
}
|
||||
StoryRotateMedia::dispatch($story)->onQueue('story');
|
||||
StoryService::removeRotateQueue($id);
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ImportPost;
|
||||
use App\Services\ImportService;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use Storage;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\MediaPathService;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Util\Lexer\Autolink;
|
||||
|
||||
class TransformImports extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:transform-imports';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Transform imports into statuses';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(!config('import.instagram.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ips = ImportPost::whereNull('status_id')->where('skip_missing_media', '!=', true)->take(500)->get();
|
||||
|
||||
if(!$ips->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($ips as $ip) {
|
||||
$id = $ip->user_id;
|
||||
$pid = $ip->profile_id;
|
||||
$profile = Profile::find($pid);
|
||||
if(!$profile) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = ImportPost::whereUserId($id)
|
||||
->whereNotNull('status_id')
|
||||
->where('filename', $ip->filename)
|
||||
->where('creation_year', $ip->creation_year)
|
||||
->where('creation_month', $ip->creation_month)
|
||||
->where('creation_day', $ip->creation_day)
|
||||
->exists();
|
||||
|
||||
if($exists == true) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
$idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day);
|
||||
if(!$idk) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) {
|
||||
ImportService::clearAttempts($profile->id);
|
||||
ImportService::getPostCount($profile->id, true);
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
$missingMedia = false;
|
||||
foreach($ip->media as $ipm) {
|
||||
$fileName = last(explode('/', $ipm['uri']));
|
||||
$og = 'imports/' . $id . '/' . $fileName;
|
||||
if(!Storage::exists($og)) {
|
||||
$missingMedia = true;
|
||||
}
|
||||
}
|
||||
|
||||
if($missingMedia === true) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
$caption = $ip->caption;
|
||||
$status = new Status;
|
||||
$status->profile_id = $pid;
|
||||
$status->caption = $caption;
|
||||
$status->rendered = strlen(trim($caption)) ? Autolink::create()->autolink($ip->caption) : null;
|
||||
$status->type = $ip->post_type;
|
||||
|
||||
$status->scope = 'unlisted';
|
||||
$status->visibility = 'unlisted';
|
||||
$status->id = $idk['id'];
|
||||
$status->created_at = now()->parse($ip->creation_date);
|
||||
$status->save();
|
||||
|
||||
foreach($ip->media as $ipm) {
|
||||
$fileName = last(explode('/', $ipm['uri']));
|
||||
$ext = last(explode('.', $fileName));
|
||||
$basePath = MediaPathService::get($profile);
|
||||
$og = 'imports/' . $id . '/' . $fileName;
|
||||
if(!Storage::exists($og)) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
$size = Storage::size($og);
|
||||
$mime = Storage::mimeType($og);
|
||||
$newFile = Str::random(40) . '.' . $ext;
|
||||
$np = $basePath . '/' . $newFile;
|
||||
Storage::move($og, $np);
|
||||
$media = new Media;
|
||||
$media->profile_id = $pid;
|
||||
$media->user_id = $id;
|
||||
$media->status_id = $status->id;
|
||||
$media->media_path = $np;
|
||||
$media->mime = $mime;
|
||||
$media->size = $size;
|
||||
$media->save();
|
||||
}
|
||||
|
||||
$ip->status_id = $status->id;
|
||||
$ip->creation_id = $idk['incr'];
|
||||
$ip->save();
|
||||
|
||||
$profile->status_count = $profile->status_count + 1;
|
||||
$profile->save();
|
||||
|
||||
AccountService::del($profile->id);
|
||||
|
||||
ImportService::clearAttempts($profile->id);
|
||||
ImportService::getPostCount($profile->id, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Transformer\ActivityPub\Verb\DeleteActor;
|
||||
use App\User;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Pool;
|
||||
use Illuminate\Console\Command;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\search;
|
||||
use function Laravel\Prompts\table;
|
||||
|
||||
class UserAccountDelete extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:user-account-delete';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Federate Account Deletion';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = search(
|
||||
label: 'Search for the account to delete by username',
|
||||
placeholder: 'john.appleseed',
|
||||
options: fn (string $value) => strlen($value) > 0
|
||||
? User::withTrashed()->whereStatus('deleted')->where('username', 'like', "%{$value}%")->pluck('username', 'id')->all()
|
||||
: [],
|
||||
);
|
||||
|
||||
$user = User::withTrashed()->find($id);
|
||||
|
||||
table(
|
||||
['Username', 'Name', 'Email', 'Created'],
|
||||
[[$user->username, $user->name, $user->email, $user->created_at]]
|
||||
);
|
||||
|
||||
$confirmed = confirm(
|
||||
label: 'Do you want to federate this account deletion?',
|
||||
default: false,
|
||||
yes: 'Proceed',
|
||||
no: 'Cancel',
|
||||
hint: 'This action is irreversible'
|
||||
);
|
||||
|
||||
if (! $confirmed) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$profile = Profile::withTrashed()->find($user->profile_id);
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($profile, new DeleteActor());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$audience = Instance::whereNotNull(['shared_inbox', 'nodeinfo_last_fetched'])
|
||||
->where('nodeinfo_last_fetched', '>', now()->subHours(12))
|
||||
->distinct()
|
||||
->pluck('shared_inbox');
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
$version = config('pixelfed.version');
|
||||
$appUrl = config('app.url');
|
||||
$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
|
||||
|
||||
$requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
|
||||
foreach ($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity, [
|
||||
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => $userAgent,
|
||||
]);
|
||||
yield function () use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
],
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => 50,
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
},
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
|
||||
$promise->wait();
|
||||
}
|
||||
}
|
|
@ -3,17 +3,16 @@
|
|||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
use App\User;
|
||||
|
||||
class UserAdmin extends Command implements PromptsForMissingInput
|
||||
class UserAdmin extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:admin {username}';
|
||||
protected $signature = 'user:admin {id}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
|
@ -23,15 +22,13 @@ class UserAdmin extends Command implements PromptsForMissingInput
|
|||
protected $description = 'Make a user an admin, or remove admin privileges.';
|
||||
|
||||
/**
|
||||
* Prompt for missing input arguments using the returned questions.
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return array
|
||||
* @return void
|
||||
*/
|
||||
protected function promptForMissingArgumentsUsing()
|
||||
public function __construct()
|
||||
{
|
||||
return [
|
||||
'username' => 'Which username should we toggle admin privileges for?',
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,15 +38,12 @@ class UserAdmin extends Command implements PromptsForMissingInput
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = $this->argument('username');
|
||||
|
||||
$user = User::whereUsername($id)->first();
|
||||
|
||||
$id = $this->argument('id');
|
||||
$user = User::whereUsername($id)->orWhere('id', $id)->first();
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username or id.');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->info('Found username: ' . $user->username);
|
||||
$state = $user->is_admin ? 'Remove admin privileges from this user?' : 'Add admin privileges to this user?';
|
||||
$confirmed = $this->confirm($state);
|
||||
|
|
|
@ -52,8 +52,8 @@ class UserCreate extends Command
|
|||
$user->name = $o['name'];
|
||||
$user->email = $o['email'];
|
||||
$user->password = bcrypt($o['password']);
|
||||
$user->is_admin = $o['is_admin'] == 'true';
|
||||
$user->email_verified_at = $o['confirm_email'] ? now() : null;
|
||||
$user->is_admin = (bool) $o['is_admin'];
|
||||
$user->email_verified_at = (bool) $o['confirm_email'] ? now() : null;
|
||||
$user->save();
|
||||
|
||||
$this->info('Successfully created user!');
|
||||
|
@ -84,11 +84,6 @@ class UserCreate extends Command
|
|||
exit;
|
||||
}
|
||||
|
||||
if (strlen($password) < 6) {
|
||||
$this->error('Must be 6 or more characters, please try again...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$is_admin = $this->confirm('Make this user an admin?');
|
||||
$confirm_email = $this->confirm('Manually verify email address?');
|
||||
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\EmailVerification;
|
||||
use App\User;
|
||||
|
||||
class UserRegistrationMagicLink extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:app-magic-link {--username=} {--email=}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Get the app magic link for users who register in-app but have not recieved the confirmation email';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$username = $this->option('username');
|
||||
$email = $this->option('email');
|
||||
if(!$username && !$email) {
|
||||
$this->error('Please provide the username or email as arguments');
|
||||
$this->line(' ');
|
||||
$this->info('Example: ');
|
||||
$this->info('php artisan user:app-magic-link --username=dansup');
|
||||
$this->info('php artisan user:app-magic-link --email=dansup@pixelfed.com');
|
||||
return;
|
||||
}
|
||||
$user = User::when($username, function($q, $username) {
|
||||
return $q->whereUsername($username);
|
||||
})
|
||||
->when($email, function($q, $email) {
|
||||
return $q->whereEmail($email);
|
||||
})
|
||||
->first();
|
||||
|
||||
if(!$user) {
|
||||
$this->error('We cannot find any matching accounts');
|
||||
return;
|
||||
}
|
||||
|
||||
if($user->email_verified_at) {
|
||||
$this->error('User already verified email address');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$user->register_source || $user->register_source !== 'app' || !$user->app_register_token) {
|
||||
$this->error('User did not register via app');
|
||||
return;
|
||||
}
|
||||
|
||||
$verify = EmailVerification::whereUserId($user->id)->first();
|
||||
|
||||
if(!$verify) {
|
||||
$this->error('Cannot find user verification codes');
|
||||
return;
|
||||
}
|
||||
|
||||
$appUrl = 'pixelfed://confirm-account/'. $user->app_register_token . '?rt=' . $verify->random_token;
|
||||
$this->line(' ');
|
||||
$this->info('Magic link found! Copy the following link and send to user');
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
$this->info($appUrl);
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -39,11 +39,7 @@ class UserShow extends Command
|
|||
public function handle()
|
||||
{
|
||||
$id = $this->argument('id');
|
||||
if(ctype_digit($id) == true) {
|
||||
$user = User::find($id);
|
||||
} else {
|
||||
$user = User::whereUsername($id)->first();
|
||||
}
|
||||
$user = User::whereUsername($id)->orWhere('id', $id)->first();
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username or id.');
|
||||
exit;
|
||||
|
|
|
@ -39,11 +39,7 @@ class UserSuspend extends Command
|
|||
public function handle()
|
||||
{
|
||||
$id = $this->argument('id');
|
||||
if(ctype_digit($id) == true) {
|
||||
$user = User::find($id);
|
||||
} else {
|
||||
$user = User::whereUsername($id)->first();
|
||||
}
|
||||
$user = User::whereUsername($id)->orWhere('id', $id)->first();
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username or id.');
|
||||
exit;
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
use App\User;
|
||||
|
||||
class UserToggle2FA extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:2fa {username}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Disable two factor authentication for given username';
|
||||
|
||||
/**
|
||||
* Prompt for missing input arguments using the returned questions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function promptForMissingArgumentsUsing()
|
||||
{
|
||||
return [
|
||||
'username' => 'Which username should we disable 2FA for?',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$user = User::whereUsername($this->argument('username'))->first();
|
||||
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username');
|
||||
exit;
|
||||
}
|
||||
|
||||
if(!$user->{'2fa_enabled'}) {
|
||||
$this->info('User did not have 2FA enabled!');
|
||||
return;
|
||||
}
|
||||
|
||||
$user->{'2fa_enabled'} = false;
|
||||
$user->{'2fa_secret'} = null;
|
||||
$user->{'2fa_backup_codes'} = null;
|
||||
$user->save();
|
||||
|
||||
$this->info('Successfully disabled 2FA on this account!');
|
||||
}
|
||||
}
|
|
@ -39,11 +39,7 @@ class UserUnsuspend extends Command
|
|||
public function handle()
|
||||
{
|
||||
$id = $this->argument('id');
|
||||
if(ctype_digit($id) == true) {
|
||||
$user = User::find($id);
|
||||
} else {
|
||||
$user = User::whereUsername($id)->first();
|
||||
}
|
||||
$user = User::whereUsername($id)->orWhere('id', $id)->first();
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username or id.');
|
||||
exit;
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use App\User;
|
||||
|
||||
class UserVerifyEmail extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:verifyemail {username}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Verify user email address';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$user = User::whereUsername($this->argument('username'))->first();
|
||||
|
||||
if(!$user) {
|
||||
$this->error('Username not found');
|
||||
return;
|
||||
}
|
||||
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
$this->info('Successfully verified email address for ' . $user->username);
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ class VideoThumbnail extends Command
|
|||
->take($limit)
|
||||
->get();
|
||||
foreach($videos as $video) {
|
||||
Pipeline::dispatch($video);
|
||||
Pipeline::dispatchNow($video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,32 +25,13 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('media:optimize')->hourlyAt(40)->onOneServer();
|
||||
$schedule->command('media:gc')->hourlyAt(5)->onOneServer();
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
|
||||
$schedule->command('story:gc')->everyFiveMinutes()->onOneServer();
|
||||
$schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer();
|
||||
$schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer();
|
||||
$schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer();
|
||||
|
||||
if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('media.delete_local_after_cloud')) {
|
||||
$schedule->command('media:s3gc')->hourlyAt(15);
|
||||
}
|
||||
|
||||
if (config('import.instagram.enabled')) {
|
||||
$schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer();
|
||||
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer();
|
||||
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer();
|
||||
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer();
|
||||
|
||||
if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer();
|
||||
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer();
|
||||
$schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer();
|
||||
$schedule->command('media:optimize')->hourly();
|
||||
$schedule->command('media:gc')->hourly();
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->command('story:gc')->everyFiveMinutes();
|
||||
$schedule->command('gc:failedjobs')->dailyAt(3);
|
||||
$schedule->command('gc:passwordreset')->dailyAt('09:41');
|
||||
$schedule->command('gc:sessions')->twiceDaily(13, 23);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,7 +41,7 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
|
|
|
@ -31,4 +31,20 @@ class DirectMessage extends Model
|
|||
{
|
||||
return Auth::user()->profile->id === $this->from_id;
|
||||
}
|
||||
|
||||
public function toText()
|
||||
{
|
||||
$actorName = $this->author->username;
|
||||
|
||||
return "{$actorName} sent a direct message.";
|
||||
}
|
||||
|
||||
public function toHtml()
|
||||
{
|
||||
$actorName = $this->author->username;
|
||||
$actorUrl = $this->author->url();
|
||||
$url = $this->url();
|
||||
|
||||
return "{$actorName} sent a direct message.";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events\LiveStream;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\LiveStream;
|
||||
|
||||
class BanUser implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $livestream;
|
||||
public $profileId;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(LiveStream $livestream, $profileId)
|
||||
{
|
||||
$this->livestream = $livestream;
|
||||
$this->profileId = $profileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new Channel('live.chat.' . $this->livestream->profile_id);
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'chat.ban-user';
|
||||
}
|
||||
|
||||
public function broadcastWith()
|
||||
{
|
||||
return ['id' => $this->profileId];
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events\LiveStream;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\LiveStream;
|
||||
|
||||
class DeleteChatComment implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $livestream;
|
||||
public $chatmsg;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(LiveStream $livestream, $chatmsg)
|
||||
{
|
||||
$this->livestream = $livestream;
|
||||
$this->chatmsg = $chatmsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new Channel('live.chat.' . $this->livestream->profile_id);
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'chat.delete-message';
|
||||
}
|
||||
|
||||
public function broadcastWith()
|
||||
{
|
||||
return ['id' => $this->chatmsg['id']];
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events\LiveStream;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\LiveStream;
|
||||
|
||||
class NewChatComment implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $livestream;
|
||||
public $chatmsg;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(LiveStream $livestream, $chatmsg)
|
||||
{
|
||||
$this->livestream = $livestream;
|
||||
$this->chatmsg = $chatmsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new Channel('live.chat.' . $this->livestream->profile_id);
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'chat.new-message';
|
||||
}
|
||||
|
||||
public function broadcastWith()
|
||||
{
|
||||
return ['msg' => $this->chatmsg];
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events\LiveStream;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\LiveStream;
|
||||
|
||||
class PinChatMessage implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $livestream;
|
||||
public $chatmsg;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(LiveStream $livestream, $chatmsg)
|
||||
{
|
||||
$this->livestream = $livestream;
|
||||
$this->chatmsg = $chatmsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new Channel('live.chat.' . $this->livestream->profile_id);
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'chat.pin-message';
|
||||
}
|
||||
|
||||
public function broadcastWith()
|
||||
{
|
||||
return $this->chatmsg;
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events\LiveStream;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class StreamEnd implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new Channel('live.chat.' . $this->id);
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'stream.end';
|
||||
}
|
||||
|
||||
public function broadcastWith()
|
||||
{
|
||||
return ['ts' => time() ];
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events\LiveStream;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class StreamStart implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new Channel('live.chat.' . $this->id);
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'stream.start';
|
||||
}
|
||||
|
||||
public function broadcastWith()
|
||||
{
|
||||
return ['ts' => time() ];
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events\LiveStream;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\LiveStream;
|
||||
|
||||
class UnpinChatMessage implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $livestream;
|
||||
public $chatmsg;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(LiveStream $livestream, $chatmsg)
|
||||
{
|
||||
$this->livestream = $livestream;
|
||||
$this->chatmsg = $chatmsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new Channel('live.chat.' . $this->livestream->profile_id);
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'chat.unpin-message';
|
||||
}
|
||||
|
||||
public function broadcastWith()
|
||||
{
|
||||
return $this->chatmsg;
|
||||
}
|
||||
}
|
|
@ -68,20 +68,11 @@ class Handler extends ExceptionHandler
|
|||
*/
|
||||
public function render($request, Throwable $exception)
|
||||
{
|
||||
if ($exception instanceof \Illuminate\Validation\ValidationException && $request->wantsJson()) {
|
||||
return response()->json(
|
||||
[
|
||||
'message' => $exception->getMessage(),
|
||||
'errors' => $exception->validator->getMessageBag()
|
||||
],
|
||||
method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500
|
||||
);
|
||||
} else if ($request->wantsJson()) {
|
||||
if ($request->wantsJson())
|
||||
return response()->json(
|
||||
['error' => $exception->getMessage()],
|
||||
method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500
|
||||
);
|
||||
}
|
||||
return parent::render($request, $exception);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,16 +6,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class FollowRequest extends Model
|
||||
{
|
||||
protected $fillable = ['follower_id', 'following_id', 'activity', 'handled_at'];
|
||||
|
||||
protected $casts = [
|
||||
'activity' => 'array',
|
||||
];
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'follower_id', 'id');
|
||||
}
|
||||
protected $fillable = ['follower_id', 'following_id'];
|
||||
|
||||
public function follower()
|
||||
{
|
||||
|
@ -27,14 +18,13 @@ class FollowRequest extends Model
|
|||
return $this->belongsTo(Profile::class, 'following_id', 'id');
|
||||
}
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'follower_id', 'id');
|
||||
}
|
||||
|
||||
public function target()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'following_id', 'id');
|
||||
}
|
||||
|
||||
public function permalink($append = null, $namespace = '#accepts')
|
||||
{
|
||||
$path = $this->target->permalink("{$namespace}/follows/{$this->id}{$append}");
|
||||
return url($path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,4 +32,20 @@ class Follower extends Model
|
|||
$path = $this->actor->permalink("#accepts/follows/{$this->id}{$append}");
|
||||
return url($path);
|
||||
}
|
||||
|
||||
public function toText()
|
||||
{
|
||||
$actorName = $this->actor->username;
|
||||
|
||||
return "{$actorName} ".__('notification.startedFollowingYou');
|
||||
}
|
||||
|
||||
public function toHtml()
|
||||
{
|
||||
$actorName = $this->actor->username;
|
||||
$actorUrl = $this->actor->url();
|
||||
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
|
||||
__('notification.startedFollowingYou');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@ class HashtagFollow extends Model
|
|||
'hashtag_id'
|
||||
];
|
||||
|
||||
const MAX_LIMIT = 250;
|
||||
|
||||
public function hashtag()
|
||||
{
|
||||
return $this->belongsTo(Hashtag::class);
|
||||
|
|
|
@ -17,25 +17,17 @@ use App\{
|
|||
EmailVerification,
|
||||
Follower,
|
||||
FollowRequest,
|
||||
Media,
|
||||
Notification,
|
||||
Profile,
|
||||
User,
|
||||
UserDevice,
|
||||
UserFilter,
|
||||
UserSetting
|
||||
UserFilter
|
||||
};
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\Mastodon\v1\AccountTransformer;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\RelationshipService;
|
||||
use App\Jobs\FollowPipeline\FollowAcceptPipeline;
|
||||
use App\Jobs\FollowPipeline\FollowRejectPipeline;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
|
@ -44,8 +36,7 @@ class AccountController extends Controller
|
|||
'user.block',
|
||||
];
|
||||
|
||||
const FILTER_LIMIT_MUTE_TEXT = 'You cannot mute more than ';
|
||||
const FILTER_LIMIT_BLOCK_TEXT = 'You cannot block more than ';
|
||||
const FILTER_LIMIT = 'You cannot block or mute more than 100 accounts';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -151,17 +142,16 @@ class AccountController extends Controller
|
|||
public function mute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string|in:user',
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$count = UserFilterService::muteCount($pid);
|
||||
$maxLimit = (int) config_cache('instance.user_filters.max_user_mutes');
|
||||
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
|
||||
$user = Auth::user()->profile;
|
||||
$count = UserFilterService::muteCount($user->id);
|
||||
abort_if($count >= 100, 422, self::FILTER_LIMIT);
|
||||
if($count == 0) {
|
||||
$filterCount = UserFilter::whereUserId($pid)->count();
|
||||
abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
|
||||
$filterCount = UserFilter::whereUserId($user->id)->count();
|
||||
abort_if($filterCount >= 100, 422, self::FILTER_LIMIT);
|
||||
}
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
|
@ -174,7 +164,7 @@ class AccountController extends Controller
|
|||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $pid) {
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
|
@ -184,30 +174,28 @@ class AccountController extends Controller
|
|||
}
|
||||
|
||||
$filter = UserFilter::firstOrCreate([
|
||||
'user_id' => $pid,
|
||||
'user_id' => $user->id,
|
||||
'filterable_id' => $filterable['id'],
|
||||
'filterable_type' => $filterable['type'],
|
||||
'filter_type' => 'mute',
|
||||
]);
|
||||
|
||||
UserFilterService::mute($pid, $filterable['id']);
|
||||
$res = RelationshipService::refresh($pid, $profile->id);
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json($res);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function unmute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string|in:user',
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.mute';
|
||||
|
@ -219,7 +207,7 @@ class AccountController extends Controller
|
|||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $pid) {
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
|
@ -232,21 +220,23 @@ class AccountController extends Controller
|
|||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::whereUserId($pid)
|
||||
$filter = UserFilter::whereUserId($user->id)
|
||||
->whereFilterableId($filterable['id'])
|
||||
->whereFilterableType($filterable['type'])
|
||||
->whereFilterType('mute')
|
||||
->first();
|
||||
|
||||
if($filter) {
|
||||
UserFilterService::unmute($pid, $filterable['id']);
|
||||
$filter->delete();
|
||||
}
|
||||
|
||||
$res = RelationshipService::refresh($pid, $profile->id);
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json($res);
|
||||
return response()->json([200]);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
|
@ -255,16 +245,16 @@ class AccountController extends Controller
|
|||
public function block(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string|in:user',
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
$pid = $request->user()->profile_id;
|
||||
$count = UserFilterService::blockCount($pid);
|
||||
$maxLimit = (int) config_cache('instance.user_filters.max_user_blocks');
|
||||
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$count = UserFilterService::blockCount($user->id);
|
||||
abort_if($count >= 100, 422, self::FILTER_LIMIT);
|
||||
if($count == 0) {
|
||||
$filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();
|
||||
abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
|
||||
$filterCount = UserFilter::whereUserId($user->id)->count();
|
||||
abort_if($filterCount >= 100, 422, self::FILTER_LIMIT);
|
||||
}
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
|
@ -276,74 +266,40 @@ class AccountController extends Controller
|
|||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $pid || ($profile->user && $profile->user->is_admin == true)) {
|
||||
if ($profile->id == $user->id || ($profile->user && $profile->user->is_admin == true)) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
|
||||
$followed = Follower::whereProfileId($profile->id)->whereFollowingId($pid)->first();
|
||||
if($followed) {
|
||||
$followed->delete();
|
||||
$profile->following_count = Follower::whereProfileId($profile->id)->count();
|
||||
$profile->save();
|
||||
$selfProfile = $request->user()->profile;
|
||||
$selfProfile->followers_count = Follower::whereFollowingId($pid)->count();
|
||||
$selfProfile->save();
|
||||
FollowerService::remove($profile->id, $pid);
|
||||
AccountService::del($pid);
|
||||
AccountService::del($profile->id);
|
||||
}
|
||||
|
||||
$following = Follower::whereProfileId($pid)->whereFollowingId($profile->id)->first();
|
||||
if($following) {
|
||||
$following->delete();
|
||||
$profile->followers_count = Follower::whereFollowingId($profile->id)->count();
|
||||
$profile->save();
|
||||
$selfProfile = $request->user()->profile;
|
||||
$selfProfile->following_count = Follower::whereProfileId($pid)->count();
|
||||
$selfProfile->save();
|
||||
FollowerService::remove($pid, $profile->pid);
|
||||
AccountService::del($pid);
|
||||
AccountService::del($profile->id);
|
||||
}
|
||||
|
||||
Notification::whereProfileId($pid)
|
||||
->whereActorId($profile->id)
|
||||
->get()
|
||||
->map(function($n) use($pid) {
|
||||
NotificationService::del($pid, $n['id']);
|
||||
$n->forceDelete();
|
||||
});
|
||||
Follower::whereProfileId($profile->id)->whereFollowingId($user->id)->delete();
|
||||
Notification::whereProfileId($user->id)->whereActorId($profile->id)->delete();
|
||||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::firstOrCreate([
|
||||
'user_id' => $pid,
|
||||
'user_id' => $user->id,
|
||||
'filterable_id' => $filterable['id'],
|
||||
'filterable_type' => $filterable['type'],
|
||||
'filter_type' => 'block',
|
||||
]);
|
||||
|
||||
UserFilterService::block($pid, $filterable['id']);
|
||||
$res = RelationshipService::refresh($pid, $profile->id);
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json($res);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function unblock(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string|in:user',
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.block';
|
||||
|
@ -354,7 +310,7 @@ class AccountController extends Controller
|
|||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $pid) {
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
|
@ -368,7 +324,7 @@ class AccountController extends Controller
|
|||
}
|
||||
|
||||
|
||||
$filter = UserFilter::whereUserId($pid)
|
||||
$filter = UserFilter::whereUserId($user->id)
|
||||
->whereFilterableId($filterable['id'])
|
||||
->whereFilterableType($filterable['type'])
|
||||
->whereFilterType('block')
|
||||
|
@ -376,16 +332,14 @@ class AccountController extends Controller
|
|||
|
||||
if($filter) {
|
||||
$filter->delete();
|
||||
UserFilterService::unblock($pid, $filterable['id']);
|
||||
}
|
||||
|
||||
$res = RelationshipService::refresh($pid, $profile->id);
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json($res);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function followRequests(Request $request)
|
||||
|
@ -404,13 +358,12 @@ class AccountController extends Controller
|
|||
'accounts' => $followers->take(10)->map(function($a) {
|
||||
$actor = $a->actor;
|
||||
return [
|
||||
'rid' => (string) $a->id,
|
||||
'id' => (string) $actor->id,
|
||||
'id' => $actor->id,
|
||||
'username' => $actor->username,
|
||||
'avatar' => $actor->avatarUrl(),
|
||||
'url' => $actor->url(),
|
||||
'local' => $actor->domain == null,
|
||||
'account' => AccountService::get($actor->id)
|
||||
'following' => $actor->followedBy(Auth::user()->profile)
|
||||
];
|
||||
})
|
||||
];
|
||||
|
@ -432,41 +385,22 @@ class AccountController extends Controller
|
|||
|
||||
switch ($action) {
|
||||
case 'accept':
|
||||
$follow = new Follower();
|
||||
$follow->profile_id = $follower->id;
|
||||
$follow->following_id = $pid;
|
||||
$follow->save();
|
||||
|
||||
$profile = Profile::findOrFail($pid);
|
||||
$profile->followers_count++;
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
|
||||
$profile = Profile::findOrFail($follower->id);
|
||||
$profile->following_count++;
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
|
||||
if($follower->domain != null && $follower->private_key === null) {
|
||||
FollowAcceptPipeline::dispatch($followRequest)->onQueue('follow');
|
||||
} else {
|
||||
FollowPipeline::dispatch($follow);
|
||||
$followRequest->delete();
|
||||
}
|
||||
$follow = new Follower();
|
||||
$follow->profile_id = $follower->id;
|
||||
$follow->following_id = $pid;
|
||||
$follow->save();
|
||||
FollowPipeline::dispatch($follow);
|
||||
$followRequest->delete();
|
||||
break;
|
||||
|
||||
case 'reject':
|
||||
if($follower->domain != null && $follower->private_key === null) {
|
||||
FollowRejectPipeline::dispatch($followRequest)->onQueue('follow');
|
||||
} else {
|
||||
$followRequest->delete();
|
||||
}
|
||||
$followRequest->is_rejected = true;
|
||||
$followRequest->save();
|
||||
break;
|
||||
}
|
||||
|
||||
Cache::forget('profile:follower_count:'.$pid);
|
||||
Cache::forget('profile:following_count:'.$pid);
|
||||
RelationshipService::refresh($pid, $follower->id);
|
||||
|
||||
return response()->json(['msg' => 'success'], 200);
|
||||
}
|
||||
|
@ -505,12 +439,6 @@ class AccountController extends Controller
|
|||
if($trustDevice == true) {
|
||||
$request->session()->put('sudoTrustDevice', 1);
|
||||
}
|
||||
|
||||
//Fix wrong scheme when using reverse proxy
|
||||
if(!str_contains($next, 'https') && config('instance.force_https_urls', true)) {
|
||||
$next = Str::of($next)->replace('http', 'https')->toString();
|
||||
}
|
||||
|
||||
return redirect($next);
|
||||
} else {
|
||||
return redirect()
|
||||
|
@ -570,9 +498,10 @@ class AccountController extends Controller
|
|||
$user->save();
|
||||
$request->session()->push('2fa.session.active', true);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,255 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use DB, Cache;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
DiscoverCategory,
|
||||
DiscoverCategoryHashtag,
|
||||
Hashtag,
|
||||
Media,
|
||||
Profile,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
User
|
||||
};
|
||||
use App\Models\ConfigCache;
|
||||
use App\Models\AutospamCustomTokens;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\StatusService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use League\ISO3166\ISO3166;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Http\Controllers\PixelfedDirectoryController;
|
||||
use \DateInterval;
|
||||
use \DatePeriod;
|
||||
use App\Http\Resources\AdminSpamReport;
|
||||
use App\Util\Lexer\Classifier;
|
||||
use App\Jobs\AutospamPipeline\AutospamPretrainPipeline;
|
||||
use App\Jobs\AutospamPipeline\AutospamPretrainNonSpamPipeline;
|
||||
use App\Jobs\AutospamPipeline\AutospamUpdateCachedDataPipeline;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use App\Services\AutospamService;
|
||||
|
||||
trait AdminAutospamController
|
||||
{
|
||||
public function autospamHome(Request $request)
|
||||
{
|
||||
return view('admin.autospam.home');
|
||||
}
|
||||
|
||||
public function getAutospamConfigApi(Request $request)
|
||||
{
|
||||
$open = Cache::remember('admin-dash:reports:spam-count', 3600, function() {
|
||||
return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count();
|
||||
});
|
||||
|
||||
$closed = Cache::remember('admin-dash:reports:spam-count-closed', 3600, function() {
|
||||
return AccountInterstitial::whereType('post.autospam')->whereNotNull('appeal_handled_at')->count();
|
||||
});
|
||||
|
||||
$thisWeek = Cache::remember('admin-dash:reports:spam-count-stats-this-week ', 86400, function() {
|
||||
$sr = config('database.default') == 'pgsql' ? "to_char(created_at, 'MM-YYYY')" : "DATE_FORMAT(created_at, '%m-%Y')";
|
||||
$gb = config('database.default') == 'pgsql' ? [DB::raw($sr)] : DB::raw($sr);
|
||||
$s = AccountInterstitial::select(
|
||||
DB::raw('count(id) as count'),
|
||||
DB::raw($sr . " as month_year")
|
||||
)
|
||||
->where('created_at', '>=', now()->subWeeks(52))
|
||||
->groupBy($gb)
|
||||
->get()
|
||||
->map(function($s) {
|
||||
$dt = now()->parse('01-' . $s->month_year);
|
||||
return [
|
||||
'id' => $dt->format('Ym'),
|
||||
'x' => $dt->format('M Y'),
|
||||
'y' => $s->count
|
||||
];
|
||||
})
|
||||
->sortBy('id')
|
||||
->values()
|
||||
->toArray();
|
||||
return $s;
|
||||
});
|
||||
|
||||
$files = [
|
||||
'spam' => [
|
||||
'exists' => Storage::exists(AutospamService::MODEL_SPAM_PATH),
|
||||
'size' => 0
|
||||
],
|
||||
'ham' => [
|
||||
'exists' => Storage::exists(AutospamService::MODEL_HAM_PATH),
|
||||
'size' => 0
|
||||
],
|
||||
'combined' => [
|
||||
'exists' => Storage::exists(AutospamService::MODEL_FILE_PATH),
|
||||
'size' => 0
|
||||
]
|
||||
];
|
||||
|
||||
if($files['spam']['exists']) {
|
||||
$files['spam']['size'] = Storage::size(AutospamService::MODEL_SPAM_PATH);
|
||||
}
|
||||
|
||||
if($files['ham']['exists']) {
|
||||
$files['ham']['size'] = Storage::size(AutospamService::MODEL_HAM_PATH);
|
||||
}
|
||||
|
||||
if($files['combined']['exists']) {
|
||||
$files['combined']['size'] = Storage::size(AutospamService::MODEL_FILE_PATH);
|
||||
}
|
||||
|
||||
return [
|
||||
'autospam_enabled' => (bool) config_cache('pixelfed.bouncer.enabled') ?? false,
|
||||
'nlp_enabled' => (bool) AutospamService::active(),
|
||||
'files' => $files,
|
||||
'open' => $open,
|
||||
'closed' => $closed,
|
||||
'graph' => collect($thisWeek)->map(fn($s) => $s['y'])->values(),
|
||||
'graphLabels' => collect($thisWeek)->map(fn($s) => $s['x'])->values()
|
||||
];
|
||||
}
|
||||
|
||||
public function getAutospamReportsClosedApi(Request $request)
|
||||
{
|
||||
$appeals = AdminSpamReport::collection(
|
||||
AccountInterstitial::orderBy('id', 'desc')
|
||||
->whereType('post.autospam')
|
||||
->whereIsSpam(true)
|
||||
->whereNotNull('appeal_handled_at')
|
||||
->cursorPaginate(6)
|
||||
->withQueryString()
|
||||
);
|
||||
|
||||
return $appeals;
|
||||
}
|
||||
|
||||
public function postAutospamTrainSpamApi(Request $request)
|
||||
{
|
||||
$aiCount = AccountInterstitial::whereItemType('App\Status')
|
||||
->whereIsSpam(true)
|
||||
->count();
|
||||
abort_if($aiCount < 100, 422, 'You don\'t have enough data to pre-train against.');
|
||||
|
||||
$existing = Cache::get('pf:admin:autospam:pretrain:recent');
|
||||
abort_if($existing, 422, 'You\'ve already run this recently, please wait 30 minutes before pre-training again');
|
||||
AutospamPretrainPipeline::dispatch();
|
||||
Cache::put('pf:admin:autospam:pretrain:recent', 1, 1440);
|
||||
|
||||
return [
|
||||
'msg' => 'Success!'
|
||||
];
|
||||
}
|
||||
|
||||
public function postAutospamTrainNonSpamSearchApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1'
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
$res = Profile::whereNull(['status', 'domain'])
|
||||
->where('username', 'like', '%' . $q . '%')
|
||||
->orderByDesc('followers_count')
|
||||
->take(10)
|
||||
->get()
|
||||
->map(function($p) {
|
||||
$acct = AccountService::get($p->id, true);
|
||||
return [
|
||||
'id' => (string) $p->id,
|
||||
'avatar' => $acct['avatar'],
|
||||
'username' => $p->username
|
||||
];
|
||||
})
|
||||
->values();
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function postAutospamTrainNonSpamSubmitApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'accounts' => 'required|array|min:1|max:10'
|
||||
]);
|
||||
|
||||
$accts = $request->input('accounts');
|
||||
|
||||
$accounts = Profile::whereNull(['domain', 'status'])->find(collect($accts)->map(function($a) { return $a['id'];}));
|
||||
|
||||
abort_if(!$accounts || !$accounts->count(), 422, 'One or more of the selected accounts are not valid');
|
||||
|
||||
AutospamPretrainNonSpamPipeline::dispatch($accounts);
|
||||
return $accounts;
|
||||
}
|
||||
|
||||
public function getAutospamCustomTokensApi(Request $request)
|
||||
{
|
||||
return AutospamCustomTokens::latest()->cursorPaginate(6);
|
||||
}
|
||||
|
||||
public function saveNewAutospamCustomTokensApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'token' => 'required|unique:autospam_custom_tokens,token',
|
||||
]);
|
||||
|
||||
$ct = new AutospamCustomTokens;
|
||||
$ct->token = $request->input('token');
|
||||
$ct->weight = $request->input('weight');
|
||||
$ct->category = $request->input('category') === 'spam' ? 'spam' : 'ham';
|
||||
$ct->note = $request->input('note');
|
||||
$ct->active = $request->input('active');
|
||||
$ct->save();
|
||||
|
||||
AutospamUpdateCachedDataPipeline::dispatch();
|
||||
return $ct;
|
||||
}
|
||||
|
||||
public function updateAutospamCustomTokensApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'token' => 'required',
|
||||
'category' => 'required|in:spam,ham',
|
||||
'active' => 'required|boolean'
|
||||
]);
|
||||
|
||||
$ct = AutospamCustomTokens::findOrFail($request->input('id'));
|
||||
$ct->weight = $request->input('weight');
|
||||
$ct->category = $request->input('category');
|
||||
$ct->note = $request->input('note');
|
||||
$ct->active = $request->input('active');
|
||||
$ct->save();
|
||||
|
||||
AutospamUpdateCachedDataPipeline::dispatch();
|
||||
|
||||
return $ct;
|
||||
}
|
||||
|
||||
public function exportAutospamCustomTokensApi(Request $request)
|
||||
{
|
||||
abort_if(!Storage::exists(AutospamService::MODEL_SPAM_PATH), 422, 'Autospam Dataset does not exist, please train spam before attempting to export');
|
||||
return Storage::download(AutospamService::MODEL_SPAM_PATH);
|
||||
}
|
||||
|
||||
public function enableAutospamApi(Request $request)
|
||||
{
|
||||
ConfigCacheService::put('autospam.nlp.enabled', true);
|
||||
Cache::forget(AutospamService::CHCKD_CACHE_KEY);
|
||||
return ['msg' => 'Success'];
|
||||
}
|
||||
|
||||
public function disableAutospamApi(Request $request)
|
||||
{
|
||||
ConfigCacheService::put('autospam.nlp.enabled', false);
|
||||
Cache::forget(AutospamService::CHCKD_CACHE_KEY);
|
||||
return ['msg' => 'Success'];
|
||||
}
|
||||
}
|
|
@ -1,454 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\PixelfedDirectoryController;
|
||||
use App\Models\ConfigCache;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\StatusService;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use League\ISO3166\ISO3166;
|
||||
|
||||
trait AdminDirectoryController
|
||||
{
|
||||
public function directoryHome(Request $request)
|
||||
{
|
||||
return view('admin.directory.home');
|
||||
}
|
||||
|
||||
public function directoryInitialData(Request $request)
|
||||
{
|
||||
$res = [];
|
||||
|
||||
$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,
|
||||
];
|
||||
});
|
||||
$config = ConfigCache::whereK('pixelfed.directory')->first();
|
||||
if ($config) {
|
||||
$data = $config->v ? json_decode($config->v, true) : [];
|
||||
$res = array_merge($res, $data);
|
||||
}
|
||||
|
||||
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'])) {
|
||||
$res['banner_image'] = url(Storage::url($res['banner_image']));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
$res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
|
||||
$res['curated_onboarding'] = (bool) config_cache('instance.curated_registration.enabled');
|
||||
$res['open_registration'] = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
|
||||
|
||||
$res['activitypub_enabled'] = (bool) config_cache('federation.activitypub.enabled');
|
||||
|
||||
$res['feature_config'] = [
|
||||
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
|
||||
'image_quality' => config_cache('pixelfed.image_quality'),
|
||||
'optimize_image' => (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' => (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' => (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) {
|
||||
return [
|
||||
'profile' => AccountService::get($t['profile_id']),
|
||||
'body' => $t['body'],
|
||||
];
|
||||
});
|
||||
$res['testimonials'] = $testimonials;
|
||||
}
|
||||
|
||||
$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())) {
|
||||
$fail('You must enable image/jpeg and image/png support.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'image_quality' => 'required_if:optimize_image,true|integer|min:75|max:100',
|
||||
'max_altext_length' => 'required|integer|min:1000|max:5000',
|
||||
'max_photo_size' => 'required|integer|min:15000|max:100000',
|
||||
'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',
|
||||
]);
|
||||
|
||||
$res['requirements_validator'] = $validator->errors();
|
||||
|
||||
$res['is_eligible'] = ($res['open_registration'] || $res['curated_onboarding']) &&
|
||||
$res['oauth_enabled'] &&
|
||||
$res['activitypub_enabled'] &&
|
||||
count($res['requirements_validator']) === 0 &&
|
||||
$this->validVal($res, 'admin') &&
|
||||
$this->validVal($res, 'summary', null, 10) &&
|
||||
$this->validVal($res, 'favourite_posts', 3) &&
|
||||
$this->validVal($res, 'contact_email') &&
|
||||
$this->validVal($res, 'privacy_pledge') &&
|
||||
$this->validVal($res, 'location');
|
||||
|
||||
$res['has_submitted'] = config_cache('pixelfed.directory.has_submitted') ?? false;
|
||||
$res['synced'] = config_cache('pixelfed.directory.is_synced') ?? false;
|
||||
$res['latest_response'] = config_cache('pixelfed.directory.latest_response') ?? null;
|
||||
|
||||
$path = base_path('resources/lang');
|
||||
$langs = collect([]);
|
||||
|
||||
foreach (new \DirectoryIterator($path) as $io) {
|
||||
$name = $io->getFilename();
|
||||
$skip = ['vendor'];
|
||||
if ($io->isDot() || in_array($name, $skip)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($io->isDir()) {
|
||||
$langs->push(['code' => $name, 'name' => locale_get_display_name($name)]);
|
||||
}
|
||||
}
|
||||
|
||||
$res['available_languages'] = $langs->sortBy('name')->values();
|
||||
$res['primary_locale'] = config('app.locale');
|
||||
|
||||
$submissionState = Http::withoutVerifying()
|
||||
->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])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($count) {
|
||||
return count($res[$val]) >= $count;
|
||||
}
|
||||
|
||||
if ($minLen) {
|
||||
return strlen($res[$val]) >= $minLen;
|
||||
}
|
||||
|
||||
return $res[$val];
|
||||
}
|
||||
|
||||
public function directoryStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'location' => 'string|min:1|max:53',
|
||||
'summary' => 'string|nullable|max:140',
|
||||
'admin_uid' => 'sometimes|nullable',
|
||||
'contact_email' => 'sometimes|nullable|email:rfc,dns',
|
||||
'favourite_posts' => 'array|max:12',
|
||||
'favourite_posts.*' => 'distinct',
|
||||
'privacy_pledge' => 'sometimes',
|
||||
'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000',
|
||||
]);
|
||||
|
||||
$config = ConfigCache::firstOrNew([
|
||||
'k' => 'pixelfed.directory',
|
||||
]);
|
||||
|
||||
$res = $config->v ? json_decode($config->v, true) : [];
|
||||
$res['summary'] = strip_tags($request->input('summary'));
|
||||
$res['favourite_posts'] = $request->input('favourite_posts');
|
||||
$res['admin'] = (string) $request->input('admin_uid');
|
||||
$res['contact_email'] = $request->input('contact_email');
|
||||
$res['privacy_pledge'] = (bool) $request->input('privacy_pledge');
|
||||
|
||||
if ($request->filled('location')) {
|
||||
$exists = (new ISO3166)->name($request->location);
|
||||
if ($exists) {
|
||||
$res['location'] = $request->input('location');
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->hasFile('banner_image')) {
|
||||
collect(Storage::files('public/headers'))
|
||||
->filter(function ($name) {
|
||||
$protected = [
|
||||
'public/headers/.gitignore',
|
||||
'public/headers/default.jpg',
|
||||
'public/headers/missing.png',
|
||||
];
|
||||
|
||||
return ! in_array($name, $protected);
|
||||
})
|
||||
->each(function ($name) {
|
||||
Storage::delete($name);
|
||||
});
|
||||
$path = $request->file('banner_image')->storePublicly('public/headers');
|
||||
$res['banner_image'] = $path;
|
||||
ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
|
||||
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
}
|
||||
|
||||
$config->v = json_encode($res);
|
||||
$config->save();
|
||||
|
||||
ConfigCacheService::put('pixelfed.directory', $config->v);
|
||||
$updated = json_decode($config->v, true);
|
||||
if (isset($updated['banner_image'])) {
|
||||
$updated['banner_image'] = url(Storage::url($updated['banner_image']));
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
public function directoryHandleServerSubmission(Request $request)
|
||||
{
|
||||
$reqs = [];
|
||||
$reqs['feature_config'] = [
|
||||
'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' => (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'),
|
||||
'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'),
|
||||
'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'),
|
||||
];
|
||||
|
||||
$validator = Validator::make($reqs['feature_config'], [
|
||||
'open_registration' => 'required_unless:curated_onboarding,true',
|
||||
'curated_onboarding' => 'required_unless:open_registration,true',
|
||||
'activitypub_enabled' => 'required|accepted',
|
||||
'oauth_enabled' => 'required|accepted',
|
||||
'media_types' => [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
|
||||
$fail('You must enable image/jpeg and image/png support.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'image_quality' => 'required_if:optimize_image,true|integer|min:75|max:100',
|
||||
'max_altext_length' => 'required|integer|min:1000|max:5000',
|
||||
'max_photo_size' => 'required|integer|min:15000|max:100000',
|
||||
'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',
|
||||
]);
|
||||
|
||||
if (! $validator->validate()) {
|
||||
return response()->json($validator->errors(), 422);
|
||||
}
|
||||
|
||||
ConfigCacheService::put('pixelfed.directory.submission-key', Str::random(random_int(40, 69)));
|
||||
ConfigCacheService::put('pixelfed.directory.submission-ts', now());
|
||||
|
||||
$data = (new PixelfedDirectoryController())->buildListing();
|
||||
$res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data);
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function directoryDeleteBannerImage(Request $request)
|
||||
{
|
||||
$bannerImage = ConfigCache::whereK('app.banner_image')->first();
|
||||
$directory = ConfigCache::whereK('pixelfed.directory')->first();
|
||||
if (! $bannerImage && ! $directory || empty($directory->v)) {
|
||||
return;
|
||||
}
|
||||
$directoryArr = json_decode($directory->v, true);
|
||||
$path = isset($directoryArr['banner_image']) ? $directoryArr['banner_image'] : false;
|
||||
$protected = [
|
||||
'public/headers/.gitignore',
|
||||
'public/headers/default.jpg',
|
||||
'public/headers/missing.png',
|
||||
];
|
||||
if (! $path || in_array($path, $protected)) {
|
||||
return;
|
||||
}
|
||||
if (Storage::exists($directoryArr['banner_image'])) {
|
||||
Storage::delete($directoryArr['banner_image']);
|
||||
}
|
||||
|
||||
$directoryArr['banner_image'] = 'public/headers/default.jpg';
|
||||
$directory->v = $directoryArr;
|
||||
$directory->save();
|
||||
$bannerImage->v = url(Storage::url('public/headers/default.jpg'));
|
||||
$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 () {
|
||||
return Status::whereLocal(true)
|
||||
->whereScope('public')
|
||||
->whereType('photo')
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->orderByDesc('likes_count')
|
||||
->take(50)
|
||||
->pluck('id');
|
||||
});
|
||||
|
||||
$res = $ids->map(function ($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function directoryGetAddPostByIdSearch(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|integer',
|
||||
]);
|
||||
|
||||
$id = $request->input('q');
|
||||
|
||||
$status = Status::whereLocal(true)
|
||||
->whereType('photo')
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->findOrFail($id);
|
||||
|
||||
$res = StatusService::get($status->id);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function directoryDeleteTestimonial(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required',
|
||||
]);
|
||||
$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) {
|
||||
return $t['profile_id'] !== $profile_id;
|
||||
})
|
||||
->values();
|
||||
ConfigCacheService::put('pixelfed.directory.testimonials', $existing);
|
||||
|
||||
return $existing;
|
||||
}
|
||||
|
||||
public function directorySaveTestimonial(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'username' => 'required',
|
||||
'body' => 'required|string|min:5|max:500',
|
||||
]);
|
||||
|
||||
$user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail();
|
||||
|
||||
$configCache = ConfigCache::firstOrCreate([
|
||||
'k' => 'pixelfed.directory.testimonials',
|
||||
]);
|
||||
|
||||
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
|
||||
|
||||
abort_if($testimonials->contains('profile_id', $user->profile_id), 422, 'Testimonial already exists');
|
||||
abort_if($testimonials->count() == 10, 422, 'You can only have 10 active testimonials');
|
||||
|
||||
$testimonials->push([
|
||||
'profile_id' => (string) $user->profile_id,
|
||||
'username' => $request->input('username'),
|
||||
'body' => $request->input('body'),
|
||||
]);
|
||||
|
||||
$configCache->v = json_encode($testimonials->toArray());
|
||||
$configCache->save();
|
||||
ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
|
||||
$res = [
|
||||
'profile' => AccountService::get($user->profile_id),
|
||||
'body' => $request->input('body'),
|
||||
];
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function directoryUpdateTestimonial(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required',
|
||||
'body' => 'required|string|min:5|max:500',
|
||||
]);
|
||||
|
||||
$profile_id = $request->input('profile_id');
|
||||
$body = $request->input('body');
|
||||
$user = User::whereProfileId($profile_id)->firstOrFail();
|
||||
|
||||
$configCache = ConfigCache::firstOrCreate([
|
||||
'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) {
|
||||
$t['body'] = $body;
|
||||
}
|
||||
|
||||
return $t;
|
||||
})
|
||||
->values();
|
||||
|
||||
$configCache->v = json_encode($updated);
|
||||
$configCache->save();
|
||||
ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
|
||||
|
||||
return $updated;
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Hashtag;
|
||||
use App\StatusHashtag;
|
||||
use App\Http\Resources\AdminHashtag;
|
||||
use App\Services\TrendingHashtagService;
|
||||
|
||||
trait AdminHashtagsController
|
||||
{
|
||||
public function hashtagsHome(Request $request)
|
||||
{
|
||||
return view('admin.hashtags.home');
|
||||
}
|
||||
|
||||
public function hashtagsApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'sometimes|in:banned,nsfw',
|
||||
'sort' => 'sometimes|in:id,name,cached_count,can_search,can_trend,is_banned,is_nsfw',
|
||||
'dir' => 'sometimes|in:asc,desc'
|
||||
]);
|
||||
$action = $request->input('action');
|
||||
$query = $request->input('q');
|
||||
$sort = $request->input('sort');
|
||||
$order = $request->input('dir');
|
||||
|
||||
$hashtags = Hashtag::when($query, function($q, $query) {
|
||||
return $q->where('name', 'like', $query . '%');
|
||||
})
|
||||
->when($sort, function($q, $sort) use($order) {
|
||||
return $q->orderBy($sort, $order);
|
||||
}, function($q) {
|
||||
return $q->orderByDesc('id');
|
||||
})
|
||||
->when($action, function($q, $action) {
|
||||
if($action === 'banned') {
|
||||
return $q->whereIsBanned(true);
|
||||
} else if ($action === 'nsfw') {
|
||||
return $q->whereIsNsfw(true);
|
||||
}
|
||||
})
|
||||
->cursorPaginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return AdminHashtag::collection($hashtags);
|
||||
}
|
||||
|
||||
public function hashtagsStats(Request $request)
|
||||
{
|
||||
$stats = [
|
||||
'total_unique' => Hashtag::count(),
|
||||
'total_posts' => StatusHashtag::count(),
|
||||
'added_14_days' => Hashtag::where('created_at', '>', now()->subDays(14))->count(),
|
||||
'total_banned' => Hashtag::whereIsBanned(true)->count(),
|
||||
'total_nsfw' => Hashtag::whereIsNsfw(true)->count()
|
||||
];
|
||||
|
||||
return response()->json($stats);
|
||||
}
|
||||
|
||||
public function hashtagsGet(Request $request)
|
||||
{
|
||||
return new AdminHashtag(Hashtag::findOrFail($request->input('id')));
|
||||
}
|
||||
|
||||
public function hashtagsUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'name' => 'required',
|
||||
'slug' => 'required',
|
||||
'can_search' => 'required:boolean',
|
||||
'can_trend' => 'required:boolean',
|
||||
'is_nsfw' => 'required:boolean',
|
||||
'is_banned' => 'required:boolean'
|
||||
]);
|
||||
|
||||
$hashtag = Hashtag::whereSlug($request->input('slug'))->findOrFail($request->input('id'));
|
||||
$canTrendPrev = $hashtag->can_trend == null ? true : $hashtag->can_trend;
|
||||
$hashtag->is_banned = $request->input('is_banned');
|
||||
$hashtag->is_nsfw = $request->input('is_nsfw');
|
||||
$hashtag->can_search = $hashtag->is_banned ? false : $request->input('can_search');
|
||||
$hashtag->can_trend = $hashtag->is_banned ? false : $request->input('can_trend');
|
||||
$hashtag->save();
|
||||
|
||||
TrendingHashtagService::refresh();
|
||||
|
||||
return new AdminHashtag($hashtag);
|
||||
}
|
||||
|
||||
public function hashtagsClearTrendingCache(Request $request)
|
||||
{
|
||||
TrendingHashtagService::refresh();
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
|
@ -7,14 +7,66 @@ use App\{Instance, Profile};
|
|||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Services\InstanceService;
|
||||
use App\Http\Resources\AdminInstance;
|
||||
|
||||
trait AdminInstanceController
|
||||
{
|
||||
|
||||
public function instances(Request $request)
|
||||
{
|
||||
return view('admin.instances.home');
|
||||
$this->validate($request, [
|
||||
|
||||
'filter' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'min:1',
|
||||
'max:20',
|
||||
Rule::in([
|
||||
'cw',
|
||||
'unlisted',
|
||||
'banned',
|
||||
// 'popular',
|
||||
'new',
|
||||
'all'
|
||||
])
|
||||
],
|
||||
]);
|
||||
if($request->has('q') && $request->filled('q')) {
|
||||
$instances = Instance::where('domain', 'like', '%' . $request->input('q') . '%')->simplePaginate(10);
|
||||
} else if($request->has('filter') && $request->filled('filter')) {
|
||||
switch ($request->filter) {
|
||||
case 'cw':
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereAutoCw(true)->orderByDesc('id')->simplePaginate(10);
|
||||
break;
|
||||
case 'unlisted':
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereUnlisted(true)->orderByDesc('id')->simplePaginate(10);
|
||||
break;
|
||||
case 'banned':
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereBanned(true)->orderByDesc('id')->simplePaginate(10);
|
||||
break;
|
||||
case 'new':
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->latest()->simplePaginate(10);
|
||||
break;
|
||||
// case 'popular':
|
||||
// $popular = Profile::selectRaw('*, count(domain) as count')
|
||||
// ->whereNotNull('domain')
|
||||
// ->groupBy('domain')
|
||||
// ->orderByDesc('count')
|
||||
// ->take(10)
|
||||
// ->get()
|
||||
// ->pluck('domain')
|
||||
// ->toArray();
|
||||
// $instances = Instance::whereIn('domain', $popular)->simplePaginate(10);
|
||||
// break;
|
||||
|
||||
default:
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->orderByDesc('id')->simplePaginate(10);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->orderByDesc('id')->simplePaginate(10);
|
||||
}
|
||||
|
||||
return view('admin.instances.home', compact('instances'));
|
||||
}
|
||||
|
||||
public function instanceScan(Request $request)
|
||||
|
@ -74,229 +126,10 @@ trait AdminInstanceController
|
|||
break;
|
||||
}
|
||||
|
||||
Cache::forget(InstanceService::CACHE_KEY_BANNED_DOMAINS);
|
||||
Cache::forget(InstanceService::CACHE_KEY_UNLISTED_DOMAINS);
|
||||
Cache::forget(InstanceService::CACHE_KEY_NSFW_DOMAINS);
|
||||
Cache::forget('instances:banned:domains');
|
||||
Cache::forget('instances:unlisted:domains');
|
||||
Cache::forget('instances:auto_cw:domains');
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
public function getInstancesStatsApi(Request $request)
|
||||
{
|
||||
return InstanceService::stats();
|
||||
}
|
||||
|
||||
public function getInstancesQueryApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required'
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
return AdminInstance::collection(
|
||||
Instance::where('domain', 'like', '%' . $q . '%')
|
||||
->orderByDesc('user_count')
|
||||
->cursorPaginate(10)
|
||||
->withQueryString()
|
||||
);
|
||||
}
|
||||
|
||||
public function getInstancesApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'filter' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'min:1',
|
||||
'max:20',
|
||||
Rule::in([
|
||||
'cw',
|
||||
'unlisted',
|
||||
'banned',
|
||||
'popular_users',
|
||||
'popular_statuses',
|
||||
'new',
|
||||
'all'
|
||||
])
|
||||
],
|
||||
'sort' => [
|
||||
'sometimes',
|
||||
'string',
|
||||
Rule::in([
|
||||
'id',
|
||||
'domain',
|
||||
'software',
|
||||
'user_count',
|
||||
'status_count',
|
||||
'banned',
|
||||
'auto_cw',
|
||||
'unlisted'
|
||||
])
|
||||
],
|
||||
'dir' => 'sometimes|in:desc,asc'
|
||||
]);
|
||||
$filter = $request->input('filter');
|
||||
$query = $request->input('q');
|
||||
$sortCol = $request->input('sort');
|
||||
$sortDir = $request->input('dir');
|
||||
|
||||
return AdminInstance::collection(Instance::when($query, function($q, $qq) use($query) {
|
||||
return $q->where('domain', 'like', '%' . $query . '%');
|
||||
})
|
||||
->when($filter, function($q, $f) use($filter) {
|
||||
if($filter == 'cw') { return $q->whereAutoCw(true); }
|
||||
if($filter == 'unlisted') { return $q->whereUnlisted(true); }
|
||||
if($filter == 'banned') { return $q->whereBanned(true); }
|
||||
if($filter == 'new') { return $q->orderByDesc('id'); }
|
||||
if($filter == 'popular_users') { return $q->orderByDesc('user_count'); }
|
||||
if($filter == 'popular_statuses') { return $q->orderByDesc('status_count'); }
|
||||
return $q->orderByDesc('id');
|
||||
})
|
||||
->when($sortCol, function($q, $s) use($sortCol, $sortDir, $filter) {
|
||||
if(!in_array($filter, ['popular_users', 'popular_statuses'])) {
|
||||
return $q->whereNotNull($sortCol)->orderBy($sortCol, $sortDir);
|
||||
}
|
||||
}, function($q) use($filter) {
|
||||
if(!$filter || !in_array($filter, ['popular_users', 'popular_statuses'])) {
|
||||
return $q->orderByDesc('id');
|
||||
}
|
||||
})
|
||||
->cursorPaginate(10)
|
||||
->withQueryString());
|
||||
}
|
||||
|
||||
public function postInstanceUpdateApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'banned' => 'boolean',
|
||||
'auto_cw' => 'boolean',
|
||||
'unlisted' => 'boolean',
|
||||
'notes' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$id = $request->input('id');
|
||||
$instance = Instance::findOrFail($id);
|
||||
$instance->update($request->only([
|
||||
'banned',
|
||||
'auto_cw',
|
||||
'unlisted',
|
||||
'notes'
|
||||
]));
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return new AdminInstance($instance);
|
||||
}
|
||||
|
||||
public function postInstanceCreateNewApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|string',
|
||||
'banned' => 'boolean',
|
||||
'auto_cw' => 'boolean',
|
||||
'unlisted' => 'boolean',
|
||||
'notes' => 'nullable|string|max:500'
|
||||
]);
|
||||
|
||||
$domain = $request->input('domain');
|
||||
|
||||
abort_if(!strpos($domain, '.'), 400, 'Invalid domain');
|
||||
abort_if(!filter_var($domain, FILTER_VALIDATE_DOMAIN), 400, 'Invalid domain');
|
||||
|
||||
$instance = new Instance;
|
||||
$instance->domain = $request->input('domain');
|
||||
$instance->banned = $request->input('banned');
|
||||
$instance->auto_cw = $request->input('auto_cw');
|
||||
$instance->unlisted = $request->input('unlisted');
|
||||
$instance->manually_added = true;
|
||||
$instance->notes = $request->input('notes');
|
||||
$instance->save();
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return new AdminInstance($instance);
|
||||
}
|
||||
|
||||
public function postInstanceRefreshStatsApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required'
|
||||
]);
|
||||
|
||||
$instance = Instance::findOrFail($request->input('id'));
|
||||
$instance->user_count = Profile::whereDomain($instance->domain)->count();
|
||||
$instance->status_count = Profile::whereDomain($instance->domain)->leftJoin('statuses', 'profiles.id', '=', 'statuses.profile_id')->count();
|
||||
$instance->save();
|
||||
|
||||
return new AdminInstance($instance);
|
||||
}
|
||||
|
||||
public function postInstanceDeleteApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required'
|
||||
]);
|
||||
|
||||
$instance = Instance::findOrFail($request->input('id'));
|
||||
$instance->delete();
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function downloadBackup(Request $request)
|
||||
{
|
||||
return response()->streamDownload(function () {
|
||||
$json = [
|
||||
'version' => 1,
|
||||
'auto_cw' => Instance::whereAutoCw(true)->pluck('domain')->toArray(),
|
||||
'unlisted' => Instance::whereUnlisted(true)->pluck('domain')->toArray(),
|
||||
'banned' => Instance::whereBanned(true)->pluck('domain')->toArray(),
|
||||
'created_at' => now()->format('c'),
|
||||
];
|
||||
$chk = hash('sha256', json_encode($json, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
||||
$json['_sha256'] = $chk;
|
||||
echo json_encode($json, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}, 'pixelfed-instances-mod.json');
|
||||
}
|
||||
|
||||
public function importBackup(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'banned' => 'sometimes|array',
|
||||
'auto_cw' => 'sometimes|array',
|
||||
'unlisted' => 'sometimes|array',
|
||||
]);
|
||||
|
||||
$banned = $request->input('banned');
|
||||
$auto_cw = $request->input('auto_cw');
|
||||
$unlisted = $request->input('unlisted');
|
||||
|
||||
foreach($banned as $i) {
|
||||
Instance::updateOrCreate(
|
||||
['domain' => $i],
|
||||
['banned' => true]
|
||||
);
|
||||
}
|
||||
|
||||
foreach($auto_cw as $i) {
|
||||
Instance::updateOrCreate(
|
||||
['domain' => $i],
|
||||
['auto_cw' => true]
|
||||
);
|
||||
}
|
||||
|
||||
foreach($unlisted as $i) {
|
||||
Instance::updateOrCreate(
|
||||
['domain' => $i],
|
||||
['unlisted' => true]
|
||||
);
|
||||
}
|
||||
|
||||
InstanceService::refresh();
|
||||
return [200];
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -11,7 +11,6 @@ use App\Mail\AdminMessage;
|
|||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Services\ModLogService;
|
||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||
use App\Services\AccountService;
|
||||
|
||||
trait AdminUserController
|
||||
{
|
||||
|
@ -26,7 +25,7 @@ trait AdminUserController
|
|||
'next' => $offset + 1,
|
||||
'query' => $search ? '&a=search&q=' . $search : null
|
||||
];
|
||||
$users = User::select('id', 'username', 'status', 'profile_id', 'is_admin')
|
||||
$users = User::select('id', 'username', 'status', 'profile_id')
|
||||
->orderBy($col, $dir)
|
||||
->when($search, function($q, $search) {
|
||||
return $q->where('username', 'like', "%{$search}%");
|
||||
|
@ -35,11 +34,7 @@ trait AdminUserController
|
|||
return $q->offset(($offset * 10));
|
||||
})
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(function($u) {
|
||||
$u['account'] = AccountService::get($u->profile_id, true);
|
||||
return $u;
|
||||
});
|
||||
->get();
|
||||
|
||||
return view('admin.users.home', compact('users', 'pagination'));
|
||||
}
|
||||
|
@ -227,7 +222,7 @@ trait AdminUserController
|
|||
->save();
|
||||
|
||||
Cache::forget('profiles:private');
|
||||
DeleteAccountPipeline::dispatch($user);
|
||||
DeleteAccountPipeline::dispatch($user)->onQueue('high');
|
||||
|
||||
$msg = "Successfully deleted {$user->username}!";
|
||||
$request->session()->flash('status', $msg);
|
||||
|
@ -299,4 +294,4 @@ trait AdminUserController
|
|||
$request->session()->flash('status', $msg);
|
||||
return redirect('/i/admin/users/modlogs/' . $user->id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,53 +6,39 @@ use App\{
|
|||
AccountInterstitial,
|
||||
Contact,
|
||||
Hashtag,
|
||||
Instance,
|
||||
Newsroom,
|
||||
OauthClient,
|
||||
Profile,
|
||||
Report,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
Story,
|
||||
User
|
||||
};
|
||||
use DB, Cache, Storage;
|
||||
use DB, Cache;
|
||||
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\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\StoryService;
|
||||
use App\Models\CustomEmoji;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
use AdminReportController,
|
||||
AdminAutospamController,
|
||||
AdminDirectoryController,
|
||||
use AdminReportController,
|
||||
AdminDiscoverController,
|
||||
AdminHashtagsController,
|
||||
// AdminGroupsController,
|
||||
AdminMediaController,
|
||||
AdminSettingsController,
|
||||
AdminMediaController,
|
||||
AdminSettingsController,
|
||||
AdminInstanceController,
|
||||
// AdminStorageController,
|
||||
AdminUserController;
|
||||
|
||||
public function __construct()
|
||||
|
@ -63,71 +49,9 @@ class AdminController extends Controller
|
|||
}
|
||||
|
||||
public function home()
|
||||
{
|
||||
return view('admin.home');
|
||||
}
|
||||
|
||||
public function stats()
|
||||
{
|
||||
$data = AdminStatsService::get();
|
||||
return view('admin.stats', compact('data'));
|
||||
}
|
||||
|
||||
public function getStats()
|
||||
{
|
||||
return AdminStatsService::summary();
|
||||
}
|
||||
|
||||
public function getAccounts()
|
||||
{
|
||||
$users = User::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;
|
||||
return $account;
|
||||
})
|
||||
->filter(function($user) {
|
||||
return $user;
|
||||
})
|
||||
];
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function getPosts()
|
||||
{
|
||||
$posts = DB::table('statuses')
|
||||
->orderByDesc('id')
|
||||
->cursorPaginate(10);
|
||||
|
||||
$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;
|
||||
})
|
||||
];
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function getInstances()
|
||||
{
|
||||
return Instance::orderByDesc('id')->cursorPaginate(10);
|
||||
return view('admin.home', compact('data'));
|
||||
}
|
||||
|
||||
public function statuses(Request $request)
|
||||
|
@ -191,7 +115,7 @@ class AdminController extends Controller
|
|||
public function appsHome(Request $request)
|
||||
{
|
||||
$filter = $request->input('filter');
|
||||
if($filter == 'revoked') {
|
||||
if(in_array($filter, ['revoked'])) {
|
||||
$apps = OauthClient::with('user')
|
||||
->whereNotNull('user_id')
|
||||
->whereRevoked(true)
|
||||
|
@ -206,6 +130,12 @@ class AdminController extends Controller
|
|||
return view('admin.apps.home', compact('apps'));
|
||||
}
|
||||
|
||||
public function hashtagsHome(Request $request)
|
||||
{
|
||||
$hashtags = Hashtag::orderByDesc('id')->paginate(10);
|
||||
return view('admin.hashtags.home', compact('hashtags'));
|
||||
}
|
||||
|
||||
public function messagesHome(Request $request)
|
||||
{
|
||||
$messages = Contact::orderByDesc('id')->paginate(10);
|
||||
|
@ -266,10 +196,6 @@ class AdminController extends Controller
|
|||
]);
|
||||
$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',
|
||||
|
@ -287,7 +213,7 @@ class AdminController extends Controller
|
|||
case 'string':
|
||||
if($request->{$field} != $news->{$field}) {
|
||||
if($field == 'title') {
|
||||
$news->slug = $slug;
|
||||
$news->slug = str_slug($request->{$field});
|
||||
}
|
||||
$news->{$field} = $request->{$field};
|
||||
$changed = true;
|
||||
|
@ -333,10 +259,6 @@ class AdminController extends Controller
|
|||
]);
|
||||
$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',
|
||||
|
@ -354,7 +276,7 @@ class AdminController extends Controller
|
|||
case 'string':
|
||||
if($request->{$field} != $news->{$field}) {
|
||||
if($field == 'title') {
|
||||
$news->slug = $slug;
|
||||
$news->slug = str_slug($request->{$field});
|
||||
}
|
||||
$news->{$field} = $request->{$field};
|
||||
$changed = true;
|
||||
|
@ -421,143 +343,4 @@ class AdminController extends Controller
|
|||
$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();
|
||||
}
|
||||
} 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();
|
||||
|
||||
$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'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,335 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Mail\CuratedRegisterAcceptUser;
|
||||
use App\Mail\CuratedRegisterRejectUser;
|
||||
use App\Mail\CuratedRegisterRequestDetailsFromUser;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\Models\CuratedRegisterActivity;
|
||||
use App\Models\CuratedRegisterTemplate;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdminCuratedRegisterController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(['auth', 'admin']);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses',
|
||||
'sort' => 'sometimes|in:asc,desc',
|
||||
]);
|
||||
$filter = $request->input('filter', 'open');
|
||||
$sort = $request->input('sort', 'asc');
|
||||
$records = CuratedRegister::when($filter, function ($q, $filter) {
|
||||
if ($filter === 'open') {
|
||||
return $q->where('is_rejected', false)
|
||||
->where(function ($query) {
|
||||
return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false);
|
||||
})
|
||||
->whereNotNull('email_verified_at')
|
||||
->whereIsClosed(false);
|
||||
} elseif ($filter === 'all') {
|
||||
return $q;
|
||||
} elseif ($filter === 'responses') {
|
||||
return $q->whereIsClosed(false)
|
||||
->whereNotNull('email_verified_at')
|
||||
->where('user_has_responded', true)
|
||||
->where('is_awaiting_more_info', true);
|
||||
} elseif ($filter === 'awaiting') {
|
||||
return $q->whereIsClosed(false)
|
||||
->where('is_rejected', false)
|
||||
->where('is_approved', false)
|
||||
->where('user_has_responded', false)
|
||||
->where('is_awaiting_more_info', true);
|
||||
} elseif ($filter === 'approved') {
|
||||
return $q->whereIsClosed(true)->whereIsApproved(true);
|
||||
} elseif ($filter === 'rejected') {
|
||||
return $q->whereIsClosed(true)->whereIsRejected(true);
|
||||
}
|
||||
})
|
||||
->when($sort, function ($query, $sort) {
|
||||
return $query->orderBy('id', $sort);
|
||||
})
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.curated-register.index', compact('records', 'filter'));
|
||||
}
|
||||
|
||||
public function show(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
|
||||
return view('admin.curated-register.show', compact('record'));
|
||||
}
|
||||
|
||||
public function apiActivityLog(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
|
||||
$res = collect([
|
||||
[
|
||||
'id' => 1,
|
||||
'action' => 'created',
|
||||
'title' => 'Onboarding application created',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->created_at,
|
||||
],
|
||||
]);
|
||||
|
||||
if ($record->email_verified_at) {
|
||||
$res->push([
|
||||
'id' => 3,
|
||||
'action' => 'email_verified_at',
|
||||
'title' => 'Applicant successfully verified email address',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->email_verified_at,
|
||||
]);
|
||||
}
|
||||
|
||||
$activities = CuratedRegisterActivity::whereRegisterId($record->id)->get();
|
||||
|
||||
$idx = 4;
|
||||
$userResponses = collect([]);
|
||||
|
||||
foreach ($activities as $activity) {
|
||||
$idx++;
|
||||
|
||||
if ($activity->type === 'user_resend_email_confirmation') {
|
||||
continue;
|
||||
}
|
||||
if ($activity->from_user) {
|
||||
$userResponses->push($activity);
|
||||
|
||||
continue;
|
||||
}
|
||||
$res->push([
|
||||
'id' => $idx,
|
||||
'aid' => $activity->id,
|
||||
'action' => $activity->type,
|
||||
'title' => $activity->from_admin ? 'Admin requested info' : 'User responded',
|
||||
'message' => $activity->message,
|
||||
'link' => $activity->adminReviewUrl(),
|
||||
'timestamp' => $activity->created_at,
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($userResponses as $ur) {
|
||||
$res = $res->map(function ($r) use ($ur) {
|
||||
if (! isset($r['aid'])) {
|
||||
return $r;
|
||||
}
|
||||
if ($ur->reply_to_id === $r['aid']) {
|
||||
$r['user_response'] = $ur;
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
return $r;
|
||||
});
|
||||
}
|
||||
|
||||
if ($record->is_approved) {
|
||||
$idx++;
|
||||
$res->push([
|
||||
'id' => $idx,
|
||||
'action' => 'approved',
|
||||
'title' => 'Application Approved',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->action_taken_at,
|
||||
]);
|
||||
} elseif ($record->is_rejected) {
|
||||
$idx++;
|
||||
$res->push([
|
||||
'id' => $idx,
|
||||
'action' => 'rejected',
|
||||
'title' => 'Application Rejected',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->action_taken_at,
|
||||
]);
|
||||
}
|
||||
|
||||
return $res->reverse()->values();
|
||||
}
|
||||
|
||||
public function apiMessagePreviewStore(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function apiMessageSendStore(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'message' => 'required|string|min:5|max:1000',
|
||||
]);
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$activity = new CuratedRegisterActivity;
|
||||
$activity->register_id = $record->id;
|
||||
$activity->admin_id = $request->user()->id;
|
||||
$activity->secret_code = Str::random(32);
|
||||
$activity->type = 'request_details';
|
||||
$activity->from_admin = true;
|
||||
$activity->message = $request->input('message');
|
||||
$activity->save();
|
||||
$record->is_awaiting_more_info = true;
|
||||
$record->user_has_responded = false;
|
||||
$record->save();
|
||||
Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity));
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function previewDetailsMessageShow(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$activity = new CuratedRegisterActivity;
|
||||
$activity->message = $request->input('message');
|
||||
|
||||
return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity);
|
||||
}
|
||||
|
||||
public function previewMessageShow(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$record->message = $request->input('message');
|
||||
|
||||
return new \App\Mail\CuratedRegisterSendMessage($record);
|
||||
}
|
||||
|
||||
public function apiHandleReject(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:reject-email,reject-silent',
|
||||
]);
|
||||
$action = $request->input('action');
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
|
||||
$record->is_rejected = true;
|
||||
$record->is_closed = true;
|
||||
$record->action_taken_at = now();
|
||||
$record->save();
|
||||
if ($action === 'reject-email') {
|
||||
Mail::to($record->email)->send(new CuratedRegisterRejectUser($record));
|
||||
}
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function apiHandleApprove(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
|
||||
$record->is_approved = true;
|
||||
$record->is_closed = true;
|
||||
$record->action_taken_at = now();
|
||||
$record->save();
|
||||
$user = User::create([
|
||||
'name' => $record->username,
|
||||
'username' => $record->username,
|
||||
'email' => $record->email,
|
||||
'password' => $record->password,
|
||||
'app_register_ip' => $record->ip_address,
|
||||
'email_verified_at' => now(),
|
||||
'register_source' => 'cur_onboarding',
|
||||
]);
|
||||
|
||||
Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record));
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function templates(Request $request)
|
||||
{
|
||||
$templates = CuratedRegisterTemplate::paginate(10);
|
||||
|
||||
return view('admin.curated-register.templates', compact('templates'));
|
||||
}
|
||||
|
||||
public function templateCreate(Request $request)
|
||||
{
|
||||
return view('admin.curated-register.template-create');
|
||||
}
|
||||
|
||||
public function templateEdit(Request $request, $id)
|
||||
{
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
|
||||
return view('admin.curated-register.template-edit', compact('template'));
|
||||
}
|
||||
|
||||
public function templateEditStore(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:30',
|
||||
'content' => 'required|string|min:5|max:3000',
|
||||
'description' => 'nullable|sometimes|string|max:1000',
|
||||
'active' => 'sometimes',
|
||||
]);
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
$template->name = $request->input('name');
|
||||
$template->content = $request->input('content');
|
||||
$template->description = $request->input('description');
|
||||
$template->is_active = $request->boolean('active');
|
||||
$template->save();
|
||||
|
||||
return redirect()->back()->with('status', 'Successfully updated template!');
|
||||
}
|
||||
|
||||
public function templateDelete(Request $request, $id)
|
||||
{
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
$template->delete();
|
||||
|
||||
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully deleted template!');
|
||||
}
|
||||
|
||||
public function templateStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:30',
|
||||
'content' => 'required|string|min:5|max:3000',
|
||||
'description' => 'nullable|sometimes|string|max:1000',
|
||||
'active' => 'sometimes',
|
||||
]);
|
||||
CuratedRegisterTemplate::create([
|
||||
'name' => $request->input('name'),
|
||||
'content' => $request->input('content'),
|
||||
'description' => $request->input('description'),
|
||||
'is_active' => $request->boolean('active'),
|
||||
]);
|
||||
|
||||
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully created new template!');
|
||||
}
|
||||
|
||||
public function getActiveTemplates(Request $request)
|
||||
{
|
||||
$templates = CuratedRegisterTemplate::whereIsActive(true)
|
||||
->orderBy('order')
|
||||
->get()
|
||||
->map(function ($tmp) {
|
||||
return [
|
||||
'name' => $tmp->name,
|
||||
'content' => $tmp->content,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($templates);
|
||||
}
|
||||
}
|
|
@ -1,243 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\AdminInvite;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use Purify;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use App\Services\EmailService;
|
||||
use App\Http\Controllers\Auth\RegisterController;
|
||||
|
||||
class AdminInviteController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
abort_if(!config('instance.admin_invites.enabled'), 404);
|
||||
}
|
||||
|
||||
public function index(Request $request, $code)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
return view('invite.admin_invite', compact('code'));
|
||||
}
|
||||
|
||||
public function apiVerifyCheck(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'token' => 'required',
|
||||
]);
|
||||
|
||||
$invite = AdminInvite::whereInviteCode($request->input('token'))->first();
|
||||
abort_if(!$invite, 404);
|
||||
abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
|
||||
abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
|
||||
$res = [
|
||||
'message' => $invite->message,
|
||||
'max_uses' => $invite->max_uses,
|
||||
'sev' => $invite->skip_email_verification
|
||||
];
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function apiUsernameCheck(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'token' => 'required',
|
||||
'username' => 'required'
|
||||
]);
|
||||
|
||||
$invite = AdminInvite::whereInviteCode($request->input('token'))->first();
|
||||
abort_if(!$invite, 404);
|
||||
abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
|
||||
abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
|
||||
|
||||
$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(($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.');
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
$rules = ['username' => $usernameRules];
|
||||
$validator = Validator::make($request->all(), $rules);
|
||||
|
||||
if($validator->fails()) {
|
||||
return response()->json($validator->errors(), 400);
|
||||
}
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
public function apiEmailCheck(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'token' => 'required',
|
||||
'email' => 'required'
|
||||
]);
|
||||
|
||||
$invite = AdminInvite::whereInviteCode($request->input('token'))->first();
|
||||
abort_if(!$invite, 404);
|
||||
abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
|
||||
abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
|
||||
|
||||
$emailRules = [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
$rules = ['email' => $emailRules];
|
||||
$validator = Validator::make($request->all(), $rules);
|
||||
|
||||
if($validator->fails()) {
|
||||
return response()->json($validator->errors(), 400);
|
||||
}
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
public function apiRegister(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'token' => 'required',
|
||||
'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.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'password' => 'required',
|
||||
'password_confirm' => 'required'
|
||||
]);
|
||||
|
||||
$invite = AdminInvite::whereInviteCode($request->input('token'))->firstOrFail();
|
||||
abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite expired');
|
||||
abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
|
||||
|
||||
$invite->uses = $invite->uses + 1;
|
||||
|
||||
event(new Registered($user = User::create([
|
||||
'name' => Purify::clean($request->input('name')) ?? $request->input('username'),
|
||||
'username' => $request->input('username'),
|
||||
'email' => $request->input('email'),
|
||||
'password' => Hash::make($request->input('password')),
|
||||
])));
|
||||
|
||||
sleep(5);
|
||||
|
||||
$invite->used_by = array_merge($invite->used_by ?? [], [[
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username
|
||||
]]);
|
||||
$invite->save();
|
||||
|
||||
if($invite->skip_email_verification) {
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
}
|
||||
|
||||
if(Auth::attempt([
|
||||
'email' => $request->input('email'),
|
||||
'password' => $request->input('password')
|
||||
])) {
|
||||
$request->session()->regenerate();
|
||||
return redirect()->intended('/');
|
||||
} else {
|
||||
return response()->json([], 400);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\AdminShadowFilter;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\AdminShadowFilterService;
|
||||
|
||||
class AdminShadowFilterController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(['auth','admin']);
|
||||
}
|
||||
|
||||
public function home(Request $request)
|
||||
{
|
||||
$filter = $request->input('filter');
|
||||
$searchQuery = $request->input('q');
|
||||
$filters = AdminShadowFilter::whereHas('profile')
|
||||
->when($filter, function($q, $filter) {
|
||||
if($filter == 'all') {
|
||||
return $q;
|
||||
} else if($filter == 'inactive') {
|
||||
return $q->whereActive(false);
|
||||
} else {
|
||||
return $q;
|
||||
}
|
||||
}, function($q, $filter) {
|
||||
return $q->whereActive(true);
|
||||
})
|
||||
->when($searchQuery, function($q, $searchQuery) {
|
||||
$ids = Profile::where('username', 'like', '%' . $searchQuery . '%')
|
||||
->limit(100)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
return $q->where('item_type', 'App\Profile')->whereIn('item_id', $ids);
|
||||
})
|
||||
->latest()
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.asf.home', compact('filters'));
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
return view('admin.asf.create');
|
||||
}
|
||||
|
||||
public function edit(Request $request, $id)
|
||||
{
|
||||
$filter = AdminShadowFilter::findOrFail($id);
|
||||
$profile = AccountService::get($filter->item_id);
|
||||
return view('admin.asf.edit', compact('filter', 'profile'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'username' => 'required',
|
||||
'active' => 'sometimes',
|
||||
'note' => 'sometimes',
|
||||
'hide_from_public_feeds' => 'sometimes'
|
||||
]);
|
||||
|
||||
$profile = Profile::whereUsername($request->input('username'))->first();
|
||||
|
||||
if(!$profile) {
|
||||
return back()->withErrors(['Invalid account']);
|
||||
}
|
||||
|
||||
if($profile->user && $profile->user->is_admin) {
|
||||
return back()->withErrors(['Cannot filter an admin account']);
|
||||
}
|
||||
|
||||
$active = $request->has('active') && $request->has('hide_from_public_feeds');
|
||||
|
||||
AdminShadowFilter::updateOrCreate([
|
||||
'item_id' => $profile->id,
|
||||
'item_type' => get_class($profile)
|
||||
], [
|
||||
'is_local' => $profile->domain === null,
|
||||
'note' => $request->input('note'),
|
||||
'hide_from_public_feeds' => $request->has('hide_from_public_feeds'),
|
||||
'admin_id' => $request->user()->profile_id,
|
||||
'active' => $active
|
||||
]);
|
||||
|
||||
AdminShadowFilterService::refresh();
|
||||
|
||||
return redirect('/i/admin/asf/home');
|
||||
}
|
||||
|
||||
public function storeEdit(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'active' => 'sometimes',
|
||||
'note' => 'sometimes',
|
||||
'hide_from_public_feeds' => 'sometimes'
|
||||
]);
|
||||
|
||||
$filter = AdminShadowFilter::findOrFail($id);
|
||||
|
||||
$profile = Profile::findOrFail($filter->item_id);
|
||||
|
||||
if($profile->user && $profile->user->is_admin) {
|
||||
return back()->withErrors(['Cannot filter an admin account']);
|
||||
}
|
||||
|
||||
$active = $request->has('active');
|
||||
$filter->active = $active;
|
||||
$filter->hide_from_public_feeds = $request->has('hide_from_public_feeds');
|
||||
$filter->note = $request->input('note');
|
||||
$filter->save();
|
||||
|
||||
AdminShadowFilterService::refresh();
|
||||
|
||||
return redirect('/i/admin/asf/home');
|
||||
}
|
||||
}
|
|
@ -5,849 +5,114 @@ namespace App\Http\Controllers\Api;
|
|||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use Auth, Cache, DB;
|
||||
use Auth, Cache;
|
||||
use Carbon\Carbon;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
Instance,
|
||||
Like,
|
||||
Notification,
|
||||
Media,
|
||||
Profile,
|
||||
Report,
|
||||
Status,
|
||||
User
|
||||
Status
|
||||
};
|
||||
use App\Models\Conversation;
|
||||
use App\Models\RemoteReport;
|
||||
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;
|
||||
|
||||
class AdminApiController extends Controller
|
||||
{
|
||||
public function supported(Request $request)
|
||||
public function __construct()
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
return response()->json(['supported' => true]);
|
||||
$this->middleware(['auth', 'admin']);
|
||||
}
|
||||
|
||||
public function getStats(Request $request)
|
||||
public function activity(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
$activity = [];
|
||||
|
||||
$limit = request()->input('limit', 20);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
$activity['captions'] = Status::select(
|
||||
'id',
|
||||
'caption',
|
||||
'rendered',
|
||||
'uri',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'scope',
|
||||
'created_at'
|
||||
)->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->orderByDesc('created_at')
|
||||
->paginate($limit);
|
||||
|
||||
$res = AdminStatsService::summary();
|
||||
$res['autospam_count'] = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->count();
|
||||
return $res;
|
||||
$activity['comments'] = Status::select(
|
||||
'id',
|
||||
'caption',
|
||||
'rendered',
|
||||
'uri',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'scope',
|
||||
'created_at'
|
||||
)->whereNotNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->orderByDesc('created_at')
|
||||
->paginate($limit);
|
||||
|
||||
return response()->json($activity, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function autospam(Request $request)
|
||||
public function moderateStatus(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$appeals = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->latest()
|
||||
->simplePaginate(6)
|
||||
->map(function($report) {
|
||||
$r = [
|
||||
'id' => $report->id,
|
||||
'type' => $report->type,
|
||||
'item_id' => $report->item_id,
|
||||
'item_type' => $report->item_type,
|
||||
'created_at' => $report->created_at
|
||||
];
|
||||
if($report->item_type === 'App\\Status') {
|
||||
$status = StatusService::get($report->item_id, false);
|
||||
if(!$status) {
|
||||
return;
|
||||
}
|
||||
|
||||
$r['status'] = $status;
|
||||
|
||||
if($status['in_reply_to_id']) {
|
||||
$r['parent'] = StatusService::get($status['in_reply_to_id'], false);
|
||||
}
|
||||
}
|
||||
return $r;
|
||||
});
|
||||
|
||||
return $appeals;
|
||||
}
|
||||
|
||||
public function autospamHandle(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
abort(400, 'Unpublished API');
|
||||
return;
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-post,delete-account',
|
||||
'id' => 'required'
|
||||
'type' => 'required|string|in:status,profile',
|
||||
'id' => 'required|integer|min:1',
|
||||
'action' => 'required|string|in:cw,unlink,unlist,suspend,delete'
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$type = $request->input('type');
|
||||
$id = $request->input('id');
|
||||
$appeal = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->findOrFail($id);
|
||||
$now = now();
|
||||
$res = ['status' => 'success'];
|
||||
$meta = json_decode($appeal->meta);
|
||||
$user = $appeal->user;
|
||||
$profile = $user->profile;
|
||||
|
||||
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('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'delete-post') {
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->is_spam = true;
|
||||
$appeal->save();
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($appeal->status->id)
|
||||
->objectType('App\Status::class')
|
||||
->user($request->user())
|
||||
->action('admin.status.delete')
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
StatusDelete::dispatch($appeal->status)->onQueue('high');
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'delete-account') {
|
||||
abort_if($user->is_admin, 400, 'Cannot delete an admin account.');
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->is_spam = true;
|
||||
$appeal->save();
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\User::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.delete')
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
DeleteAccountPipeline::dispatch($appeal->user)->onQueue('high');
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'dismiss-all') {
|
||||
AccountInterstitial::whereType('post.autospam')
|
||||
->whereItemType('App\Status')
|
||||
->whereNull('appeal_handled_at')
|
||||
->whereUserId($appeal->user_id)
|
||||
->update(['appeal_handled_at' => $now, 'is_spam' => true]);
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'approve') {
|
||||
$status = $appeal->status;
|
||||
$status->is_nsfw = $meta->is_nsfw;
|
||||
$status->scope = 'public';
|
||||
$status->visibility = 'public';
|
||||
$status->save();
|
||||
|
||||
$appeal->is_spam = false;
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
|
||||
StatusService::del($status->id);
|
||||
|
||||
Notification::whereAction('autospam.warning')
|
||||
->whereProfileId($appeal->user->profile_id)
|
||||
->get()
|
||||
->each(function($n) use($appeal) {
|
||||
NotificationService::del($appeal->user->profile_id, $n->id);
|
||||
$n->forceDelete();
|
||||
});
|
||||
|
||||
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') {
|
||||
AccountInterstitial::whereType('post.autospam')
|
||||
->whereItemType('App\Status')
|
||||
->whereNull('appeal_handled_at')
|
||||
->whereUserId($appeal->user_id)
|
||||
->get()
|
||||
->each(function($report) use($meta) {
|
||||
$report->is_spam = false;
|
||||
$report->appeal_handled_at = now();
|
||||
$report->save();
|
||||
$status = Status::find($report->item_id);
|
||||
if($status) {
|
||||
$status->is_nsfw = $meta->is_nsfw;
|
||||
$status->scope = 'public';
|
||||
$status->visibility = 'public';
|
||||
$status->save();
|
||||
StatusService::del($status->id, true);
|
||||
}
|
||||
|
||||
Notification::whereAction('autospam.warning')
|
||||
->whereProfileId($report->user->profile_id)
|
||||
->get()
|
||||
->each(function($n) use($report) {
|
||||
NotificationService::del($report->user->profile_id, $n->id);
|
||||
$n->forceDelete();
|
||||
});
|
||||
});
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function modReports(Request $request)
|
||||
{
|
||||
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')
|
||||
->paginate(6)
|
||||
->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
|
||||
];
|
||||
|
||||
if($report->profile_id) {
|
||||
$r['reported_by_account'] = AccountService::get($report->profile_id, true);
|
||||
}
|
||||
|
||||
if($report->object_type === 'App\\Status') {
|
||||
$status = StatusService::get($report->object_id, false);
|
||||
if(!$status) {
|
||||
return;
|
||||
}
|
||||
|
||||
$r['status'] = $status;
|
||||
|
||||
if($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);
|
||||
}
|
||||
return $r;
|
||||
})
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
return $reports;
|
||||
}
|
||||
|
||||
public function modReportHandle(Request $request)
|
||||
{
|
||||
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 = $request->input('action');
|
||||
$id = $request->input('id');
|
||||
|
||||
$actions = [
|
||||
'ignore',
|
||||
'cw',
|
||||
'unlist'
|
||||
];
|
||||
|
||||
if (!in_array($action, $actions)) {
|
||||
return abort(403);
|
||||
}
|
||||
|
||||
$report = Report::findOrFail($id);
|
||||
$item = $report->reported();
|
||||
$report->admin_seen = now();
|
||||
|
||||
switch ($action) {
|
||||
case 'ignore':
|
||||
$report->not_interested = true;
|
||||
break;
|
||||
|
||||
case 'cw':
|
||||
Cache::forget('status:thumb:'.$item->id);
|
||||
$item->is_nsfw = true;
|
||||
$item->save();
|
||||
$report->nsfw = true;
|
||||
StatusService::del($item->id, true);
|
||||
break;
|
||||
|
||||
case 'unlist':
|
||||
$item->visibility = 'unlisted';
|
||||
$item->save();
|
||||
StatusService::del($item->id, true);
|
||||
break;
|
||||
|
||||
default:
|
||||
$report->admin_seen = null;
|
||||
break;
|
||||
}
|
||||
|
||||
$report->save();
|
||||
Cache::forget('admin-dash:reports:list-cache');
|
||||
Cache::forget('admin:dashboard:home:data:v0:15min');
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
public function getConfiguration(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
abort_unless(config('instance.enable_cc'), 400);
|
||||
|
||||
return collect([
|
||||
[
|
||||
'name' => 'ActivityPub Federation',
|
||||
'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
|
||||
'key' => 'federation.activitypub.enabled'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Open Registration',
|
||||
'description' => 'Allow new account registrations.',
|
||||
'key' => 'pixelfed.open_registration'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Stories',
|
||||
'description' => 'Enable the ephemeral Stories feature.',
|
||||
'key' => 'instance.stories.enabled'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Require Email Verification',
|
||||
'description' => 'Require new accounts to verify their email address.',
|
||||
'key' => 'pixelfed.enforce_email_verification'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'AutoSpam Detection',
|
||||
'description' => 'Detect and remove spam from public timelines.',
|
||||
'key' => 'pixelfed.bouncer.enabled'
|
||||
],
|
||||
])
|
||||
->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_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
abort_unless(config('instance.enable_cc'), 400);
|
||||
|
||||
$this->validate($request, [
|
||||
'key' => 'required',
|
||||
'value' => 'required'
|
||||
]);
|
||||
|
||||
$allowedKeys = [
|
||||
'federation.activitypub.enabled',
|
||||
'pixelfed.open_registration',
|
||||
'instance.stories.enabled',
|
||||
'pixelfed.enforce_email_verification',
|
||||
'pixelfed.bouncer.enabled',
|
||||
];
|
||||
|
||||
$key = $request->input('key');
|
||||
$value = (bool) filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
||||
abort_if(!in_array($key, $allowedKeys), 400, 'Invalid cache key.');
|
||||
|
||||
ConfigCacheService::put($key, $value);
|
||||
|
||||
return collect([
|
||||
[
|
||||
'name' => 'ActivityPub Federation',
|
||||
'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
|
||||
'key' => 'federation.activitypub.enabled'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Open Registration',
|
||||
'description' => 'Allow new account registrations.',
|
||||
'key' => 'pixelfed.open_registration'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Stories',
|
||||
'description' => 'Enable the ephemeral Stories feature.',
|
||||
'key' => 'instance.stories.enabled'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Require Email Verification',
|
||||
'description' => 'Require new accounts to verify their email address.',
|
||||
'key' => 'pixelfed.enforce_email_verification'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'AutoSpam Detection',
|
||||
'description' => 'Detect and remove spam from public timelines.',
|
||||
'key' => 'pixelfed.bouncer.enabled'
|
||||
],
|
||||
])
|
||||
->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_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'sort' => 'sometimes|in:asc,desc',
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
|
||||
$res = User::whereNull('status')
|
||||
->when($q, function($query, $q) {
|
||||
return $query->where('username', 'like', '%' . $q . '%');
|
||||
})
|
||||
->orderBy('id', $sort)
|
||||
->cursorPaginate(10);
|
||||
return AdminUser::collection($res);
|
||||
}
|
||||
|
||||
public function getUser(Request $request)
|
||||
{
|
||||
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')) {
|
||||
Cache::forget($key);
|
||||
}
|
||||
return Cache::remember($key, 86400, function() use($id) {
|
||||
$user = User::findOrFail($id);
|
||||
$profile = $user->profile;
|
||||
$account = AccountService::get($user->profile_id, true);
|
||||
$res = (new AdminUser($user))->additional(['meta' => [
|
||||
'cached_at' => str_replace('+00:00', 'Z', now()->format(DATE_RFC3339_EXTENDED)),
|
||||
'account' => $account,
|
||||
'dms_sent' => Conversation::whereFromId($profile->id)->count(),
|
||||
'report_count' => Report::where('object_id', $profile->id)->orWhere('reported_profile_id', $profile->id)->count(),
|
||||
'remote_report_count' => RemoteReport::whereAccountId($profile->id)->count(),
|
||||
'moderation' => [
|
||||
'unlisted' => (bool) $profile->unlisted,
|
||||
'cw' => (bool) $profile->cw,
|
||||
'no_autolink' => (bool) $profile->no_autolink
|
||||
]
|
||||
]]);
|
||||
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public function userAdminAction(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email,delete',
|
||||
'value' => 'sometimes'
|
||||
]);
|
||||
|
||||
$id = $request->input('id');
|
||||
$user = User::findOrFail($id);
|
||||
$profile = Profile::findOrFail($user->profile_id);
|
||||
$action = $request->input('action');
|
||||
|
||||
abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts');
|
||||
|
||||
if($action === 'delete') {
|
||||
if(config('pixelfed.account_deletion') == false) {
|
||||
abort(404);
|
||||
if ($type == 'status') {
|
||||
$status = Status::findOrFail($id);
|
||||
switch ($action) {
|
||||
case 'cw':
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
break;
|
||||
case 'unlink':
|
||||
$status->rendered = $status->caption;
|
||||
$status->save();
|
||||
break;
|
||||
case 'unlist':
|
||||
$status->scope = 'unlisted';
|
||||
$status->visibility = 'unlisted';
|
||||
$status->save();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if ($type == 'profile') {
|
||||
$profile = Profile::findOrFail($id);
|
||||
switch ($action) {
|
||||
|
||||
abort_if($user->is_admin, 400, 'Cannot delete an admin account.');
|
||||
|
||||
$ts = now()->addMonth();
|
||||
|
||||
$user->status = 'delete';
|
||||
$user->delete_after = $ts;
|
||||
$user->save();
|
||||
|
||||
$profile->status = 'delete';
|
||||
$profile->delete_after = $ts;
|
||||
$profile->save();
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.delete')
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
NetworkTimelineService::deleteByProfileId($profile->id);
|
||||
|
||||
if($profile->user_id) {
|
||||
DB::table('oauth_access_tokens')->whereUserId($user->id)->delete();
|
||||
DB::table('oauth_auth_codes')->whereUserId($user->id)->delete();
|
||||
$user->email = $user->id;
|
||||
$user->password = '';
|
||||
$user->status = 'delete';
|
||||
$user->save();
|
||||
$profile->status = 'delete';
|
||||
$profile->delete_after = now()->addMonth();
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
DeleteAccountPipeline::dispatch($user)->onQueue('high');
|
||||
} else {
|
||||
$profile->status = 'delete';
|
||||
$profile->delete_after = now()->addMonth();
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
|
||||
case 'delete':
|
||||
StatusDelete::dispatch($status);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return [
|
||||
'status' => 200,
|
||||
'msg' => 'deleted',
|
||||
];
|
||||
} else if($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)
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('scope', ['public', 'unlisted', 'private'])
|
||||
->count();
|
||||
$profile->status_count = $statusCount;
|
||||
$profile->save();
|
||||
} else if($action === 'verify_email') {
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($user->id)
|
||||
->objectId($user->id)
|
||||
->objectType('App\User::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => 'Manually verified email address',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
} else if($action === 'unlisted') {
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
$profile->unlisted = !$profile->unlisted;
|
||||
$profile->save();
|
||||
} else if($action === 'cw') {
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
$profile->cw = !$profile->cw;
|
||||
$profile->save();
|
||||
} else if($action === 'no_autolink') {
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
$profile->no_autolink = !$profile->no_autolink;
|
||||
$profile->save();
|
||||
} else {
|
||||
$profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
||||
$profile->save();
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($user->id)
|
||||
->objectId($user->id)
|
||||
->objectType('App\User::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
}
|
||||
|
||||
AccountService::del($user->profile_id);
|
||||
$account = AccountService::get($user->profile_id, true);
|
||||
|
||||
return (new AdminUser($user))->additional(['meta' => [
|
||||
'account' => $account,
|
||||
'moderation' => [
|
||||
'unlisted' => (bool) $profile->unlisted,
|
||||
'cw' => (bool) $profile->cw,
|
||||
'no_autolink' => (bool) $profile->no_autolink
|
||||
]
|
||||
]]);
|
||||
}
|
||||
|
||||
public function instances(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'sometimes',
|
||||
'sort' => 'sometimes|in:asc,desc',
|
||||
'sort_by' => 'sometimes|in:id,status_count,user_count,domain',
|
||||
'filter' => 'sometimes|in:all,unlisted,auto_cw,banned',
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
$sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
|
||||
$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') {
|
||||
return $query;
|
||||
} else {
|
||||
return $query->where($filter, true);
|
||||
}
|
||||
})
|
||||
->when($sortBy, function($query, $sortBy) use($sort) {
|
||||
return $query->orderBy($sortBy, $sort);
|
||||
}, function($query) {
|
||||
return $query->orderBy('id', 'desc');
|
||||
})
|
||||
->cursorPaginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return AdminInstance::collection($res);
|
||||
}
|
||||
|
||||
public function getInstance(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$id = $request->input('id');
|
||||
$res = Instance::findOrFail($id);
|
||||
|
||||
return new AdminInstance($res);
|
||||
}
|
||||
|
||||
public function moderateInstance(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'key' => 'required|in:unlisted,auto_cw,banned',
|
||||
'value' => 'required'
|
||||
]);
|
||||
|
||||
$id = $request->input('id');
|
||||
$key = $request->input('key');
|
||||
$value = (bool) filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
||||
$res = Instance::findOrFail($id);
|
||||
$res->{$key} = $value;
|
||||
$res->save();
|
||||
|
||||
InstanceService::refresh();
|
||||
NetworkTimelineService::warmCache(true);
|
||||
|
||||
return new AdminInstance($res);
|
||||
}
|
||||
|
||||
public function refreshInstanceStats(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
]);
|
||||
|
||||
$id = $request->input('id');
|
||||
$instance = Instance::findOrFail($id);
|
||||
$instance->user_count = Profile::whereDomain($instance->domain)->count();
|
||||
$instance->status_count = Profile::whereDomain($instance->domain)->leftJoin('statuses', 'profiles.id', '=', 'statuses.profile_id')->count();
|
||||
$instance->save();
|
||||
|
||||
return new AdminInstance($instance);
|
||||
}
|
||||
|
||||
public function getAllStats(Request $request)
|
||||
{
|
||||
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')) {
|
||||
Cache::forget('admin-api:instance-all-stats-v1');
|
||||
}
|
||||
|
||||
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) {
|
||||
$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()
|
||||
];
|
||||
|
||||
$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()
|
||||
];
|
||||
|
||||
$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()
|
||||
];
|
||||
}
|
||||
|
||||
$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['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');
|
||||
$res['posts']['change'] = collect($res['posts']['days'])->sum('count');
|
||||
$res['instances']['total'] = DB::table('instances')->count();
|
||||
$res['instances']['min'] = collect($res['instances']['days'])->min('count');
|
||||
$res['instances']['max'] = collect($res['instances']['days'])->max('count');
|
||||
$res['instances']['change'] = collect($res['instances']['days'])->sum('count');
|
||||
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,940 +0,0 @@
|
|||
<?php
|
||||
|
||||
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\Place;
|
||||
use App\Status;
|
||||
use App\Report;
|
||||
use App\Profile;
|
||||
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\RestrictedNames;
|
||||
use App\Services\BouncerService;
|
||||
use App\Services\EmailService;
|
||||
use Illuminate\Support\Str;
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
$uid = $request->user()->id;
|
||||
$settings = UserSetting::firstOrCreate([
|
||||
'user_id' => $uid
|
||||
]);
|
||||
if(!$settings->other) {
|
||||
return [];
|
||||
}
|
||||
return $settings->other;
|
||||
}
|
||||
|
||||
public function setWebSettings(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'field' => 'required|in:enable_reblogs,hide_reblog_banner',
|
||||
'value' => 'required'
|
||||
]);
|
||||
$field = $request->input('field');
|
||||
$value = $request->input('value');
|
||||
$settings = UserSetting::firstOrCreate([
|
||||
'user_id' => $request->user()->id
|
||||
]);
|
||||
if(!$settings->other) {
|
||||
$other = [];
|
||||
} else {
|
||||
$other = $settings->other;
|
||||
}
|
||||
$other[$field] = $value;
|
||||
$settings->other = $other;
|
||||
$settings->save();
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function getMutualAccounts(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('follows'), 403);
|
||||
|
||||
$account = AccountService::get($id, true);
|
||||
if(!$account || !isset($account['id'])) { return []; }
|
||||
$res = collect(FollowerService::mutualAccounts($request->user()->profile_id, $id))
|
||||
->map(function($accountId) {
|
||||
return AccountService::get($accountId, true);
|
||||
})
|
||||
->filter()
|
||||
->take(24)
|
||||
->values();
|
||||
return $this->json($res);
|
||||
}
|
||||
}
|
|
@ -1,339 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
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\Util\Media\Filter;
|
||||
use App\Jobs\MediaPipeline\MediaDeletePipeline;
|
||||
use App\Jobs\VideoPipeline\{
|
||||
VideoOptimize,
|
||||
VideoPostProcess,
|
||||
VideoThumbnail
|
||||
};
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
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";
|
||||
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
{
|
||||
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function instance(Request $request)
|
||||
{
|
||||
$contact = Cache::remember('api:v1:instance-data:contact', 604800, function () {
|
||||
if(config_cache('instance.admin.pid')) {
|
||||
return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
|
||||
}
|
||||
$admin = User::whereIsAdmin(true)->first();
|
||||
return $admin && isset($admin->profile_id) ?
|
||||
AccountService::getMastodon($admin->profile_id, true) :
|
||||
null;
|
||||
});
|
||||
|
||||
$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() : [];
|
||||
});
|
||||
|
||||
$res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use($contact, $rules) {
|
||||
return [
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'title' => config_cache('app.name'),
|
||||
'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')',
|
||||
'source_url' => 'https://github.com/pixelfed/pixelfed',
|
||||
'description' => config_cache('app.short_description'),
|
||||
'usage' => [
|
||||
'users' => [
|
||||
'active_month' => (int) Nodeinfo::activeUsersMonthly()
|
||||
]
|
||||
],
|
||||
'thumbnail' => [
|
||||
'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'blurhash' => InstanceService::headerBlurhash(),
|
||||
'versions' => [
|
||||
'@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
|
||||
]
|
||||
],
|
||||
'languages' => [config('app.locale')],
|
||||
'configuration' => [
|
||||
'urls' => [
|
||||
'streaming' => null,
|
||||
'status' => null
|
||||
],
|
||||
'vapid' => [
|
||||
'public_key' => config('webpush.vapid.public_key'),
|
||||
],
|
||||
'accounts' => [
|
||||
'max_featured_tags' => 0,
|
||||
],
|
||||
'statuses' => [
|
||||
'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
|
||||
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
|
||||
'characters_reserved_per_url' => 23
|
||||
],
|
||||
'media_attachments' => [
|
||||
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
|
||||
'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'image_matrix_limit' => 3686400,
|
||||
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'video_frame_rate_limit' => 240,
|
||||
'video_matrix_limit' => 3686400
|
||||
],
|
||||
'polls' => [
|
||||
'max_options' => 0,
|
||||
'max_characters_per_option' => 0,
|
||||
'min_expiration' => 0,
|
||||
'max_expiration' => 0,
|
||||
],
|
||||
'translation' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
],
|
||||
'registrations' => [
|
||||
'enabled' => null,
|
||||
'approval_required' => false,
|
||||
'message' => null,
|
||||
'url' => null,
|
||||
],
|
||||
'contact' => [
|
||||
'email' => config('instance.email'),
|
||||
'account' => $contact
|
||||
],
|
||||
'rules' => $rules
|
||||
];
|
||||
});
|
||||
|
||||
$res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled');
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v2/search
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function search(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1|max:100',
|
||||
'account_id' => 'nullable|string',
|
||||
'max_id' => 'nullable|string',
|
||||
'min_id' => 'nullable|string',
|
||||
'type' => 'nullable|in:accounts,hashtags,statuses',
|
||||
'exclude_unreviewed' => 'nullable',
|
||||
'resolve' => 'nullable',
|
||||
'limit' => 'nullable|integer|max:40',
|
||||
'offset' => 'nullable|integer',
|
||||
'following' => 'nullable'
|
||||
]);
|
||||
|
||||
if($request->user()->has_roles && !UserRoleService::can('can-view-discover', $request->user()->id)) {
|
||||
return [
|
||||
'accounts' => [],
|
||||
'hashtags' => [],
|
||||
'statuses' => []
|
||||
];
|
||||
}
|
||||
|
||||
$mastodonMode = !$request->has('_pe');
|
||||
return $this->json(SearchApiV2Service::query($request, $mastodonMode));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v2/streaming/config
|
||||
*
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getWebsocketConfig()
|
||||
{
|
||||
return config('broadcasting.default') === 'pusher' ? [
|
||||
'host' => config('broadcasting.connections.pusher.options.host'),
|
||||
'port' => config('broadcasting.connections.pusher.options.port'),
|
||||
'key' => config('broadcasting.connections.pusher.key'),
|
||||
'cluster' => config('broadcasting.connections.pusher.options.cluster')
|
||||
] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v2/media
|
||||
*
|
||||
*
|
||||
* @return MediaTransformer
|
||||
*/
|
||||
public function mediaUploadV2(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'file.*' => [
|
||||
'required_without:file',
|
||||
'mimetypes:' . config_cache('pixelfed.media_types'),
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
],
|
||||
'file' => [
|
||||
'required_without:file.*',
|
||||
'mimetypes:' . config_cache('pixelfed.media_types'),
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
],
|
||||
'filter_name' => 'nullable|string|max:24',
|
||||
'filter_class' => 'nullable|alpha_dash|max:24',
|
||||
'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'),
|
||||
'replace_id' => 'sometimes'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if($user->last_active_at == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if(empty($request->file('file'))) {
|
||||
return response('', 422);
|
||||
}
|
||||
|
||||
$limitKey = 'compose:rate-limit:media-upload:' . $user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
|
||||
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
|
||||
|
||||
return $dailyLimit >= 1250;
|
||||
});
|
||||
abort_if($limitReached == true, 429);
|
||||
|
||||
$profile = $user->profile;
|
||||
|
||||
if(config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
|
||||
return Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
});
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
||||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
}
|
||||
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
$license = null;
|
||||
$mime = $photo->getMimeType();
|
||||
|
||||
$settings = UserSetting::whereUserId($user->id)->first();
|
||||
|
||||
if($settings && !empty($settings->compose_settings)) {
|
||||
$compose = $settings->compose_settings;
|
||||
|
||||
if(isset($compose['default_license']) && $compose['default_license'] != 1) {
|
||||
$license = $compose['default_license'];
|
||||
}
|
||||
}
|
||||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
||||
if($request->has('replace_id')) {
|
||||
$rpid = $request->input('replace_id');
|
||||
$removeMedia = Media::whereNull('status_id')
|
||||
->whereUserId($user->id)
|
||||
->whereProfileId($profile->id)
|
||||
->where('created_at', '>', now()->subHours(2))
|
||||
->find($rpid);
|
||||
if($removeMedia) {
|
||||
MediaDeletePipeline::dispatch($removeMedia)
|
||||
->onQueue('mmo')
|
||||
->delay(now()->addMinutes(15));
|
||||
}
|
||||
}
|
||||
|
||||
$media = new Media();
|
||||
$media->status_id = null;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $mime;
|
||||
$media->caption = $request->input('description');
|
||||
$media->filter_class = $filterClass;
|
||||
$media->filter_name = $filterName;
|
||||
if($license) {
|
||||
$media->license = $license;
|
||||
}
|
||||
$media->save();
|
||||
|
||||
switch ($media->mime) {
|
||||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
ImageOptimize::dispatch($media)->onQueue('mmo');
|
||||
break;
|
||||
|
||||
case 'video/mp4':
|
||||
VideoThumbnail::dispatch($media)->onQueue('mmo');
|
||||
$preview_url = '/storage/no-preview.png';
|
||||
$url = '/storage/no-preview.png';
|
||||
break;
|
||||
}
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
$res['preview_url'] = $media->url(). '?v=' . time();
|
||||
$res['url'] = null;
|
||||
return $this->json($res, 202);
|
||||
}
|
||||
}
|
|
@ -90,18 +90,100 @@ class BaseApiController extends Controller
|
|||
|
||||
if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
|
||||
Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
|
||||
NotificationService::warmCache($pid, 100, true);
|
||||
NotificationService::warmCache($pid, 400, true);
|
||||
}
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accounts(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$profile = Profile::findOrFail($id);
|
||||
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountFollowers(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$profile = Profile::findOrFail($id);
|
||||
$followers = $profile->followers;
|
||||
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountFollowing(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$profile = Profile::findOrFail($id);
|
||||
$following = $profile->following;
|
||||
$resource = new Fractal\Resource\Collection($following, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountStatuses(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'only_media' => 'nullable',
|
||||
'pinned' => 'nullable',
|
||||
'exclude_replies' => 'nullable',
|
||||
'max_id' => 'nullable|integer|min:1',
|
||||
'since_id' => 'nullable|integer|min:1',
|
||||
'min_id' => 'nullable|integer|min:1',
|
||||
'limit' => 'nullable|integer|min:1|max:24'
|
||||
]);
|
||||
$limit = $request->limit ?? 20;
|
||||
$max_id = $request->max_id ?? false;
|
||||
$min_id = $request->min_id ?? false;
|
||||
$since_id = $request->since_id ?? false;
|
||||
$only_media = $request->only_media ?? false;
|
||||
$user = Auth::user();
|
||||
$account = Profile::whereNull('status')->findOrFail($id);
|
||||
$statuses = $account->statuses()->getQuery();
|
||||
if($only_media == true) {
|
||||
$statuses = $statuses
|
||||
->whereIn('scope', ['public','unlisted'])
|
||||
->whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id');
|
||||
}
|
||||
if($id == $account->id && !$max_id && !$min_id && !$since_id) {
|
||||
$statuses = $statuses->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
} else if($since_id) {
|
||||
$statuses = $statuses->where('id', '>', $since_id)
|
||||
->orderBy('id', 'DESC')
|
||||
->paginate($limit);
|
||||
} else if($min_id) {
|
||||
$statuses = $statuses->where('id', '>', $min_id)
|
||||
->orderBy('id', 'ASC')
|
||||
->paginate($limit);
|
||||
} else if($max_id) {
|
||||
$statuses = $statuses->where('id', '<', $max_id)
|
||||
->orderBy('id', 'DESC')
|
||||
->paginate($limit);
|
||||
} else {
|
||||
$statuses = $statuses->whereScope('public')->orderBy('id', 'desc')->paginate($limit);
|
||||
}
|
||||
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function avatarUpdate(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'),
|
||||
'upload' => 'required|mimes:jpeg,png,gif|max:'.config('pixelfed.max_avatar_size'),
|
||||
]);
|
||||
|
||||
try {
|
||||
|
@ -113,7 +195,7 @@ class BaseApiController extends Controller
|
|||
$name = $path['name'];
|
||||
$public = $path['storage'];
|
||||
$currentAvatar = storage_path('app/'.$profile->avatar->media_path);
|
||||
$loc = $request->file('upload')->storePubliclyAs($public, $name);
|
||||
$loc = $request->file('upload')->storeAs($public, $name);
|
||||
|
||||
$avatar = Avatar::whereProfileId($profile->id)->firstOrFail();
|
||||
$opath = $avatar->media_path;
|
||||
|
@ -133,12 +215,26 @@ class BaseApiController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
public function showTempMedia(Request $request, $profileId, $mediaId, $timestamp)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
}
|
||||
|
||||
public function uploadMedia(Request $request)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
}
|
||||
|
||||
public function deleteMedia(Request $request)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
}
|
||||
|
||||
public function verifyCredentials(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$user = $request->user();
|
||||
if ($user->status != null) {
|
||||
abort_if(!$user, 403);
|
||||
if($user->status != null) {
|
||||
Auth::logout();
|
||||
abort(403);
|
||||
}
|
||||
|
@ -146,32 +242,44 @@ class BaseApiController extends Controller
|
|||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountLikes(Request $request)
|
||||
public function drafts(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'page' => 'sometimes|int|min:1|max:20',
|
||||
'limit' => 'sometimes|int|min:1|max:10'
|
||||
]);
|
||||
$medias = Media::whereUserId($user->id)
|
||||
->whereNull('status_id')
|
||||
->latest()
|
||||
->take(13)
|
||||
->get();
|
||||
$resource = new Fractal\Resource\Collection($medias, new MediaDraftTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function accountLikes(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
$limit = $request->input('limit', 10);
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$res = \DB::table('likes')
|
||||
->whereProfileId($user->profile_id)
|
||||
->latest()
|
||||
->simplePaginate($limit)
|
||||
->map(function($id) {
|
||||
$status = StatusService::get($id->status_id, false);
|
||||
$status['favourited'] = true;
|
||||
return $status;
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
return response()->json($res);
|
||||
$limit = 10;
|
||||
$page = (int) $request->input('page', 1);
|
||||
|
||||
if($page > 20) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$favourites = $user->profile->likes()
|
||||
->latest()
|
||||
->simplePaginate($limit)
|
||||
->pluck('status_id');
|
||||
|
||||
$statuses = Status::find($favourites)->reverse();
|
||||
|
||||
$resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function archive(Request $request, $id)
|
||||
|
|
|
@ -19,7 +19,7 @@ class InstanceApiController extends Controller {
|
|||
'acct' => $admin->username,
|
||||
'display_name' => e($admin->name),
|
||||
'locked' => (bool) $admin->is_private,
|
||||
'created_at' => str_replace('+00:00', 'Z', $admin->created_at->format(DATE_RFC3339_EXTENDED)),
|
||||
'created_at' => $admin->created_at->format('c'),
|
||||
'note' => e($admin->bio),
|
||||
'url' => $admin->url(),
|
||||
'avatar' => $admin->avatarUrl(),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue