mirror of
https://github.com/YunoHost-Apps/pixelfed_ynh.git
synced 2024-09-03 20:06:04 +02:00
Merge remote-tracking branch 'upstream/staging'
This commit is contained in:
commit
43a76c9b00
1943 changed files with 1023274 additions and 0 deletions
54
.circleci/config.yml
Normal file
54
.circleci/config.yml
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# PHP CircleCI 2.0 configuration file
|
||||||
|
#
|
||||||
|
# Check https://circleci.com/docs/2.0/language-php/ for more details
|
||||||
|
#
|
||||||
|
version: 2
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
docker:
|
||||||
|
# Specify the version you desire here
|
||||||
|
- image: cimg/php:8.1.12
|
||||||
|
|
||||||
|
# Specify service dependencies here if necessary
|
||||||
|
# CircleCI maintains a library of pre-built images
|
||||||
|
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||||
|
# Using the RAM variation mitigates I/O contention
|
||||||
|
# for database intensive operations.
|
||||||
|
# - image: circleci/mysql:5.7-ram
|
||||||
|
#
|
||||||
|
# - image: redis:2.8.19
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
|
||||||
|
- run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
|
||||||
|
|
||||||
|
# Download and cache dependencies
|
||||||
|
|
||||||
|
# composer cache
|
||||||
|
- restore_cache:
|
||||||
|
keys:
|
||||||
|
# "composer.lock" can be used if it is committed to the repo
|
||||||
|
- v1-dependencies-{{ checksum "composer.json" }}
|
||||||
|
# fallback to using the latest cache if no exact match is found
|
||||||
|
- v1-dependencies-
|
||||||
|
|
||||||
|
- run: composer install -n --prefer-dist
|
||||||
|
|
||||||
|
- save_cache:
|
||||||
|
key: composer-v1-{{ checksum "composer.lock" }}
|
||||||
|
paths:
|
||||||
|
- vendor
|
||||||
|
|
||||||
|
- 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: ./vendor/bin/phpunit
|
||||||
|
- store_test_results:
|
||||||
|
path: tests/_output
|
||||||
|
- store_artifacts:
|
||||||
|
path: tests/_output
|
7
.ddev/commands/redis/redis-cli
Executable file
7
.ddev/commands/redis/redis-cli
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/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"
|
||||||
|
|
||||||
|
redis-cli -p 6379 -h redis $@
|
32
.ddev/config.yaml
Normal file
32
.ddev/config.yaml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
14
.ddev/docker-compose.redis.yaml
Normal file
14
.ddev/docker-compose.redis.yaml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#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"]
|
8
.ddev/redis/redis.conf
Normal file
8
.ddev/redis/redis.conf
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# 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
|
8
.dockerignore
Normal file
8
.dockerignore
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
data
|
||||||
|
Dockerfile
|
||||||
|
contrib/docker/Dockerfile.*
|
||||||
|
docker-compose*.yml
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env
|
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = tab
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
149
.env.docker
Normal file
149
.env.docker
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
## Crypto
|
||||||
|
APP_KEY=
|
||||||
|
|
||||||
|
## General Settings
|
||||||
|
APP_NAME="Pixelfed Prod"
|
||||||
|
APP_ENV=production
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_URL=https://real.domain
|
||||||
|
APP_DOMAIN="real.domain"
|
||||||
|
ADMIN_DOMAIN="real.domain"
|
||||||
|
SESSION_DOMAIN="real.domain"
|
||||||
|
|
||||||
|
OPEN_REGISTRATION=true
|
||||||
|
ENFORCE_EMAIL_VERIFICATION=false
|
||||||
|
PF_MAX_USERS=1000
|
||||||
|
OAUTH_ENABLED=true
|
||||||
|
|
||||||
|
APP_TIMEZONE=UTC
|
||||||
|
APP_LOCALE=en
|
||||||
|
|
||||||
|
## Pixelfed Tweaks
|
||||||
|
LIMIT_ACCOUNT_SIZE=true
|
||||||
|
MAX_ACCOUNT_SIZE=1000000
|
||||||
|
MAX_PHOTO_SIZE=15000
|
||||||
|
MAX_AVATAR_SIZE=2000
|
||||||
|
MAX_CAPTION_LENGTH=500
|
||||||
|
MAX_BIO_LENGTH=125
|
||||||
|
MAX_NAME_LENGTH=30
|
||||||
|
MAX_ALBUM_LENGTH=4
|
||||||
|
IMAGE_QUALITY=80
|
||||||
|
PF_OPTIMIZE_IMAGES=true
|
||||||
|
PF_OPTIMIZE_VIDEOS=true
|
||||||
|
ADMIN_ENV_EDITOR=false
|
||||||
|
ACCOUNT_DELETION=true
|
||||||
|
ACCOUNT_DELETE_AFTER=false
|
||||||
|
MAX_LINKS_PER_POST=0
|
||||||
|
|
||||||
|
## Instance
|
||||||
|
#INSTANCE_DESCRIPTION=
|
||||||
|
INSTANCE_PUBLIC_HASHTAGS=false
|
||||||
|
#INSTANCE_CONTACT_EMAIL=
|
||||||
|
INSTANCE_PUBLIC_LOCAL_TIMELINE=false
|
||||||
|
#BANNED_USERNAMES=
|
||||||
|
STORIES_ENABLED=false
|
||||||
|
RESTRICTED_INSTANCE=false
|
||||||
|
|
||||||
|
## Mail
|
||||||
|
MAIL_DRIVER=log
|
||||||
|
MAIL_HOST=smtp.mailtrap.io
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_FROM_ADDRESS="pixelfed@example.com"
|
||||||
|
MAIL_FROM_NAME="Pixelfed"
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_ENCRYPTION=null
|
||||||
|
|
||||||
|
## Databases (MySQL)
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_DATABASE=pixelfed_prod
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PASSWORD=pixelfed_db_pass
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USERNAME=pixelfed
|
||||||
|
# pass the same values to the db itself
|
||||||
|
MYSQL_DATABASE=pixelfed_prod
|
||||||
|
MYSQL_PASSWORD=pixelfed_db_pass
|
||||||
|
MYSQL_RANDOM_ROOT_PASSWORD=true
|
||||||
|
MYSQL_USER=pixelfed
|
||||||
|
|
||||||
|
## Databases (Postgres)
|
||||||
|
#DB_CONNECTION=pgsql
|
||||||
|
#DB_HOST=postgres
|
||||||
|
#DB_PORT=5432
|
||||||
|
#DB_DATABASE=pixelfed
|
||||||
|
#DB_USERNAME=postgres
|
||||||
|
#DB_PASSWORD=postgres
|
||||||
|
|
||||||
|
## Cache (Redis)
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_SCHEME=tcp
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PASSWORD=redis_password
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_DATABASE=0
|
||||||
|
|
||||||
|
HORIZON_PREFIX="horizon-"
|
||||||
|
|
||||||
|
## EXPERIMENTS
|
||||||
|
EXP_LC=false
|
||||||
|
EXP_REC=false
|
||||||
|
EXP_LOOPS=false
|
||||||
|
|
||||||
|
## ActivityPub Federation
|
||||||
|
ACTIVITY_PUB=false
|
||||||
|
AP_REMOTE_FOLLOW=false
|
||||||
|
AP_SHAREDINBOX=false
|
||||||
|
AP_INBOX=false
|
||||||
|
AP_OUTBOX=false
|
||||||
|
ATOM_FEEDS=true
|
||||||
|
NODEINFO=true
|
||||||
|
WEBFINGER=true
|
||||||
|
|
||||||
|
## S3
|
||||||
|
FILESYSTEM_CLOUD=s3
|
||||||
|
PF_ENABLE_CLOUD=false
|
||||||
|
#AWS_ACCESS_KEY_ID=
|
||||||
|
#AWS_SECRET_ACCESS_KEY=
|
||||||
|
#AWS_DEFAULT_REGION=
|
||||||
|
#AWS_BUCKET=
|
||||||
|
#AWS_URL=
|
||||||
|
#AWS_ENDPOINT=
|
||||||
|
#AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
## Horizon
|
||||||
|
HORIZON_DARKMODE=false
|
||||||
|
|
||||||
|
## COSTAR - Confirm Object Sentiment Transform and Reduce
|
||||||
|
PF_COSTAR_ENABLED=false
|
||||||
|
|
||||||
|
# Media
|
||||||
|
MEDIA_EXIF_DATABASE=false
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
LOG_CHANNEL=stderr
|
||||||
|
|
||||||
|
## Image
|
||||||
|
IMAGE_DRIVER=imagick
|
||||||
|
|
||||||
|
## Broadcasting: log driver for local development
|
||||||
|
BROADCAST_DRIVER=log
|
||||||
|
|
||||||
|
## Cache
|
||||||
|
CACHE_DRIVER=redis
|
||||||
|
|
||||||
|
## Purify
|
||||||
|
RESTRICT_HTML_TYPES=true
|
||||||
|
|
||||||
|
## Queue
|
||||||
|
QUEUE_DRIVER=redis
|
||||||
|
|
||||||
|
## Session
|
||||||
|
SESSION_DRIVER=redis
|
||||||
|
|
||||||
|
## Trusted Proxy
|
||||||
|
TRUST_PROXIES="*"
|
||||||
|
|
||||||
|
## Passport
|
||||||
|
#PASSPORT_PRIVATE_KEY=
|
||||||
|
#PASSPORT_PUBLIC_KEY=
|
78
.env.example
Normal file
78
.env.example
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
APP_NAME="Pixelfed"
|
||||||
|
APP_ENV="production"
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG="false"
|
||||||
|
|
||||||
|
# Instance Configuration
|
||||||
|
OPEN_REGISTRATION="false"
|
||||||
|
ENFORCE_EMAIL_VERIFICATION="false"
|
||||||
|
PF_MAX_USERS="1000"
|
||||||
|
OAUTH_ENABLED="true"
|
||||||
|
|
||||||
|
# Media Configuration
|
||||||
|
PF_OPTIMIZE_IMAGES="true"
|
||||||
|
IMAGE_QUALITY="80"
|
||||||
|
MAX_PHOTO_SIZE="15000"
|
||||||
|
MAX_CAPTION_LENGTH="500"
|
||||||
|
MAX_ALBUM_LENGTH="4"
|
||||||
|
|
||||||
|
# Instance URL Configuration
|
||||||
|
APP_URL="http://localhost"
|
||||||
|
APP_DOMAIN="localhost"
|
||||||
|
ADMIN_DOMAIN="localhost"
|
||||||
|
SESSION_DOMAIN="localhost"
|
||||||
|
TRUST_PROXIES="*"
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_CONNECTION="mysql"
|
||||||
|
DB_HOST="127.0.0.1"
|
||||||
|
DB_PORT="3306"
|
||||||
|
DB_DATABASE="pixelfed"
|
||||||
|
DB_USERNAME="pixelfed"
|
||||||
|
DB_PASSWORD="pixelfed"
|
||||||
|
|
||||||
|
# Redis Configuration
|
||||||
|
REDIS_CLIENT="predis"
|
||||||
|
REDIS_SCHEME="tcp"
|
||||||
|
REDIS_HOST="127.0.0.1"
|
||||||
|
REDIS_PASSWORD="null"
|
||||||
|
REDIS_PORT="6379"
|
||||||
|
|
||||||
|
# Laravel Configuration
|
||||||
|
SESSION_DRIVER="database"
|
||||||
|
CACHE_DRIVER="redis"
|
||||||
|
QUEUE_DRIVER="redis"
|
||||||
|
BROADCAST_DRIVER="log"
|
||||||
|
LOG_CHANNEL="stack"
|
||||||
|
HORIZON_PREFIX="horizon-"
|
||||||
|
|
||||||
|
# ActivityPub Configuration
|
||||||
|
ACTIVITY_PUB="false"
|
||||||
|
AP_REMOTE_FOLLOW="false"
|
||||||
|
AP_INBOX="false"
|
||||||
|
AP_OUTBOX="false"
|
||||||
|
AP_SHAREDINBOX="false"
|
||||||
|
|
||||||
|
# Experimental Configuration
|
||||||
|
EXP_EMC="true"
|
||||||
|
|
||||||
|
## Mail Configuration (Post-Installer)
|
||||||
|
MAIL_DRIVER=log
|
||||||
|
MAIL_HOST=smtp.mailtrap.io
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_ENCRYPTION=null
|
||||||
|
MAIL_FROM_ADDRESS="pixelfed@example.com"
|
||||||
|
MAIL_FROM_NAME="Pixelfed"
|
||||||
|
|
||||||
|
## S3 Configuration (Post-Installer)
|
||||||
|
PF_ENABLE_CLOUD=false
|
||||||
|
FILESYSTEM_CLOUD=s3
|
||||||
|
#AWS_ACCESS_KEY_ID=
|
||||||
|
#AWS_SECRET_ACCESS_KEY=
|
||||||
|
#AWS_DEFAULT_REGION=
|
||||||
|
#AWS_BUCKET=<BucketName>
|
||||||
|
#AWS_URL=
|
||||||
|
#AWS_ENDPOINT=
|
||||||
|
#AWS_USE_PATH_STYLE_ENDPOINT=false
|
67
.env.testing
Normal file
67
.env.testing
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
APP_NAME="Pixelfed Test"
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E=
|
||||||
|
APP_DEBUG=true
|
||||||
|
|
||||||
|
APP_URL=https://pixelfed.dev
|
||||||
|
APP_DOMAIN="pixelfed.dev"
|
||||||
|
ADMIN_DOMAIN="pixelfed.dev"
|
||||||
|
SESSION_DOMAIN="pixelfed.dev"
|
||||||
|
TRUST_PROXIES="*"
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
|
||||||
|
DB_CONNECTION=sqlite
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE='tests/database.sqlite'
|
||||||
|
DB_USERNAME=
|
||||||
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
BROADCAST_DRIVER=log
|
||||||
|
CACHE_DRIVER=array
|
||||||
|
SESSION_DRIVER=array
|
||||||
|
QUEUE_DRIVER=redis
|
||||||
|
|
||||||
|
REDIS_SCHEME=tcp
|
||||||
|
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
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_ENCRYPTION=null
|
||||||
|
MAIL_FROM_ADDRESS="pixelfed@example.com"
|
||||||
|
MAIL_FROM_NAME="Pixelfed"
|
||||||
|
|
||||||
|
OPEN_REGISTRATION=true
|
||||||
|
ENFORCE_EMAIL_VERIFICATION=false
|
||||||
|
PF_MAX_USERS=1000
|
||||||
|
|
||||||
|
MAX_PHOTO_SIZE=15000
|
||||||
|
MAX_CAPTION_LENGTH=150
|
||||||
|
MAX_ALBUM_LENGTH=4
|
||||||
|
|
||||||
|
ACTIVITY_PUB=false
|
||||||
|
REMOTE_FOLLOW=false
|
||||||
|
ACTIVITYPUB_INBOX=false
|
||||||
|
ACTIVITYPUB_SHAREDINBOX=false
|
||||||
|
# Set these "true" to enable federation.
|
||||||
|
# You might need to also run:
|
||||||
|
# php artisan cache:clear
|
||||||
|
# php artisan optimize:clear
|
||||||
|
# php artisan optimize
|
||||||
|
|
||||||
|
PF_COSTAR_ENABLED=true
|
||||||
|
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
|
||||||
|
#HORIZON_DARKMODE=false # Horizon theme darkmode
|
||||||
|
#HORIZON_EMBED=false # Single Docker Container mode
|
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
* text=auto
|
||||||
|
*.css linguist-vendored
|
||||||
|
*.scss linguist-vendored
|
||||||
|
*.js linguist-vendored
|
||||||
|
CHANGELOG.md export-ignore
|
5
.github/FUNDING.yml
vendored
Normal file
5
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: dansup
|
||||||
|
patreon: dansup
|
||||||
|
open_collective: pixelfed
|
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# 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
|
125
.github/workflows/build-docker.yml
vendored
Normal file
125
.github/workflows/build-docker.yml
vendored
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
---
|
||||||
|
name: Build Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/build-docker.yml
|
||||||
|
- contrib/docker/Dockerfile.apache
|
||||||
|
- contrib/docker/Dockerfile.fpm
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-docker-apache:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Docker Lint
|
||||||
|
uses: hadolint/hadolint-action@v3.0.0
|
||||||
|
with:
|
||||||
|
dockerfile: contrib/docker/Dockerfile.apache
|
||||||
|
failure-threshold: error
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
|
- name: Fetch tags
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
secrets: inherit
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
|
||||||
|
flavor: |
|
||||||
|
latest=auto
|
||||||
|
suffix=-apache
|
||||||
|
tags: |
|
||||||
|
type=edge,branch=dev
|
||||||
|
type=pep440,pattern={{raw}}
|
||||||
|
type=pep440,pattern=v{{major}}.{{minor}}
|
||||||
|
type=ref,event=pr
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: contrib/docker/Dockerfile.apache
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
build-docker-fpm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Docker Lint
|
||||||
|
uses: hadolint/hadolint-action@v3.0.0
|
||||||
|
with:
|
||||||
|
dockerfile: contrib/docker/Dockerfile.fpm
|
||||||
|
failure-threshold: error
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
|
- name: Fetch tags
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
secrets: inherit
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
|
||||||
|
flavor: |
|
||||||
|
suffix=-fpm
|
||||||
|
tags: |
|
||||||
|
type=edge,branch=dev
|
||||||
|
type=pep440,pattern={{raw}}
|
||||||
|
type=pep440,pattern=v{{major}}.{{minor}}
|
||||||
|
type=ref,event=pr
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: contrib/docker/Dockerfile.fpm
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/node_modules
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/vendor
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
/.vagrant
|
||||||
|
/docker-volumes
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
.bash_profile
|
||||||
|
.bash_history
|
||||||
|
.bashrc
|
||||||
|
.gitconfig
|
||||||
|
.git-credentials
|
||||||
|
/.composer/
|
||||||
|
/nginx.conf
|
1
.node-version
Normal file
1
.node-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
v14.20.1
|
1364
CHANGELOG.md
Normal file
1364
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@pixelfed.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version]
|
||||||
|
|
||||||
|
[homepage]: https://contributor-covenant.org
|
||||||
|
[version]: https://contributor-covenant.org/version/1/4/
|
22
CONTRIBUTING.md
Normal file
22
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
## Bug Reports
|
||||||
|
To encourage active collaboration, Pixelfed strongly encourages pull requests, not just bug reports. "Bug reports" may also be sent in the form of a pull request containing a failing test.
|
||||||
|
|
||||||
|
However, if you file a bug report, your issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible and a code sample that demonstrates the issue. The goal of a bug report is to make it easy for yourself - and others - to replicate the bug and develop a fix.
|
||||||
|
|
||||||
|
Remember, bug reports are created in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the bug report will automatically see any activity or that others will jump to fix it. Creating a bug report serves to help yourself and others start on the path of fixing the problem.
|
||||||
|
|
||||||
|
## Core Development Discussion
|
||||||
|
Informal discussion regarding bugs, new features, and implementation of existing features takes place in the ```#pixelfed-dev``` channel on the Freenode IRC network.
|
||||||
|
|
||||||
|
## Branches
|
||||||
|
If you want to contribute to this repository, please file your pull request against the `staging` branch.
|
||||||
|
|
||||||
|
Pixelfed Beta currently uses the `dev` branch for deployable code. When v1.0 is released, the stable branch will be changed to `master`, with `dev` branch being used for development and testing.
|
||||||
|
|
||||||
|
## Compiled Assets
|
||||||
|
If you are submitting a change that will affect a compiled file, such as most of the files in ```resources/assets/sass``` or ```resources/assets/js``` of the pixelfed/pixelfed repository, do not commit the compiled files. Due to their large size, they cannot realistically be reviewed by a maintainer. This could be exploited as a way to inject malicious code into Pixelfed. In order to defensively prevent this, all compiled files will be generated and committed by Pixelfed maintainers.
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
If you discover a security vulnerability within Pixelfed, please send an email to Daniel Supernault at hello@pixelfed.org. All security vulnerabilities will be promptly addressed.
|
48
README.md
48
README.md
|
@ -1,3 +1,4 @@
|
||||||
|
<<<<<<< HEAD
|
||||||
<!--
|
<!--
|
||||||
N.B.: This README was automatically generated by https://github.com/YunoHost/apps/tree/master/tools/README-generator
|
N.B.: This README was automatically generated by https://github.com/YunoHost/apps/tree/master/tools/README-generator
|
||||||
It shall NOT be edited by hand.
|
It shall NOT be edited by hand.
|
||||||
|
@ -100,3 +101,50 @@ sudo yunohost app upgrade pixelfed -u https://github.com/YunoHost-Apps/pixelfed_
|
||||||
```
|
```
|
||||||
|
|
||||||
**More info regarding app packaging:** <https://yunohost.org/packaging_apps>
|
**More info regarding app packaging:** <https://yunohost.org/packaging_apps>
|
||||||
|
=======
|
||||||
|
<p align="center"><img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/logos/pixelfed-full-color.svg" width="300px"></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://circleci.com/gh/pixelfed/pixelfed"><img src="https://circleci.com/gh/pixelfed/pixelfed.svg?style=svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/v/stable.svg" alt="Latest Stable Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/license.svg" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
A free and ethical photo sharing platform, powered by ActivityPub federation.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/pixelfed-screenshot.jpg">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Official Documentation
|
||||||
|
|
||||||
|
Documentation for Pixelfed can be found on the [Pixelfed documentation website](https://docs.pixelfed.org/).
|
||||||
|
|
||||||
|
## Run on YunoHost
|
||||||
|
|
||||||
|
[](https://install-app.yunohost.org/?app=pixelfed)
|
||||||
|
|
||||||
|
Pixelfed app for [YunoHost](https://yunohost.org 'YunoHost'). See [the package source code](https://github.com/YunoHost-Apps/pixelfed_ynh 'pixelfed_ynh repository on GitHub')
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Pixelfed is open-sourced software licensed under the AGPL license.
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
The ways you can communicate on the project are below. Before interacting, please
|
||||||
|
read through the [Code Of Conduct](CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
* Mastodon: [@pixelfed@mastodon.social](https://mastodon.social/@pixelfed)
|
||||||
|
* E-mail: [hello@pixelfed.org](mailto:hello@pixelfed.org)
|
||||||
|
|
||||||
|
## Pixelfed Sponsors
|
||||||
|
|
||||||
|
We would like to extend our thanks to the following sponsors for funding Pixelfed development. If you are interested in becoming a sponsor, please visit the Pixelfed [Patreon Page](https://www.patreon.com/dansup/overview)
|
||||||
|
|
||||||
|
- [NLnet Foundation](https://nlnet.nl) and [NGI0
|
||||||
|
Discovery](https://nlnet.nl/discovery/), part of the [Next Generation
|
||||||
|
Internet](https://ngi.eu) initiative.
|
||||||
|
>>>>>>> upstream/staging
|
||||||
|
|
3
SECURITY.md
Normal file
3
SECURITY.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover any security related issues, please email hello@pixelfed.org instead of using the issue tracker.
|
30
app/AccountInterstitial.php
Normal file
30
app/AccountInterstitial.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class AccountInterstitial extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The attributes that should be mutated to dates.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $dates = ['read_at', 'appeal_requested_at'];
|
||||||
|
|
||||||
|
public const JSON_MESSAGE = 'Please use web browser to proceed.';
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status()
|
||||||
|
{
|
||||||
|
if($this->item_type != 'App\Status') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return $this->hasOne(Status::class, 'id', 'item_id');
|
||||||
|
}
|
||||||
|
}
|
16
app/AccountLog.php
Normal file
16
app/AccountLog.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class AccountLog extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $fillable = ['*'];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
21
app/Activity.php
Normal file
21
app/Activity.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Activity extends Model
|
||||||
|
{
|
||||||
|
protected $dates = ['processed_at'];
|
||||||
|
protected $fillable = ['data', 'to_id', 'from_id', 'object_type'];
|
||||||
|
|
||||||
|
public function toProfile()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'to_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromProfile()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'from_id');
|
||||||
|
}
|
||||||
|
}
|
25
app/Auth/BearerTokenResponse.php
Normal file
25
app/Auth/BearerTokenResponse.php
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?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(),
|
||||||
|
'scope' => 'read write follow push'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
36
app/Avatar.php
Normal file
36
app/Avatar.php
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class Avatar extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be mutated to dates.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $dates = [
|
||||||
|
'deleted_at',
|
||||||
|
'last_fetched_at',
|
||||||
|
'last_processed_at'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $visible = [
|
||||||
|
'id',
|
||||||
|
'profile_id',
|
||||||
|
'media_path',
|
||||||
|
'size',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function profile()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class);
|
||||||
|
}
|
||||||
|
}
|
21
app/Bookmark.php
Normal file
21
app/Bookmark.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Bookmark extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['profile_id', 'status_id'];
|
||||||
|
|
||||||
|
public function status()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Status::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function profile()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class);
|
||||||
|
}
|
||||||
|
}
|
39
app/Circle.php
Normal file
39
app/Circle.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Circle extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'profile_id',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'bcc',
|
||||||
|
'scope',
|
||||||
|
'active'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function members()
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(
|
||||||
|
Profile::class,
|
||||||
|
CircleProfile::class,
|
||||||
|
'circle_id',
|
||||||
|
'id',
|
||||||
|
'id',
|
||||||
|
'profile_id'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function owner()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'profile_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url()
|
||||||
|
{
|
||||||
|
return url("/i/circle/show/{$this->id}");
|
||||||
|
}
|
||||||
|
}
|
13
app/CircleProfile.php
Normal file
13
app/CircleProfile.php
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class CircleProfile extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'circle_id',
|
||||||
|
'profile_id'
|
||||||
|
];
|
||||||
|
}
|
50
app/Collection.php
Normal file
50
app/Collection.php
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\HasSnowflakePrimary;
|
||||||
|
|
||||||
|
class Collection extends Model
|
||||||
|
{
|
||||||
|
use HasSnowflakePrimary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the IDs are auto-incrementing.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
public $fillable = ['profile_id', 'published_at'];
|
||||||
|
|
||||||
|
public $dates = ['published_at'];
|
||||||
|
|
||||||
|
public function profile()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function items()
|
||||||
|
{
|
||||||
|
return $this->hasMany(CollectionItem::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function posts()
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(
|
||||||
|
Status::class,
|
||||||
|
CollectionItem::class,
|
||||||
|
'collection_id',
|
||||||
|
'id',
|
||||||
|
'id',
|
||||||
|
'object_id'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url()
|
||||||
|
{
|
||||||
|
return url("/c/{$this->id}");
|
||||||
|
}
|
||||||
|
}
|
30
app/CollectionItem.php
Normal file
30
app/CollectionItem.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\HasSnowflakePrimary;
|
||||||
|
|
||||||
|
class CollectionItem extends Model
|
||||||
|
{
|
||||||
|
use HasSnowflakePrimary;
|
||||||
|
|
||||||
|
public $fillable = [
|
||||||
|
'collection_id',
|
||||||
|
'object_type',
|
||||||
|
'object_id',
|
||||||
|
'order'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the IDs are auto-incrementing.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Collection::class);
|
||||||
|
}
|
||||||
|
}
|
18
app/Comment.php
Normal file
18
app/Comment.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Comment extends Model
|
||||||
|
{
|
||||||
|
public function profile()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Status::class);
|
||||||
|
}
|
||||||
|
}
|
179
app/Console/Commands/AdminInviteCommand.php
Normal file
179
app/Console/Commands/AdminInviteCommand.php
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
<?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());
|
||||||
|
}
|
||||||
|
}
|
92
app/Console/Commands/AvatarDefaultMigration.php
Normal file
92
app/Console/Commands/AvatarDefaultMigration.php
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Avatar;
|
||||||
|
use Cache, DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class AvatarDefaultMigration extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'fix:avatars';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Replace old svg identicon avatars with default png avatar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Running avatar migration...');
|
||||||
|
$count = Avatar::whereChangeCount(0)->count();
|
||||||
|
|
||||||
|
if($count == 0) {
|
||||||
|
$this->info('Found no avatars needing to be migrated!');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($count);
|
||||||
|
$this->info("Found {$count} avatars that may need to be migrated");
|
||||||
|
|
||||||
|
Avatar::whereChangeCount(0)->chunk(50, function($avatars) use ($bar) {
|
||||||
|
foreach($avatars as $avatar) {
|
||||||
|
if( $avatar->media_path == 'public/avatars/default.png' ||
|
||||||
|
$avatar->thumb_path == 'public/avatars/default.png' ||
|
||||||
|
$avatar->media_path == 'public/avatars/default.jpg' ||
|
||||||
|
$avatar->thumb_path == 'public/avatars/default.jpg'
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Str::endsWith($avatar->media_path, '/avatar.svg') == false) {
|
||||||
|
// do not modify non-default avatars
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function() use ($avatar, $bar) {
|
||||||
|
|
||||||
|
if(is_file(storage_path('app/' . $avatar->media_path))) {
|
||||||
|
@unlink(storage_path('app/' . $avatar->media_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(is_file(storage_path('app/' . $avatar->thumb_path))) {
|
||||||
|
@unlink(storage_path('app/' . $avatar->thumb_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
$avatar->media_path = 'public/avatars/default.jpg';
|
||||||
|
$avatar->thumb_path = 'public/avatars/default.jpg';
|
||||||
|
$avatar->change_count = $avatar->change_count + 1;
|
||||||
|
$avatar->save();
|
||||||
|
|
||||||
|
Cache::forget('avatar:' . $avatar->profile_id);
|
||||||
|
$bar->advance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
}
|
||||||
|
}
|
293
app/Console/Commands/AvatarStorage.php
Normal file
293
app/Console/Commands/AvatarStorage.php
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
<?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(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(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 = 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(!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(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');
|
||||||
|
}
|
||||||
|
}
|
199
app/Console/Commands/AvatarSync.php
Normal file
199
app/Console/Commands/AvatarSync.php
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Avatar;
|
||||||
|
use App\Profile;
|
||||||
|
use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
|
||||||
|
use App\Util\ActivityPub\Helpers;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class AvatarSync extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'avatars:sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Perform actions on avatars';
|
||||||
|
|
||||||
|
public $found = 0;
|
||||||
|
public $notFetched = 0;
|
||||||
|
public $fixed = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
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',
|
||||||
|
'Full Analyze',
|
||||||
|
'Fetch - Fetch missing remote avatars',
|
||||||
|
'Fix - Fix remote accounts without avatar record',
|
||||||
|
'Sync - Store latest remote avatars',
|
||||||
|
];
|
||||||
|
|
||||||
|
$name = $this->choice(
|
||||||
|
'Select an action',
|
||||||
|
$actions,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->info('Selected: ' . $name);
|
||||||
|
|
||||||
|
switch($name) {
|
||||||
|
case $actions[0]:
|
||||||
|
$this->analyze();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case $actions[1]:
|
||||||
|
$this->fullAnalyze();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case $actions[2]:
|
||||||
|
$this->fetch();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case $actions[3]:
|
||||||
|
$this->fix();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case $actions[4]:
|
||||||
|
$this->sync();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 analyze()
|
||||||
|
{
|
||||||
|
$count = Avatar::whereIsRemote(true)->whereNull('cdn_url')->count();
|
||||||
|
$this->info('Found ' . $count . ' profiles with blank avatars.');
|
||||||
|
$this->line(' ');
|
||||||
|
$this->comment('We suggest running php artisan avatars:sync again and selecting the sync option');
|
||||||
|
$this->line(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fullAnalyze()
|
||||||
|
{
|
||||||
|
$count = Profile::count();
|
||||||
|
$bar = $this->output->createProgressBar($count);
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
Profile::chunk(50, function($profiles) use ($bar) {
|
||||||
|
foreach($profiles as $profile) {
|
||||||
|
if($profile->domain == null) {
|
||||||
|
$bar->advance();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$avatar = Avatar::whereProfileId($profile->id)->first();
|
||||||
|
if(!$avatar || $avatar->cdn_url == null) {
|
||||||
|
$this->incr('notFetched');
|
||||||
|
}
|
||||||
|
$this->incr('found');
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->line(' ');
|
||||||
|
$this->line(' ');
|
||||||
|
$this->info('Found ' . $this->found . ' remote accounts');
|
||||||
|
$this->info('Found ' . $this->notFetched . ' remote avatars to fetch');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fix()
|
||||||
|
{
|
||||||
|
Profile::chunk(5000, function($profiles) {
|
||||||
|
foreach($profiles as $profile) {
|
||||||
|
if($profile->domain == null || $profile->private_key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$avatar = Avatar::whereProfileId($profile->id)->first();
|
||||||
|
if($avatar) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$avatar = new Avatar;
|
||||||
|
$avatar->is_remote = true;
|
||||||
|
$avatar->profile_id = $profile->id;
|
||||||
|
$avatar->save();
|
||||||
|
$this->incr('fixed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$this->line(' ');
|
||||||
|
$this->line(' ');
|
||||||
|
$this->info('Fixed ' . $this->fixed . ' accounts with a blank avatar');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
76
app/Console/Commands/BackupToCloud.php
Normal file
76
app/Console/Commands/BackupToCloud.php
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Http\File;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Spatie\Backup\BackupDestination\BackupDestination;
|
||||||
|
|
||||||
|
class BackupToCloud extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'backup:cloud';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Send backups to cloud storage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$localDisk = Storage::disk('local');
|
||||||
|
$cloudDisk = Storage::disk('backup');
|
||||||
|
$backupDestination = new BackupDestination($localDisk, '', 'local');
|
||||||
|
|
||||||
|
if(
|
||||||
|
empty(config('filesystems.disks.backup.key')) ||
|
||||||
|
empty(config('filesystems.disks.backup.secret')) ||
|
||||||
|
empty(config('filesystems.disks.backup.endpoint')) ||
|
||||||
|
empty(config('filesystems.disks.backup.region')) ||
|
||||||
|
empty(config('filesystems.disks.backup.bucket'))
|
||||||
|
) {
|
||||||
|
$this->error('Backup disk not configured.');
|
||||||
|
$this->error('See https://docs.pixelfed.org/technical-documentation/env.html#filesystem for more information.');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newest = $backupDestination->newestBackup();
|
||||||
|
$name = $newest->path();
|
||||||
|
$parts = explode('/', $name);
|
||||||
|
$fileName = array_pop($parts);
|
||||||
|
$storagePath = 'backups';
|
||||||
|
$path = storage_path('app/'. $name);
|
||||||
|
$file = $cloudDisk->putFileAs($storagePath, new File($path), $fileName, 'private');
|
||||||
|
$this->info("Backup file successfully saved!");
|
||||||
|
$url = $cloudDisk->url($file);
|
||||||
|
$this->table(
|
||||||
|
['Name', 'URL'],
|
||||||
|
[
|
||||||
|
[$fileName, $url]
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
50
app/Console/Commands/BannedEmailCheck.php
Normal file
50
app/Console/Commands/BannedEmailCheck.php
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\User;
|
||||||
|
use App\Services\EmailService;
|
||||||
|
|
||||||
|
class BannedEmailCheck extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'email:bancheck';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Checks user emails for banned domains';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$users = User::whereNull('status')->get()->filter(function($u) {
|
||||||
|
return EmailService::isBanned($u->email) == true;
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach($users as $user) {
|
||||||
|
$this->info('Found banned domain: ' . $user->email . PHP_EOL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
app/Console/Commands/CatchUnoptimizedMedia.php
Normal file
59
app/Console/Commands/CatchUnoptimizedMedia.php
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use DB;
|
||||||
|
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||||
|
use App\Media;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CatchUnoptimizedMedia extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'media:optimize';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Find and optimize media that has not yet been optimized.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Media $media)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
92
app/Console/Commands/CloudMediaMigrate.php
Normal file
92
app/Console/Commands/CloudMediaMigrate.php
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?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 = config('pixelfed.cloud_storage');
|
||||||
|
if(!$enabled) {
|
||||||
|
$this->error('Cloud storage not enabled. Exiting...');
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
56
app/Console/Commands/DatabaseSessionGarbageCollector.php
Normal file
56
app/Console/Commands/DatabaseSessionGarbageCollector.php
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class DatabaseSessionGarbageCollector extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'gc:sessions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Database sessions garbage collector';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if(config('session.driver') !== 'database') {
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function() {
|
||||||
|
DB::table('sessions')->whereNull('user_id')->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::transaction(function() {
|
||||||
|
$ts = now()->subMonths(3)->timestamp;
|
||||||
|
DB::table('sessions')->where('last_activity', '<', $ts)->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
74
app/Console/Commands/ExportLanguages.php
Normal file
74
app/Console/Commands/ExportLanguages.php
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class ExportLanguages extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'i18n:export';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Build and export js localization files.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if(config('app.env') !== 'local') {
|
||||||
|
$this->error('This command is meant for development purposes and should only be run in a local environment');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = base_path('resources/lang');
|
||||||
|
$langs = [];
|
||||||
|
|
||||||
|
foreach (new \DirectoryIterator($path) as $io) {
|
||||||
|
$name = $io->getFilename();
|
||||||
|
$skip = ['vendor'];
|
||||||
|
if($io->isDot() || in_array($name, $skip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($io->isDir()) {
|
||||||
|
array_push($langs, $name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$exportDir = resource_path('assets/js/i18n/');
|
||||||
|
$exportDirAlt = public_path('_lang/');
|
||||||
|
|
||||||
|
foreach($langs as $lang) {
|
||||||
|
$strings = \Lang::get('web', [], $lang);
|
||||||
|
$json = json_encode($strings, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||||
|
$path = "{$exportDir}{$lang}.json";
|
||||||
|
file_put_contents($path, $json);
|
||||||
|
$pathAlt = "{$exportDirAlt}{$lang}.json";
|
||||||
|
file_put_contents($pathAlt, $json);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
49
app/Console/Commands/FailedJobGC.php
Normal file
49
app/Console/Commands/FailedJobGC.php
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\FailedJob;
|
||||||
|
|
||||||
|
class FailedJobGC extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'gc:failedjobs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Delete failed jobs over 1 month old';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
FailedJob::chunk(50, function($jobs) {
|
||||||
|
foreach($jobs as $job) {
|
||||||
|
if($job->failed_at->lt(now()->subHours(48))) {
|
||||||
|
$job->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
254
app/Console/Commands/FixDuplicateProfiles.php
Normal file
254
app/Console/Commands/FixDuplicateProfiles.php
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
use App\Models\{
|
||||||
|
Conversation,
|
||||||
|
Portfolio,
|
||||||
|
UserPronoun
|
||||||
|
};
|
||||||
|
use DB, Cache;
|
||||||
|
|
||||||
|
class FixDuplicateProfiles extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'fix:profile:duplicates';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Fix duplicate profiles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$duplicates = DB::table('profiles')
|
||||||
|
->whereNull('domain')
|
||||||
|
->select('username', DB::raw('COUNT(*) as "count"'))
|
||||||
|
->groupBy('username')
|
||||||
|
->havingRaw('COUNT(*) > 1')
|
||||||
|
->pluck('username');
|
||||||
|
|
||||||
|
foreach($duplicates as $dupe) {
|
||||||
|
$ids = Profile::whereNull('domain')->whereUsername($dupe)->pluck('id');
|
||||||
|
if(!$ids || $ids->count() != 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$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();
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
109
app/Console/Commands/FixHashtags.php
Normal file
109
app/Console/Commands/FixHashtags.php
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use DB;
|
||||||
|
use App\{
|
||||||
|
Hashtag,
|
||||||
|
Status,
|
||||||
|
StatusHashtag
|
||||||
|
};
|
||||||
|
|
||||||
|
class FixHashtags extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'fix:hashtags';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Fix Hashtags';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
|
||||||
|
$this->info(' ____ _ ______ __ ');
|
||||||
|
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||||
|
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||||
|
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||||
|
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info('Pixelfed version: ' . config('pixelfed.version'));
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info('Running Fix Hashtags command');
|
||||||
|
$this->info(' ');
|
||||||
|
|
||||||
|
$missingCount = StatusHashtag::doesntHave('profile')->doesntHave('status')->count();
|
||||||
|
if($missingCount > 0) {
|
||||||
|
$this->info("Found {$missingCount} orphaned StatusHashtag records to delete ...");
|
||||||
|
$this->info(' ');
|
||||||
|
$bar = $this->output->createProgressBar($missingCount);
|
||||||
|
$bar->start();
|
||||||
|
foreach(StatusHashtag::doesntHave('profile')->doesntHave('status')->get() as $tag) {
|
||||||
|
$tag->delete();
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
$bar->finish();
|
||||||
|
$this->info(' ');
|
||||||
|
} else {
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info('Found no orphaned hashtags to delete!');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$this->info(' ');
|
||||||
|
|
||||||
|
$count = StatusHashtag::whereNull('status_visibility')->count();
|
||||||
|
if($count > 0) {
|
||||||
|
$this->info("Found {$count} hashtags to fix ...");
|
||||||
|
$this->info(' ');
|
||||||
|
} else {
|
||||||
|
$this->info('Found no hashtags to fix!');
|
||||||
|
$this->info(' ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($count);
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
StatusHashtag::with('status')
|
||||||
|
->whereNull('status_visibility')
|
||||||
|
->chunk(50, function($tags) use($bar) {
|
||||||
|
foreach($tags as $tag) {
|
||||||
|
if(!$tag->status || !$tag->status->scope) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$tag->status_visibility = $tag->status->scope;
|
||||||
|
$tag->save();
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info(' ');
|
||||||
|
}
|
||||||
|
}
|
75
app/Console/Commands/FixLikes.php
Normal file
75
app/Console/Commands/FixLikes.php
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\{Like, Status};
|
||||||
|
use DB;
|
||||||
|
|
||||||
|
class FixLikes extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'fix:likes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Fix Like counts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$chunk = 100;
|
||||||
|
$limit = Like::select('status_id')->groupBy('status_id')->get()->count();
|
||||||
|
|
||||||
|
if($limit > 1000) {
|
||||||
|
if($this->confirm('We have found more than 1000 records to update, this may take a few moments. Are you sure you want to continue?') == false) {
|
||||||
|
$this->error('Cancelling command...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($limit);
|
||||||
|
$this->line(' ');
|
||||||
|
$this->info(' Starting like count fix ...');
|
||||||
|
$this->line(' ');
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
Like::selectRaw('count(id) as count, status_id')
|
||||||
|
->groupBy(['status_id','id'])
|
||||||
|
->chunk($chunk, function($likes) use($bar) {
|
||||||
|
foreach($likes as $like) {
|
||||||
|
$s = Status::find($like['status_id']);
|
||||||
|
if($s && $s->likes_count == 0) {
|
||||||
|
$s->likes_count = $like['count'];
|
||||||
|
$s->save();
|
||||||
|
}
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->line(' ');
|
||||||
|
$this->line(' ');
|
||||||
|
}
|
||||||
|
}
|
133
app/Console/Commands/FixMediaDriver.php
Normal file
133
app/Console/Commands/FixMediaDriver.php
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
<?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(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::dispatchSync($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;
|
||||||
|
}
|
||||||
|
}
|
53
app/Console/Commands/FixRemotePostCount.php
Normal file
53
app/Console/Commands/FixRemotePostCount.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
56
app/Console/Commands/FixStatusCount.php
Normal file
56
app/Console/Commands/FixStatusCount.php
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Profile;
|
||||||
|
|
||||||
|
class FixStatusCount extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'fix:statuscount';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'fix profile status count';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
164
app/Console/Commands/FixUsernames.php
Normal file
164
app/Console/Commands/FixUsernames.php
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\{Profile, User};
|
||||||
|
use DB;
|
||||||
|
use App\Util\Lexer\RestrictedNames;
|
||||||
|
|
||||||
|
class FixUsernames extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'fix:usernames';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Fix invalid usernames';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->line(' ');
|
||||||
|
$this->info('Collecting data ...');
|
||||||
|
$this->line(' ');
|
||||||
|
$this->restrictedCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function restrictedCheck()
|
||||||
|
{
|
||||||
|
$affected = collect([]);
|
||||||
|
|
||||||
|
$restricted = RestrictedNames::get();
|
||||||
|
|
||||||
|
$users = User::chunk(100, function($users) use($affected, $restricted) {
|
||||||
|
foreach($users as $user) {
|
||||||
|
if($user->is_admin || $user->status == 'deleted') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(in_array(strtolower($user->username), array_map('strtolower', $restricted))) {
|
||||||
|
$affected->push($user);
|
||||||
|
}
|
||||||
|
$val = str_replace(['-', '_', '.'], '', $user->username);
|
||||||
|
if(!ctype_alnum($val)) {
|
||||||
|
$this->info('Found invalid username: ' . $user->username);
|
||||||
|
$affected->push($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if($affected->count() > 0) {
|
||||||
|
$this->info('Found: ' . $affected->count() . ' affected usernames');
|
||||||
|
|
||||||
|
$opts = [
|
||||||
|
'Random replace (assigns random username)',
|
||||||
|
'Best try replace (assigns alpha numeric username)',
|
||||||
|
'Manual replace (manually set username)',
|
||||||
|
'Skip (do not replace. Use at your own risk)'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach($affected as $u) {
|
||||||
|
$old = $u->username;
|
||||||
|
$this->info("Found user: {$old}");
|
||||||
|
$opt = $this->choice('Select fix method:', $opts, 3);
|
||||||
|
|
||||||
|
switch ($opt) {
|
||||||
|
case $opts[0]:
|
||||||
|
$new = "user_" . str_random(6);
|
||||||
|
$this->info('New username: ' . $new);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case $opts[1]:
|
||||||
|
$new = filter_var($old, FILTER_SANITIZE_STRING|FILTER_FLAG_STRIP_LOW);
|
||||||
|
if(strlen($new) < 6) {
|
||||||
|
$new = $new . '_' . str_random(4);
|
||||||
|
}
|
||||||
|
$this->info('New username: ' . $new);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case $opts[2]:
|
||||||
|
$new = $this->ask('Enter new username:');
|
||||||
|
$this->info('New username: ' . $new);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case $opts[3]:
|
||||||
|
$new = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$new = "user_" . str_random(6);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($new) {
|
||||||
|
DB::transaction(function() use($u, $new) {
|
||||||
|
$profile = $u->profile;
|
||||||
|
$profile->username = $new;
|
||||||
|
$u->username = $new;
|
||||||
|
$u->save();
|
||||||
|
$profile->save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$this->info('Selected: ' . $opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Fixed ' . $affected->count() . ' usernames!');
|
||||||
|
} else {
|
||||||
|
$this->info('No restricted usernames found!');
|
||||||
|
}
|
||||||
|
$this->line(' ');
|
||||||
|
$this->versionZeroTenNineFix();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function versionZeroTenNineFix()
|
||||||
|
{
|
||||||
|
$profiles = Profile::whereNotNull('domain')
|
||||||
|
->whereNull('private_key')
|
||||||
|
->where('username', 'not like', '@%@%')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$count = $profiles->count();
|
||||||
|
|
||||||
|
if($count > 0) {
|
||||||
|
$this->info("Found {$count} remote usernames to fix ...");
|
||||||
|
$this->line(' ');
|
||||||
|
} else {
|
||||||
|
$this->info('No remote fixes found!');
|
||||||
|
$this->line(' ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach($profiles as $p) {
|
||||||
|
$this->info("Fixed $p->username => $p->webfinger");
|
||||||
|
$p->username = $p->webfinger ?? "@{$p->username}@{$p->domain}";
|
||||||
|
if(Profile::whereUsername($p->username)->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$p->save();
|
||||||
|
}
|
||||||
|
if($count > 0) {
|
||||||
|
$this->line(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
73
app/Console/Commands/GenerateInstanceActor.php
Normal file
73
app/Console/Commands/GenerateInstanceActor.php
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Models\InstanceActor;
|
||||||
|
use Cache;
|
||||||
|
|
||||||
|
class GenerateInstanceActor extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'instance:actor';
|
||||||
|
protected $description = 'Generate instance actor';
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if(Schema::hasTable('instance_actors') == false) {
|
||||||
|
$this->line(' ');
|
||||||
|
$this->error('Missing instance_actors table.');
|
||||||
|
$this->info('Run "php artisan migrate" and try again.');
|
||||||
|
$this->line(' ');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(InstanceActor::exists()) {
|
||||||
|
$actor = InstanceActor::whereNotNull('public_key')
|
||||||
|
->whereNotNull('private_key')
|
||||||
|
->firstOrFail();
|
||||||
|
Cache::rememberForever(InstanceActor::PKI_PUBLIC, function() use($actor) {
|
||||||
|
return $actor->public_key;
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pkiConfig = [
|
||||||
|
'digest_alg' => 'sha512',
|
||||||
|
'private_key_bits' => 2048,
|
||||||
|
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||||
|
];
|
||||||
|
$pki = openssl_pkey_new($pkiConfig);
|
||||||
|
openssl_pkey_export($pki, $pki_private);
|
||||||
|
$pki_public = openssl_pkey_get_details($pki);
|
||||||
|
$pki_public = $pki_public['key'];
|
||||||
|
|
||||||
|
$actor = new InstanceActor();
|
||||||
|
$actor->public_key = $pki_public;
|
||||||
|
$actor->private_key = $pki_private;
|
||||||
|
$actor->save();
|
||||||
|
|
||||||
|
Cache::rememberForever(InstanceActor::PKI_PUBLIC, function() use($actor) {
|
||||||
|
return $actor->public_key;
|
||||||
|
});
|
||||||
|
|
||||||
|
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 0;
|
||||||
|
}
|
||||||
|
}
|
160
app/Console/Commands/ImportCities.php
Normal file
160
app/Console/Commands/ImportCities.php
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Place;
|
||||||
|
use DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ImportCities extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'import:cities {chunk=1000}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Import Cities to database';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checksum of city dataset.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const CHECKSUM = 'e203c0247538788b2a91166c7cf4b95f58291d998f514e9306d315aa72b09e48bfd3ddf310bf737afc4eefadca9083b8ff796c67796c6bd8e882a3d268bd16af';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of shortened countries.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $countries = [
|
||||||
|
'AE' => 'UAE',
|
||||||
|
'BA' => 'Bosnia-Herzegovina',
|
||||||
|
'BO' => 'Bolivia',
|
||||||
|
'CD' => 'Democratic Republic of Congo',
|
||||||
|
'CG' => 'Republic of Congo',
|
||||||
|
'FM' => 'Micronesia',
|
||||||
|
'GB' => 'United Kingdom',
|
||||||
|
'IR' => 'Iran',
|
||||||
|
'KP' => 'DRPK',
|
||||||
|
'KR' => 'South Korea',
|
||||||
|
'LA' => 'Laos',
|
||||||
|
'MD' => 'Moldova',
|
||||||
|
'PS' => 'Palestine',
|
||||||
|
'RU' => 'Russia',
|
||||||
|
'SH' => 'Saint Helena',
|
||||||
|
'SY' => 'Syria',
|
||||||
|
'TW' => 'Taiwan',
|
||||||
|
'TZ' => 'Tanzania',
|
||||||
|
'US' => 'USA',
|
||||||
|
'VE' => 'Venezuela',
|
||||||
|
'XK' => 'Kosovo'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$old_memory_limit = ini_get('memory_limit');
|
||||||
|
ini_set('memory_limit', '256M');
|
||||||
|
$path = storage_path('app/cities.json');
|
||||||
|
|
||||||
|
if(hash_file('sha512', $path) !== self::CHECKSUM) {
|
||||||
|
$this->error('Invalid or corrupt storage/app/cities.json data.');
|
||||||
|
$this->line('');
|
||||||
|
$this->info('Run the following command to fix:');
|
||||||
|
$this->info('git checkout storage/app/cities.json');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file($path)) {
|
||||||
|
$this->error('Missing storage/app/cities.json file!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Place::count() > 0) {
|
||||||
|
DB::table('places')->truncate();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Importing city data into database ...');
|
||||||
|
|
||||||
|
$cities = json_decode(file_get_contents($path));
|
||||||
|
$cityCount = count($cities);
|
||||||
|
|
||||||
|
$this->line('');
|
||||||
|
$this->info("Found {$cityCount} cities to insert ...");
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($cityCount);
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
$buffer = [];
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($cities as $city) {
|
||||||
|
$buffer[] = [
|
||||||
|
"name" => $city->name,
|
||||||
|
"slug" => Str::slug($city->name),
|
||||||
|
"country" => $this->codeToCountry($city->country),
|
||||||
|
"lat" => $city->lat,
|
||||||
|
"long" => $city->lng
|
||||||
|
];
|
||||||
|
|
||||||
|
$count++;
|
||||||
|
|
||||||
|
if ($count % $this->argument('chunk') == 0) {
|
||||||
|
$this->insertBuffer($buffer);
|
||||||
|
$bar->advance(count($buffer));
|
||||||
|
$buffer = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->insertBuffer($buffer);
|
||||||
|
|
||||||
|
$bar->advance(count($buffer));
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
|
||||||
|
$this->line('');
|
||||||
|
$this->line('');
|
||||||
|
$this->info('Successfully imported ' . $cityCount . ' entries!');
|
||||||
|
$this->line('');
|
||||||
|
ini_set('memory_limit', $old_memory_limit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function insertBuffer($buffer)
|
||||||
|
{
|
||||||
|
DB::table('places')->insert($buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function codeToCountry($code)
|
||||||
|
{
|
||||||
|
$countries = $this->countries;
|
||||||
|
if(isset($countries[$code])) {
|
||||||
|
return $countries[$code];
|
||||||
|
}
|
||||||
|
|
||||||
|
$country = (new \League\ISO3166\ISO3166)->alpha2($code);
|
||||||
|
$this->countries[$code] = $country['name'];
|
||||||
|
return $this->countries[$code];
|
||||||
|
}
|
||||||
|
}
|
505
app/Console/Commands/Installer.php
Normal file
505
app/Console/Commands/Installer.php
Normal file
|
@ -0,0 +1,505 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use \PDO;
|
||||||
|
|
||||||
|
class Installer extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'install {--dangerously-overwrite-env : Re-run installation and overwrite current .env }';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'CLI Installer';
|
||||||
|
|
||||||
|
public $installType = 'Simple';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->welcome();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function welcome()
|
||||||
|
{
|
||||||
|
$this->info(' ____ _ ______ __ ');
|
||||||
|
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||||
|
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||||
|
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||||
|
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info(' Welcome to the Pixelfed Installer!');
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info('Pixelfed version: ' . config('pixelfed.version'));
|
||||||
|
$this->line(' ');
|
||||||
|
$this->installerSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
$this->line(' ');
|
||||||
|
$this->info('Checking for Required PHP Extensions...');
|
||||||
|
|
||||||
|
$extensions = [
|
||||||
|
'bcmath',
|
||||||
|
'ctype',
|
||||||
|
'curl',
|
||||||
|
'json',
|
||||||
|
'mbstring',
|
||||||
|
'openssl',
|
||||||
|
'gd',
|
||||||
|
'intl',
|
||||||
|
'xml',
|
||||||
|
'zip',
|
||||||
|
'redis',
|
||||||
|
];
|
||||||
|
|
||||||
|
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");
|
||||||
|
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");
|
||||||
|
} else {
|
||||||
|
$this->info("- \"{$dep}\" found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function checkDiskPermissions()
|
||||||
|
{
|
||||||
|
$this->line('');
|
||||||
|
$this->info('Checking for proper filesystem permissions...');
|
||||||
|
$this->callSilently('storage:link');
|
||||||
|
|
||||||
|
$paths = [
|
||||||
|
base_path('bootstrap'),
|
||||||
|
base_path('storage'),
|
||||||
|
];
|
||||||
|
|
||||||
|
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");
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$this->info("- Found valid permissions for {$path}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function envProd()
|
||||||
|
{
|
||||||
|
$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!');
|
||||||
|
} 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resetArtisanCache()
|
||||||
|
{
|
||||||
|
$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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function updateEnvFile($key, $value)
|
||||||
|
{
|
||||||
|
$envPath = app()->environmentFilePath();
|
||||||
|
$payload = file_get_contents($envPath);
|
||||||
|
|
||||||
|
if ($existing = $this->existingEnv($key, $payload)) {
|
||||||
|
$payload = str_replace("{$key}={$existing}", "{$key}=\"{$value}\"", $payload);
|
||||||
|
$this->storeEnv($payload);
|
||||||
|
} else {
|
||||||
|
$payload = $payload . "\n{$key}=\"{$value}\"\n";
|
||||||
|
$this->storeEnv($payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function existingEnv($needle, $haystack)
|
||||||
|
{
|
||||||
|
preg_match("/^{$needle}=[^\r\n]*/m", $haystack, $matches);
|
||||||
|
if ($matches && count($matches)) {
|
||||||
|
return substr($matches[0], strlen($needle) + 1);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function storeEnv($payload)
|
||||||
|
{
|
||||||
|
$file = fopen(app()->environmentFilePath(), 'w');
|
||||||
|
fwrite($file, $payload);
|
||||||
|
fclose($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseSize($size)
|
||||||
|
{
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
app/Console/Commands/MediaFix.php
Normal file
61
app/Console/Commands/MediaFix.php
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Util\Media\Filter;
|
||||||
|
use App\Media;
|
||||||
|
|
||||||
|
class MediaFix extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'media:fix';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Fix media on v0.10.8+';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if(!version_compare(config('pixelfed.version'),'0.10.8','ge')) {
|
||||||
|
$this->error('Please update to version 0.10.8 or newer.');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$classes = Filter::classes();
|
||||||
|
|
||||||
|
Media::whereNotNull('filter_class')
|
||||||
|
->chunk(50, function($filters) use($classes) {
|
||||||
|
foreach($filters as $filter) {
|
||||||
|
$match = $filter->filter_class ? in_array($filter->filter_class, $classes) : true;
|
||||||
|
if(!$match) {
|
||||||
|
$filter->filter_class = null;
|
||||||
|
$filter->filter_name = null;
|
||||||
|
$filter->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
58
app/Console/Commands/MediaGarbageCollector.php
Normal file
58
app/Console/Commands/MediaGarbageCollector.php
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\{Media, Status};
|
||||||
|
use App\Services\MediaStorageService;
|
||||||
|
|
||||||
|
class MediaGarbageCollector extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$limit = 500;
|
||||||
|
|
||||||
|
$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('');
|
||||||
|
}
|
||||||
|
}
|
189
app/Console/Commands/MediaS3GarbageCollector.php
Normal file
189
app/Console/Commands/MediaS3GarbageCollector.php
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
<?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 = config('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()->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!');
|
||||||
|
}
|
||||||
|
}
|
48
app/Console/Commands/PasswordResetGC.php
Normal file
48
app/Console/Commands/PasswordResetGC.php
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\EmailVerification;
|
||||||
|
|
||||||
|
class PasswordResetGC extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'gc:passwordreset';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Delete password reset tokens over 24 hours old';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
EmailVerification::where('created_at', '<', now()->subMinutes(1441))
|
||||||
|
->chunk(50, function($emails) {
|
||||||
|
foreach($emails as $em) {
|
||||||
|
$em->delete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
51
app/Console/Commands/RegenerateThumbnails.php
Normal file
51
app/Console/Commands/RegenerateThumbnails.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Media;
|
||||||
|
use DB;
|
||||||
|
|
||||||
|
class RegenerateThumbnails extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'regenerate:thumbnails';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Regenerate thumbnails';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
DB::transaction(function() {
|
||||||
|
Media::whereIn('mime', ['image/jpeg', 'image/png'])
|
||||||
|
->chunk(50, function($medias) {
|
||||||
|
foreach($medias as $media) {
|
||||||
|
\App\Jobs\ImageOptimizePipeline\ImageThumbnail::dispatch($media);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
66
app/Console/Commands/SeedFollows.php
Normal file
66
app/Console/Commands/SeedFollows.php
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Follower;
|
||||||
|
use App\Jobs\FollowPipeline\FollowPipeline;
|
||||||
|
use App\Profile;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SeedFollows extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'seed:follows';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Seed follows for testing';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$limit = 100;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $limit; $i++) {
|
||||||
|
try {
|
||||||
|
$actor = Profile::whereDomain(false)->inRandomOrder()->firstOrFail();
|
||||||
|
$target = Profile::whereDomain(false)->inRandomOrder()->firstOrFail();
|
||||||
|
|
||||||
|
if($actor->id == $target->id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$follow = Follower::firstOrCreate([
|
||||||
|
'profile_id' => $actor->id,
|
||||||
|
'following_id' => $target->id
|
||||||
|
]);
|
||||||
|
if($follow->wasRecentlyCreated == true) {
|
||||||
|
FollowPipeline::dispatch($follow);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
181
app/Console/Commands/SendUpdateActor.php
Normal file
181
app/Console/Commands/SendUpdateActor.php
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
<?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'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
68
app/Console/Commands/StatusDedupe.php
Normal file
68
app/Console/Commands/StatusDedupe.php
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Status;
|
||||||
|
use DB;
|
||||||
|
use App\Jobs\StatusPipeline\StatusDelete;
|
||||||
|
|
||||||
|
class StatusDedupe extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'status:dedup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Removes duplicate statuses from before unique uri migration';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
|
||||||
|
if(config('database.default') == 'pgsql') {
|
||||||
|
$this->info('This command is not compatible with Postgres, we are working on a fix.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DB::table('statuses')
|
||||||
|
->selectRaw('id, uri, count(uri) as occurences')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->whereNotNull('uri')
|
||||||
|
->groupBy('uri')
|
||||||
|
->orderBy('created_at')
|
||||||
|
->having('occurences', '>', 1)
|
||||||
|
->chunk(50, function($statuses) {
|
||||||
|
foreach($statuses as $status) {
|
||||||
|
$this->info("Found duplicate: $status->uri");
|
||||||
|
Status::whereUri($status->uri)
|
||||||
|
->where('id', '!=', $status->id)
|
||||||
|
->get()
|
||||||
|
->map(function($status) {
|
||||||
|
$this->info("Deleting Duplicate ID: $status->id");
|
||||||
|
StatusDelete::dispatch($status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
84
app/Console/Commands/StoryGC.php
Normal file
84
app/Console/Commands/StoryGC.php
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use App\Story;
|
||||||
|
use App\StoryView;
|
||||||
|
use App\Jobs\StoryPipeline\StoryExpire;
|
||||||
|
use App\Jobs\StoryPipeline\StoryRotateMedia;
|
||||||
|
use App\Services\StoryService;
|
||||||
|
|
||||||
|
class StoryGC extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'story:gc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Clear expired Stories';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->archiveExpiredStories();
|
||||||
|
$this->rotateMedia();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function archiveExpiredStories()
|
||||||
|
{
|
||||||
|
$stories = Story::whereActive(true)
|
||||||
|
->where('expires_at', '<', now())
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach($stories as $story) {
|
||||||
|
StoryExpire::dispatch($story)->onQueue('story');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function rotateMedia()
|
||||||
|
{
|
||||||
|
$queue = StoryService::rotateQueue();
|
||||||
|
|
||||||
|
if(!$queue || count($queue) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
collect($queue)
|
||||||
|
->each(function($id) {
|
||||||
|
$story = StoryService::getById($id);
|
||||||
|
if(!$story) {
|
||||||
|
StoryService::removeRotateQueue($id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if($story->created_at->gt(now()->subMinutes(20))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StoryRotateMedia::dispatch($story)->onQueue('story');
|
||||||
|
StoryService::removeRotateQueue($id);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
86
app/Console/Commands/UpdateCommand.php
Normal file
86
app/Console/Commands/UpdateCommand.php
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Schema;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
|
||||||
|
|
||||||
|
class UpdateCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'update';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Run pixelfed schema updates between versions.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update()
|
||||||
|
{
|
||||||
|
$v = $this->getVersionFile();
|
||||||
|
if($v && isset($v['commit_hash']) && $v['commit_hash'] == exec('git rev-parse HEAD') && \App\StatusHashtag::whereNull('profile_id')->count() == 0) {
|
||||||
|
$this->info('No updates found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$bar = $this->output->createProgressBar(\App\StatusHashtag::whereNull('profile_id')->count());
|
||||||
|
\App\StatusHashtag::whereNull('profile_id')->with('status')->chunk(50, function($sh) use ($bar) {
|
||||||
|
foreach($sh as $status_hashtag) {
|
||||||
|
if(!$status_hashtag->status) {
|
||||||
|
$status_hashtag->delete();
|
||||||
|
} else {
|
||||||
|
$status_hashtag->profile_id = $status_hashtag->status->profile_id;
|
||||||
|
$status_hashtag->save();
|
||||||
|
}
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$this->updateVersionFile();
|
||||||
|
$bar->finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getVersionFile()
|
||||||
|
{
|
||||||
|
$path = storage_path('app/version.json');
|
||||||
|
return is_file($path) ?
|
||||||
|
json_decode(file_get_contents($path), true) :
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function updateVersionFile() {
|
||||||
|
$path = storage_path('app/version.json');
|
||||||
|
$contents = [
|
||||||
|
'commit_hash' => exec('git rev-parse HEAD'),
|
||||||
|
'version' => config('pixelfed.version'),
|
||||||
|
'timestamp' => date('c')
|
||||||
|
];
|
||||||
|
$json = json_encode($contents, JSON_PRETTY_PRINT);
|
||||||
|
file_put_contents($path, $json);
|
||||||
|
}
|
||||||
|
}
|
62
app/Console/Commands/UserAdmin.php
Normal file
62
app/Console/Commands/UserAdmin.php
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\User;
|
||||||
|
|
||||||
|
class UserAdmin extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'user:admin {id}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Make a user an admin, or remove admin privileges.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$id = $this->argument('id');
|
||||||
|
if(ctype_digit($id) == true) {
|
||||||
|
$user = User::find($id);
|
||||||
|
} else {
|
||||||
|
$user = User::whereUsername($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);
|
||||||
|
if(!$confirmed) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->is_admin = !$user->is_admin;
|
||||||
|
$user->save();
|
||||||
|
$this->info('Successfully changed permissions!');
|
||||||
|
}
|
||||||
|
}
|
113
app/Console/Commands/UserCreate.php
Normal file
113
app/Console/Commands/UserCreate.php
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\User;
|
||||||
|
|
||||||
|
class UserCreate extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'user:create {--name=} {--username=} {--email=} {--password=} {--is_admin=0} {--confirm_email=0}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Create a new user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Creating a new user...');
|
||||||
|
|
||||||
|
$o = $this->options();
|
||||||
|
|
||||||
|
if( $o['name'] &&
|
||||||
|
$o['username'] &&
|
||||||
|
$o['email'] &&
|
||||||
|
$o['password']
|
||||||
|
) {
|
||||||
|
$user = new User;
|
||||||
|
$user->username = $o['username'];
|
||||||
|
$user->name = $o['name'];
|
||||||
|
$user->email = $o['email'];
|
||||||
|
$user->password = bcrypt($o['password']);
|
||||||
|
$user->is_admin = (bool) $o['is_admin'];
|
||||||
|
$user->email_verified_at = $o['confirm_email'] ? now() : null;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$this->info('Successfully created user!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->ask('Name');
|
||||||
|
|
||||||
|
$username = $this->ask('Username');
|
||||||
|
|
||||||
|
if(User::whereUsername($username)->exists()) {
|
||||||
|
$this->error('Username already in use, please try again...');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $this->ask('Email');
|
||||||
|
|
||||||
|
if(User::whereEmail($email)->exists()) {
|
||||||
|
$this->error('Email already in use, please try again...');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$password = $this->secret('Password');
|
||||||
|
$confirm = $this->secret('Confirm Password');
|
||||||
|
|
||||||
|
if($password !== $confirm) {
|
||||||
|
$this->error('Password mismatch, please try again...');
|
||||||
|
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?');
|
||||||
|
|
||||||
|
if($this->confirm('Are you sure you want to create this user?') &&
|
||||||
|
$username &&
|
||||||
|
$name &&
|
||||||
|
$email &&
|
||||||
|
$password
|
||||||
|
) {
|
||||||
|
$user = new User;
|
||||||
|
$user->username = $username;
|
||||||
|
$user->name = $name;
|
||||||
|
$user->email = $email;
|
||||||
|
$user->password = bcrypt($password);
|
||||||
|
$user->is_admin = $is_admin;
|
||||||
|
$user->email_verified_at = $confirm_email ? now() : null;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$this->info('Created new user!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
app/Console/Commands/UserDelete.php
Normal file
86
app/Console/Commands/UserDelete.php
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\User;
|
||||||
|
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||||
|
|
||||||
|
class UserDelete extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'user:delete {id} {--force}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Delete account';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$id = $this->argument('id');
|
||||||
|
$force = $this->option('force');
|
||||||
|
|
||||||
|
if(ctype_digit($id) == true) {
|
||||||
|
$user = User::find($id);
|
||||||
|
} else {
|
||||||
|
$user = User::whereUsername($id)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$user) {
|
||||||
|
$this->error('Could not find any user with that username or id.');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($user->status == 'deleted' && $force == false) {
|
||||||
|
$this->error('Account has already been deleted.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($user->is_admin == true) {
|
||||||
|
$this->error('Cannot delete an admin account from CLI.');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$this->confirm('Are you sure you want to delete this account?')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$confirmation = $this->ask('Enter the username to confirm deletion');
|
||||||
|
|
||||||
|
if($confirmation !== $user->username) {
|
||||||
|
$this->error('Username does not match, exiting...');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($user->status !== 'deleted') {
|
||||||
|
$profile = $user->profile;
|
||||||
|
$profile->status = $user->status = 'deleted';
|
||||||
|
$profile->save();
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteAccountPipeline::dispatch($user)->onQueue('high');
|
||||||
|
}
|
||||||
|
}
|
82
app/Console/Commands/UserRegistrationMagicLink.php
Normal file
82
app/Console/Commands/UserRegistrationMagicLink.php
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
58
app/Console/Commands/UserShow.php
Normal file
58
app/Console/Commands/UserShow.php
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\User;
|
||||||
|
|
||||||
|
class UserShow extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'user:show {id}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Show user info';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$id = $this->argument('id');
|
||||||
|
if(ctype_digit($id) == true) {
|
||||||
|
$user = User::find($id);
|
||||||
|
} else {
|
||||||
|
$user = User::whereUsername($id)->first();
|
||||||
|
}
|
||||||
|
if(!$user) {
|
||||||
|
$this->error('Could not find any user with that username or id.');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('User ID: ' . $user->id);
|
||||||
|
$this->info('Username: ' . $user->username);
|
||||||
|
$this->info('Email: ' . $user->email);
|
||||||
|
$this->info('Joined: ' . $user->created_at->diffForHumans());
|
||||||
|
$this->info('Status Count: ' . $user->statuses()->count());
|
||||||
|
}
|
||||||
|
}
|
60
app/Console/Commands/UserSuspend.php
Normal file
60
app/Console/Commands/UserSuspend.php
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\User;
|
||||||
|
|
||||||
|
class UserSuspend extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'user:suspend {id}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Suspend a local user.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$id = $this->argument('id');
|
||||||
|
if(ctype_digit($id) == true) {
|
||||||
|
$user = User::find($id);
|
||||||
|
} else {
|
||||||
|
$user = User::whereUsername($id)->first();
|
||||||
|
}
|
||||||
|
if(!$user) {
|
||||||
|
$this->error('Could not find any user with that username or id.');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$this->info('Found user, username: ' . $user->username);
|
||||||
|
if($this->confirm('Are you sure you want to suspend this user?')) {
|
||||||
|
$profile = $user->profile;
|
||||||
|
$user->status = $profile->status = 'suspended';
|
||||||
|
$user->save();
|
||||||
|
$profile->save();
|
||||||
|
$this->info('User account has been suspended.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
app/Console/Commands/UserTable.php
Normal file
49
app/Console/Commands/UserTable.php
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\User;
|
||||||
|
|
||||||
|
class UserTable extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'user:table {limit=10}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Display latest users';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$limit = $this->argument('limit');
|
||||||
|
|
||||||
|
$headers = ['ID', 'Username', 'Name', 'Registered'];
|
||||||
|
|
||||||
|
$users = User::orderByDesc('id')->take($limit)->get(['id', 'username', 'name', 'created_at'])->toArray();
|
||||||
|
|
||||||
|
$this->table($headers, $users);
|
||||||
|
}
|
||||||
|
}
|
60
app/Console/Commands/UserUnsuspend.php
Normal file
60
app/Console/Commands/UserUnsuspend.php
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\User;
|
||||||
|
|
||||||
|
class UserUnsuspend extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'user:unsuspend {id}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Unsuspend a local user.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$id = $this->argument('id');
|
||||||
|
if(ctype_digit($id) == true) {
|
||||||
|
$user = User::find($id);
|
||||||
|
} else {
|
||||||
|
$user = User::whereUsername($id)->first();
|
||||||
|
}
|
||||||
|
if(!$user) {
|
||||||
|
$this->error('Could not find any user with that username or id.');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$this->info('Found user, username: ' . $user->username);
|
||||||
|
if($this->confirm('Are you sure you want to unsuspend this user?')) {
|
||||||
|
$profile = $user->profile;
|
||||||
|
$user->status = $profile->status = null;
|
||||||
|
$user->save();
|
||||||
|
$profile->save();
|
||||||
|
$this->info('User account has been unsuspended.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
app/Console/Commands/UserVerifyEmail.php
Normal file
53
app/Console/Commands/UserVerifyEmail.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
52
app/Console/Commands/VideoThumbnail.php
Normal file
52
app/Console/Commands/VideoThumbnail.php
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
use App\Media;
|
||||||
|
use App\Jobs\VideoPipeline\VideoThumbnail as Pipeline;
|
||||||
|
|
||||||
|
class VideoThumbnail extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'video:thumbnail';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Generate missing video thumbnails';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$limit = 10;
|
||||||
|
$videos = Media::whereMime('video/mp4')
|
||||||
|
->whereNull('thumbnail_path')
|
||||||
|
->take($limit)
|
||||||
|
->get();
|
||||||
|
foreach($videos as $video) {
|
||||||
|
Pipeline::dispatchNow($video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
app/Console/Kernel.php
Normal file
52
app/Console/Kernel.php
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console;
|
||||||
|
|
||||||
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
|
|
||||||
|
class Kernel extends ConsoleKernel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The Artisan commands provided by your application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $commands = [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the application's command schedule.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function schedule(Schedule $schedule)
|
||||||
|
{
|
||||||
|
$schedule->command('media:optimize')->hourlyAt(40);
|
||||||
|
$schedule->command('media:gc')->hourlyAt(5);
|
||||||
|
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||||
|
$schedule->command('story:gc')->everyFiveMinutes();
|
||||||
|
$schedule->command('gc:failedjobs')->dailyAt(3);
|
||||||
|
$schedule->command('gc:passwordreset')->dailyAt('09:41');
|
||||||
|
$schedule->command('gc:sessions')->twiceDaily(13, 23);
|
||||||
|
|
||||||
|
if(config('pixelfed.cloud_storage') && config('media.delete_local_after_cloud')) {
|
||||||
|
$schedule->command('media:s3gc')->hourlyAt(15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the commands for the application.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function commands()
|
||||||
|
{
|
||||||
|
$this->load(__DIR__.'/Commands');
|
||||||
|
|
||||||
|
require base_path('routes/console.php');
|
||||||
|
}
|
||||||
|
}
|
18
app/Contact.php
Normal file
18
app/Contact.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Contact extends Model
|
||||||
|
{
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function adminUrl()
|
||||||
|
{
|
||||||
|
return url('/i/admin/messages/show/' . $this->id);
|
||||||
|
}
|
||||||
|
}
|
50
app/DirectMessage.php
Normal file
50
app/DirectMessage.php
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Auth;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class DirectMessage extends Model
|
||||||
|
{
|
||||||
|
public function status()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Status::class, 'status_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url()
|
||||||
|
{
|
||||||
|
return config('app.url') . '/account/direct/m/' . $this->status_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function author()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'from_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function recipient()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'to_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function me()
|
||||||
|
{
|
||||||
|
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.";
|
||||||
|
}
|
||||||
|
}
|
64
app/DiscoverCategory.php
Normal file
64
app/DiscoverCategory.php
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\{Status, StatusHashtag};
|
||||||
|
|
||||||
|
class DiscoverCategory extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['slug'];
|
||||||
|
|
||||||
|
public function media()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Media::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url()
|
||||||
|
{
|
||||||
|
return url('/discover/c/'.$this->slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editUrl()
|
||||||
|
{
|
||||||
|
return url('/i/admin/discover/category/edit/' . $this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function thumb()
|
||||||
|
{
|
||||||
|
return $this->media->thumb();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mediaUrl()
|
||||||
|
{
|
||||||
|
return $this->media->url();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function items()
|
||||||
|
{
|
||||||
|
return $this->hasMany(DiscoverCategoryHashtag::class, 'discover_category_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hashtags()
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(
|
||||||
|
Hashtag::class,
|
||||||
|
DiscoverCategoryHashtag::class,
|
||||||
|
'discover_category_id',
|
||||||
|
'id',
|
||||||
|
'id',
|
||||||
|
'hashtag_id'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function posts()
|
||||||
|
{
|
||||||
|
return Status::select('*')
|
||||||
|
->join('status_hashtags', 'statuses.id', '=', 'status_hashtags.status_id')
|
||||||
|
->join('hashtags', 'status_hashtags.hashtag_id', '=', 'hashtags.id')
|
||||||
|
->join('discover_category_hashtags', 'hashtags.id', '=', 'discover_category_hashtags.hashtag_id')
|
||||||
|
->join('discover_categories', 'discover_category_hashtags.discover_category_id', '=', 'discover_categories.id')
|
||||||
|
->where('discover_categories.id', $this->id);
|
||||||
|
}
|
||||||
|
}
|
13
app/DiscoverCategoryHashtag.php
Normal file
13
app/DiscoverCategoryHashtag.php
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class DiscoverCategoryHashtag extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'discover_category_id',
|
||||||
|
'hashtag_id'
|
||||||
|
];
|
||||||
|
}
|
21
app/EmailVerification.php
Normal file
21
app/EmailVerification.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class EmailVerification extends Model
|
||||||
|
{
|
||||||
|
public function url()
|
||||||
|
{
|
||||||
|
$base = config('app.url');
|
||||||
|
$path = '/i/confirm-email/'.$this->user_token.'/'.$this->random_token;
|
||||||
|
|
||||||
|
return "{$base}{$path}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
51
app/Events/LiveStream/BanUser.php
Normal file
51
app/Events/LiveStream/BanUser.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?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];
|
||||||
|
}
|
||||||
|
}
|
51
app/Events/LiveStream/DeleteChatComment.php
Normal file
51
app/Events/LiveStream/DeleteChatComment.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?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']];
|
||||||
|
}
|
||||||
|
}
|
51
app/Events/LiveStream/NewChatComment.php
Normal file
51
app/Events/LiveStream/NewChatComment.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?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];
|
||||||
|
}
|
||||||
|
}
|
51
app/Events/LiveStream/PinChatMessage.php
Normal file
51
app/Events/LiveStream/PinChatMessage.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
48
app/Events/LiveStream/StreamEnd.php
Normal file
48
app/Events/LiveStream/StreamEnd.php
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<?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() ];
|
||||||
|
}
|
||||||
|
}
|
48
app/Events/LiveStream/StreamStart.php
Normal file
48
app/Events/LiveStream/StreamStart.php
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<?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() ];
|
||||||
|
}
|
||||||
|
}
|
51
app/Events/LiveStream/UnpinChatMessage.php
Normal file
51
app/Events/LiveStream/UnpinChatMessage.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
51
app/Events/NewMention.php
Normal file
51
app/Events/NewMention.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Broadcasting\PresenceChannel;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use App\User;
|
||||||
|
|
||||||
|
class NewMention implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
|
protected $user;
|
||||||
|
protected $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(User $user, $data)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastAs()
|
||||||
|
{
|
||||||
|
return 'notification.new.mention';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
return new PrivateChannel('App.User.' . $this->user->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastWith()
|
||||||
|
{
|
||||||
|
return ['id' => $this->user->id];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function via()
|
||||||
|
{
|
||||||
|
return 'broadcast';
|
||||||
|
}
|
||||||
|
}
|
57
app/Events/Notification/NewPublicPost.php
Normal file
57
app/Events/Notification/NewPublicPost.php
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events\Notification;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Broadcasting\PresenceChannel;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use App\Status;
|
||||||
|
use App\Transformer\Api\StatusTransformer;
|
||||||
|
use League\Fractal;
|
||||||
|
use League\Fractal\Serializer\ArraySerializer;
|
||||||
|
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||||
|
|
||||||
|
class NewPublicPost implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
|
protected $status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Status $status)
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastAs()
|
||||||
|
{
|
||||||
|
return 'status';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
return new Channel('firehost.public');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastWith()
|
||||||
|
{
|
||||||
|
$resource = new Fractal\Resource\Item($this->status, new StatusTransformer());
|
||||||
|
$res = $this->fractal->createData($resource)->toArray();
|
||||||
|
return [
|
||||||
|
'entity' => $res
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function via()
|
||||||
|
{
|
||||||
|
return 'broadcast';
|
||||||
|
}
|
||||||
|
}
|
87
app/Exceptions/Handler.php
Normal file
87
app/Exceptions/Handler.php
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
|
use Throwable;
|
||||||
|
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||||
|
|
||||||
|
class Handler extends ExceptionHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* A list of the exception types that are not reported.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $dontReport = [
|
||||||
|
OAuthServerException::class,
|
||||||
|
\Zttp\ConnectionException::class,
|
||||||
|
\GuzzleHttp\Exception\ConnectException::class,
|
||||||
|
\Illuminate\Http\Client\ConnectionException::class
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of the inputs that are never flashed for validation exceptions.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $dontFlash = [
|
||||||
|
'password',
|
||||||
|
'password_confirmation',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report or log an exception.
|
||||||
|
*
|
||||||
|
* @param \Exception $exception
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function report(Throwable $exception)
|
||||||
|
{
|
||||||
|
parent::report($exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the exception handling callbacks for the application.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
$this->reportable(function (\BadMethodCallException $e) {
|
||||||
|
return app()->environment() !== 'production';
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->reportable(function (\Illuminate\Http\Client\ConnectionException $e) {
|
||||||
|
return app()->environment() !== 'production';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an exception into an HTTP response.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Exception $exception
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
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()) {
|
||||||
|
return response()->json(
|
||||||
|
['error' => $exception->getMessage()],
|
||||||
|
method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return parent::render($request, $exception);
|
||||||
|
}
|
||||||
|
}
|
19
app/FailedJob.php
Normal file
19
app/FailedJob.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class FailedJob extends Model
|
||||||
|
{
|
||||||
|
const CREATED_AT = 'failed_at';
|
||||||
|
const UPDATED_AT = 'failed_at';
|
||||||
|
|
||||||
|
public $timestamps = 'failed_at';
|
||||||
|
|
||||||
|
public function getFailedAtAttribute($val)
|
||||||
|
{
|
||||||
|
return Carbon::parse($val);
|
||||||
|
}
|
||||||
|
}
|
40
app/FollowRequest.php
Normal file
40
app/FollowRequest.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function follower()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'follower_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function following()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'following_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);
|
||||||
|
}
|
||||||
|
}
|
51
app/Follower.php
Normal file
51
app/Follower.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Follower extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $fillable = ['profile_id', 'following_id', 'local_profile'];
|
||||||
|
|
||||||
|
const MAX_FOLLOWING = 7500;
|
||||||
|
const FOLLOW_PER_HOUR = 150;
|
||||||
|
|
||||||
|
public function actor()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function target()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'following_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function profile()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'following_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permalink($append = null)
|
||||||
|
{
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
}
|
19
app/HasSnowflakePrimary.php
Normal file
19
app/HasSnowflakePrimary.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use App\Services\SnowflakeService;
|
||||||
|
|
||||||
|
trait HasSnowflakePrimary
|
||||||
|
{
|
||||||
|
public static function bootHasSnowflakePrimary()
|
||||||
|
{
|
||||||
|
static::saving(function ($model) {
|
||||||
|
if (is_null($model->getKey())) {
|
||||||
|
$keyName = $model->getKeyName();
|
||||||
|
$id = SnowflakeService::next();
|
||||||
|
$model->setAttribute($keyName, $id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
27
app/Hashtag.php
Normal file
27
app/Hashtag.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Hashtag extends Model
|
||||||
|
{
|
||||||
|
public $fillable = ['name', 'slug'];
|
||||||
|
|
||||||
|
public function posts()
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(
|
||||||
|
Status::class,
|
||||||
|
StatusHashtag::class,
|
||||||
|
'hashtag_id',
|
||||||
|
'id',
|
||||||
|
'id',
|
||||||
|
'status_id'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url($suffix = '')
|
||||||
|
{
|
||||||
|
return config('routes.hashtag.base').$this->slug.$suffix;
|
||||||
|
}
|
||||||
|
}
|
19
app/HashtagFollow.php
Normal file
19
app/HashtagFollow.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class HashtagFollow extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'profile_id',
|
||||||
|
'hashtag_id'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function hashtag()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Hashtag::class);
|
||||||
|
}
|
||||||
|
}
|
636
app/Http/Controllers/AccountController.php
Normal file
636
app/Http/Controllers/AccountController.php
Normal file
|
@ -0,0 +1,636 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Auth;
|
||||||
|
use Cache;
|
||||||
|
use Mail;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use App\Mail\ConfirmEmail;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use PragmaRX\Google2FA\Google2FA;
|
||||||
|
use App\Jobs\FollowPipeline\FollowPipeline;
|
||||||
|
use App\{
|
||||||
|
DirectMessage,
|
||||||
|
EmailVerification,
|
||||||
|
Follower,
|
||||||
|
FollowRequest,
|
||||||
|
Media,
|
||||||
|
Notification,
|
||||||
|
Profile,
|
||||||
|
User,
|
||||||
|
UserDevice,
|
||||||
|
UserFilter,
|
||||||
|
UserSetting
|
||||||
|
};
|
||||||
|
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\NotificationService;
|
||||||
|
use App\Services\UserFilterService;
|
||||||
|
use App\Services\RelationshipService;
|
||||||
|
use App\Jobs\FollowPipeline\FollowAcceptPipeline;
|
||||||
|
use App\Jobs\FollowPipeline\FollowRejectPipeline;
|
||||||
|
|
||||||
|
class AccountController extends Controller
|
||||||
|
{
|
||||||
|
protected $filters = [
|
||||||
|
'user.mute',
|
||||||
|
'user.block',
|
||||||
|
];
|
||||||
|
|
||||||
|
const FILTER_LIMIT_MUTE_TEXT = 'You cannot mute more than ';
|
||||||
|
const FILTER_LIMIT_BLOCK_TEXT = 'You cannot block more than ';
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notifications(Request $request)
|
||||||
|
{
|
||||||
|
return view('account.activity');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function followingActivity(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'page' => 'nullable|min:1|max:3',
|
||||||
|
'a' => 'nullable|alpha_dash',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$action = $request->input('a');
|
||||||
|
$allowed = ['like', 'follow'];
|
||||||
|
$timeago = Carbon::now()->subMonths(3);
|
||||||
|
|
||||||
|
$profile = Auth::user()->profile;
|
||||||
|
$following = $profile->following->pluck('id');
|
||||||
|
|
||||||
|
$notifications = Notification::whereIn('actor_id', $following)
|
||||||
|
->whereIn('action', $allowed)
|
||||||
|
->where('actor_id', '<>', $profile->id)
|
||||||
|
->where('profile_id', '<>', $profile->id)
|
||||||
|
->whereDate('created_at', '>', $timeago)
|
||||||
|
->orderBy('notifications.created_at', 'desc')
|
||||||
|
->simplePaginate(30);
|
||||||
|
|
||||||
|
return view('account.following', compact('profile', 'notifications'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyEmail(Request $request)
|
||||||
|
{
|
||||||
|
$recentSent = EmailVerification::whereUserId(Auth::id())
|
||||||
|
->whereDate('created_at', '>', now()->subHours(12))->count();
|
||||||
|
|
||||||
|
return view('account.verify_email', compact('recentSent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendVerifyEmail(Request $request)
|
||||||
|
{
|
||||||
|
$recentAttempt = EmailVerification::whereUserId(Auth::id())
|
||||||
|
->whereDate('created_at', '>', now()->subHours(12))->count();
|
||||||
|
|
||||||
|
if ($recentAttempt > 0) {
|
||||||
|
return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailVerification::whereUserId(Auth::id())->delete();
|
||||||
|
|
||||||
|
$user = User::whereNull('email_verified_at')->find(Auth::id());
|
||||||
|
$utoken = Str::uuid() . Str::random(mt_rand(5,9));
|
||||||
|
$rtoken = Str::random(mt_rand(64, 70));
|
||||||
|
|
||||||
|
$verify = new EmailVerification();
|
||||||
|
$verify->user_id = $user->id;
|
||||||
|
$verify->email = $user->email;
|
||||||
|
$verify->user_token = $utoken;
|
||||||
|
$verify->random_token = $rtoken;
|
||||||
|
$verify->save();
|
||||||
|
|
||||||
|
Mail::to($user->email)->send(new ConfirmEmail($verify));
|
||||||
|
|
||||||
|
return redirect()->back()->with('status', 'Verification email sent!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
|
||||||
|
{
|
||||||
|
$verify = EmailVerification::where('user_token', $userToken)
|
||||||
|
->where('created_at', '>', now()->subHours(24))
|
||||||
|
->where('random_token', $randomToken)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
if (Auth::id() === $verify->user_id && $verify->user_token === $userToken && $verify->random_token === $randomToken) {
|
||||||
|
$user = User::find(Auth::id());
|
||||||
|
$user->email_verified_at = Carbon::now();
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
} else {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function direct()
|
||||||
|
{
|
||||||
|
return view('account.direct');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function directMessage(Request $request, $id)
|
||||||
|
{
|
||||||
|
$profile = Profile::where('id', '!=', $request->user()->profile_id)
|
||||||
|
// ->whereNull('domain')
|
||||||
|
->findOrFail($id);
|
||||||
|
return view('account.directmessage', compact('id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mute(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'type' => 'required|string|in:user',
|
||||||
|
'item' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pid = $request->user()->profile_id;
|
||||||
|
$count = UserFilterService::muteCount($pid);
|
||||||
|
$maxLimit = intval(config('instance.user_filters.max_user_mutes'));
|
||||||
|
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
|
||||||
|
if($count == 0) {
|
||||||
|
$filterCount = UserFilter::whereUserId($pid)->count();
|
||||||
|
abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
|
||||||
|
}
|
||||||
|
$type = $request->input('type');
|
||||||
|
$item = $request->input('item');
|
||||||
|
$action = $type . '.mute';
|
||||||
|
|
||||||
|
if (!in_array($action, $this->filters)) {
|
||||||
|
return abort(406);
|
||||||
|
}
|
||||||
|
$filterable = [];
|
||||||
|
switch ($type) {
|
||||||
|
case 'user':
|
||||||
|
$profile = Profile::findOrFail($item);
|
||||||
|
if ($profile->id == $pid) {
|
||||||
|
return abort(403);
|
||||||
|
}
|
||||||
|
$class = get_class($profile);
|
||||||
|
$filterable['id'] = $profile->id;
|
||||||
|
$filterable['type'] = $class;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filter = UserFilter::firstOrCreate([
|
||||||
|
'user_id' => $pid,
|
||||||
|
'filterable_id' => $filterable['id'],
|
||||||
|
'filterable_type' => $filterable['type'],
|
||||||
|
'filter_type' => 'mute',
|
||||||
|
]);
|
||||||
|
|
||||||
|
UserFilterService::mute($pid, $filterable['id']);
|
||||||
|
$res = RelationshipService::refresh($pid, $profile->id);
|
||||||
|
|
||||||
|
if($request->wantsJson()) {
|
||||||
|
return response()->json($res);
|
||||||
|
} else {
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unmute(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'type' => 'required|string|in:user',
|
||||||
|
'item' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pid = $request->user()->profile_id;
|
||||||
|
$type = $request->input('type');
|
||||||
|
$item = $request->input('item');
|
||||||
|
$action = $type . '.mute';
|
||||||
|
|
||||||
|
if (!in_array($action, $this->filters)) {
|
||||||
|
return abort(406);
|
||||||
|
}
|
||||||
|
$filterable = [];
|
||||||
|
switch ($type) {
|
||||||
|
case 'user':
|
||||||
|
$profile = Profile::findOrFail($item);
|
||||||
|
if ($profile->id == $pid) {
|
||||||
|
return abort(403);
|
||||||
|
}
|
||||||
|
$class = get_class($profile);
|
||||||
|
$filterable['id'] = $profile->id;
|
||||||
|
$filterable['type'] = $class;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
abort(400);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filter = UserFilter::whereUserId($pid)
|
||||||
|
->whereFilterableId($filterable['id'])
|
||||||
|
->whereFilterableType($filterable['type'])
|
||||||
|
->whereFilterType('mute')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if($filter) {
|
||||||
|
UserFilterService::unmute($pid, $filterable['id']);
|
||||||
|
$filter->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = RelationshipService::refresh($pid, $profile->id);
|
||||||
|
|
||||||
|
if($request->wantsJson()) {
|
||||||
|
return response()->json($res);
|
||||||
|
} else {
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function block(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'type' => 'required|string|in:user',
|
||||||
|
'item' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
$pid = $request->user()->profile_id;
|
||||||
|
$count = UserFilterService::blockCount($pid);
|
||||||
|
$maxLimit = intval(config('instance.user_filters.max_user_blocks'));
|
||||||
|
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
|
||||||
|
if($count == 0) {
|
||||||
|
$filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();
|
||||||
|
abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
|
||||||
|
}
|
||||||
|
$type = $request->input('type');
|
||||||
|
$item = $request->input('item');
|
||||||
|
$action = $type.'.block';
|
||||||
|
if (!in_array($action, $this->filters)) {
|
||||||
|
return abort(406);
|
||||||
|
}
|
||||||
|
$filterable = [];
|
||||||
|
switch ($type) {
|
||||||
|
case 'user':
|
||||||
|
$profile = Profile::findOrFail($item);
|
||||||
|
if ($profile->id == $pid || ($profile->user && $profile->user->is_admin == true)) {
|
||||||
|
return abort(403);
|
||||||
|
}
|
||||||
|
$class = get_class($profile);
|
||||||
|
$filterable['id'] = $profile->id;
|
||||||
|
$filterable['type'] = $class;
|
||||||
|
|
||||||
|
Follower::whereProfileId($profile->id)->whereFollowingId($pid)->delete();
|
||||||
|
Notification::whereProfileId($pid)
|
||||||
|
->whereActorId($profile->id)
|
||||||
|
->get()
|
||||||
|
->map(function($n) use($pid) {
|
||||||
|
NotificationService::del($pid, $n['id']);
|
||||||
|
$n->forceDelete();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filter = UserFilter::firstOrCreate([
|
||||||
|
'user_id' => $pid,
|
||||||
|
'filterable_id' => $filterable['id'],
|
||||||
|
'filterable_type' => $filterable['type'],
|
||||||
|
'filter_type' => 'block',
|
||||||
|
]);
|
||||||
|
|
||||||
|
UserFilterService::block($pid, $filterable['id']);
|
||||||
|
$res = RelationshipService::refresh($pid, $profile->id);
|
||||||
|
|
||||||
|
if($request->wantsJson()) {
|
||||||
|
return response()->json($res);
|
||||||
|
} else {
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unblock(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'type' => 'required|string|in:user',
|
||||||
|
'item' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pid = $request->user()->profile_id;
|
||||||
|
$type = $request->input('type');
|
||||||
|
$item = $request->input('item');
|
||||||
|
$action = $type . '.block';
|
||||||
|
if (!in_array($action, $this->filters)) {
|
||||||
|
return abort(406);
|
||||||
|
}
|
||||||
|
$filterable = [];
|
||||||
|
switch ($type) {
|
||||||
|
case 'user':
|
||||||
|
$profile = Profile::findOrFail($item);
|
||||||
|
if ($profile->id == $pid) {
|
||||||
|
return abort(403);
|
||||||
|
}
|
||||||
|
$class = get_class($profile);
|
||||||
|
$filterable['id'] = $profile->id;
|
||||||
|
$filterable['type'] = $class;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
abort(400);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$filter = UserFilter::whereUserId($pid)
|
||||||
|
->whereFilterableId($filterable['id'])
|
||||||
|
->whereFilterableType($filterable['type'])
|
||||||
|
->whereFilterType('block')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if($filter) {
|
||||||
|
UserFilterService::unblock($pid, $filterable['id']);
|
||||||
|
$filter->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = RelationshipService::refresh($pid, $profile->id);
|
||||||
|
|
||||||
|
if($request->wantsJson()) {
|
||||||
|
return response()->json($res);
|
||||||
|
} else {
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function followRequests(Request $request)
|
||||||
|
{
|
||||||
|
$pid = Auth::user()->profile->id;
|
||||||
|
$followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->simplePaginate(10);
|
||||||
|
return view('account.follow-requests', compact('followers'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function followRequestsJson(Request $request)
|
||||||
|
{
|
||||||
|
$pid = Auth::user()->profile_id;
|
||||||
|
$followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->get();
|
||||||
|
$res = [
|
||||||
|
'count' => $followers->count(),
|
||||||
|
'accounts' => $followers->take(10)->map(function($a) {
|
||||||
|
$actor = $a->actor;
|
||||||
|
return [
|
||||||
|
'rid' => (string) $a->id,
|
||||||
|
'id' => (string) $actor->id,
|
||||||
|
'username' => $actor->username,
|
||||||
|
'avatar' => $actor->avatarUrl(),
|
||||||
|
'url' => $actor->url(),
|
||||||
|
'local' => $actor->domain == null,
|
||||||
|
'account' => AccountService::get($actor->id)
|
||||||
|
];
|
||||||
|
})
|
||||||
|
];
|
||||||
|
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function followRequestHandle(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'action' => 'required|string|max:10',
|
||||||
|
'id' => 'required|integer|min:1'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pid = Auth::user()->profile->id;
|
||||||
|
$action = $request->input('action') === 'accept' ? 'accept' : 'reject';
|
||||||
|
$id = $request->input('id');
|
||||||
|
$followRequest = FollowRequest::whereFollowingId($pid)->findOrFail($id);
|
||||||
|
$follower = $followRequest->follower;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'reject':
|
||||||
|
if($follower->domain != null && $follower->private_key === null) {
|
||||||
|
FollowRejectPipeline::dispatch($followRequest)->onQueue('follow');
|
||||||
|
} else {
|
||||||
|
$followRequest->delete();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::forget('profile:follower_count:'.$pid);
|
||||||
|
Cache::forget('profile:following_count:'.$pid);
|
||||||
|
RelationshipService::refresh($pid, $follower->id);
|
||||||
|
|
||||||
|
return response()->json(['msg' => 'success'], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sudoMode(Request $request)
|
||||||
|
{
|
||||||
|
if($request->session()->has('sudoModeAttempts') && $request->session()->get('sudoModeAttempts') >= 3) {
|
||||||
|
$request->session()->pull('2fa.session.active');
|
||||||
|
$request->session()->pull('redirectNext');
|
||||||
|
$request->session()->pull('sudoModeAttempts');
|
||||||
|
Auth::logout();
|
||||||
|
return redirect(route('login'));
|
||||||
|
}
|
||||||
|
return view('auth.sudo');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sudoModeVerify(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'password' => 'required|string|max:500',
|
||||||
|
'trustDevice' => 'nullable'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
$password = $request->input('password');
|
||||||
|
$trustDevice = $request->input('trustDevice') == 'on';
|
||||||
|
$next = $request->session()->get('redirectNext', '/');
|
||||||
|
if($request->session()->has('sudoModeAttempts')) {
|
||||||
|
$count = (int) $request->session()->get('sudoModeAttempts');
|
||||||
|
$request->session()->put('sudoModeAttempts', $count + 1);
|
||||||
|
} else {
|
||||||
|
$request->session()->put('sudoModeAttempts', 1);
|
||||||
|
}
|
||||||
|
if(password_verify($password, $user->password) === true) {
|
||||||
|
$request->session()->put('sudoMode', time());
|
||||||
|
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()
|
||||||
|
->back()
|
||||||
|
->withErrors(['password' => __('auth.failed')]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function twoFactorCheckpoint(Request $request)
|
||||||
|
{
|
||||||
|
return view('auth.checkpoint');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function twoFactorVerify(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'code' => 'required|string|max:32'
|
||||||
|
]);
|
||||||
|
$user = Auth::user();
|
||||||
|
$code = $request->input('code');
|
||||||
|
$google2fa = new Google2FA();
|
||||||
|
$verify = $google2fa->verifyKey($user->{'2fa_secret'}, $code);
|
||||||
|
if($verify) {
|
||||||
|
$request->session()->push('2fa.session.active', true);
|
||||||
|
return redirect('/');
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if($this->twoFactorBackupCheck($request, $code, $user)) {
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->session()->has('2fa.attempts')) {
|
||||||
|
$count = (int) $request->session()->get('2fa.attempts');
|
||||||
|
if($count == 3) {
|
||||||
|
Auth::logout();
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
$request->session()->put('2fa.attempts', $count + 1);
|
||||||
|
} else {
|
||||||
|
$request->session()->put('2fa.attempts', 1);
|
||||||
|
}
|
||||||
|
return redirect('/i/auth/checkpoint')->withErrors([
|
||||||
|
'code' => 'Invalid code'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function twoFactorBackupCheck($request, $code, User $user)
|
||||||
|
{
|
||||||
|
$backupCodes = $user->{'2fa_backup_codes'};
|
||||||
|
if($backupCodes) {
|
||||||
|
$codes = json_decode($backupCodes, true);
|
||||||
|
foreach ($codes as $c) {
|
||||||
|
if(hash_equals($c, $code)) {
|
||||||
|
$codes = array_flatten(array_diff($codes, [$code]));
|
||||||
|
$user->{'2fa_backup_codes'} = json_encode($codes);
|
||||||
|
$user->save();
|
||||||
|
$request->session()->push('2fa.session.active', true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accountRestored(Request $request)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accountMutes(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'limit' => 'nullable|integer|min:1|max:40'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
$limit = $request->input('limit') ?? 40;
|
||||||
|
|
||||||
|
$mutes = UserFilter::whereUserId($user->profile_id)
|
||||||
|
->whereFilterableType('App\Profile')
|
||||||
|
->whereFilterType('mute')
|
||||||
|
->simplePaginate($limit)
|
||||||
|
->pluck('filterable_id');
|
||||||
|
|
||||||
|
$accounts = Profile::find($mutes);
|
||||||
|
$fractal = new Fractal\Manager();
|
||||||
|
$fractal->setSerializer(new ArraySerializer());
|
||||||
|
$resource = new Fractal\Resource\Collection($accounts, new AccountTransformer());
|
||||||
|
$res = $fractal->createData($resource)->toArray();
|
||||||
|
$url = $request->url();
|
||||||
|
$page = $request->input('page', 1);
|
||||||
|
$next = $page < 40 ? $page + 1 : 40;
|
||||||
|
$prev = $page > 1 ? $page - 1 : 1;
|
||||||
|
$links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
|
||||||
|
return response()->json($res, 200, ['Link' => $links]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accountBlocks(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'limit' => 'nullable|integer|min:1|max:40',
|
||||||
|
'page' => 'nullable|integer|min:1|max:10'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
$limit = $request->input('limit') ?? 40;
|
||||||
|
|
||||||
|
$blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
|
||||||
|
->whereUserId($user->profile_id)
|
||||||
|
->whereFilterableType('App\Profile')
|
||||||
|
->whereFilterType('block')
|
||||||
|
->simplePaginate($limit)
|
||||||
|
->pluck('filterable_id');
|
||||||
|
|
||||||
|
$profiles = Profile::findOrFail($blocked);
|
||||||
|
$fractal = new Fractal\Manager();
|
||||||
|
$fractal->setSerializer(new ArraySerializer());
|
||||||
|
$resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
|
||||||
|
$res = $fractal->createData($resource)->toArray();
|
||||||
|
$url = $request->url();
|
||||||
|
$page = $request->input('page', 1);
|
||||||
|
$next = $page < 40 ? $page + 1 : 40;
|
||||||
|
$prev = $page > 1 ? $page - 1 : 1;
|
||||||
|
$links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
|
||||||
|
return response()->json($res, 200, ['Link' => $links]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accountBlocksV2(Request $request)
|
||||||
|
{
|
||||||
|
return response()->json(UserFilterService::blocks($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accountMutesV2(Request $request)
|
||||||
|
{
|
||||||
|
return response()->json(UserFilterService::mutes($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accountFiltersV2(Request $request)
|
||||||
|
{
|
||||||
|
return response()->json(UserFilterService::filters($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
}
|
78
app/Http/Controllers/AccountInterstitialController.php
Normal file
78
app/Http/Controllers/AccountInterstitialController.php
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
use App\Status;
|
||||||
|
use App\AccountInterstitial;
|
||||||
|
|
||||||
|
class AccountInterstitialController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(Request $request)
|
||||||
|
{
|
||||||
|
$interstitial = $request->user()
|
||||||
|
->interstitials()
|
||||||
|
->whereNull('read_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if(!$interstitial) {
|
||||||
|
$user = $request->user();
|
||||||
|
$user->has_interstitial = false;
|
||||||
|
$user->save();
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = json_decode($interstitial->meta);
|
||||||
|
$view = $interstitial->view;
|
||||||
|
return view($view, compact('interstitial', 'meta'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'id' => 'required',
|
||||||
|
'type' => 'required|in:post.cw,post.removed,post.unlist,post.autospam',
|
||||||
|
'action' => 'required|in:appeal,confirm',
|
||||||
|
'appeal_message' => 'nullable|max:500'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$redirect = '/';
|
||||||
|
|
||||||
|
$id = decrypt($request->input('id'));
|
||||||
|
$action = $request->input('action');
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
$ai = AccountInterstitial::whereUserId($user->id)
|
||||||
|
->whereType($request->input('type'))
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
if($action == 'appeal') {
|
||||||
|
$ai->appeal_requested_at = now();
|
||||||
|
$ai->appeal_message = $request->input('appeal_message');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ai->read_at = now();
|
||||||
|
$ai->save();
|
||||||
|
|
||||||
|
$more = AccountInterstitial::whereUserId($user->id)
|
||||||
|
->whereNull('read_at')
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if(!$more) {
|
||||||
|
$user->has_interstitial = false;
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(in_array($ai->type, ['post.cw', 'post.unlist'])) {
|
||||||
|
$redirect = Status::findOrFail($ai->item_id)->url();
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect($redirect);
|
||||||
|
}
|
||||||
|
}
|
453
app/Http/Controllers/Admin/AdminDirectoryController.php
Normal file
453
app/Http/Controllers/Admin/AdminDirectoryController.php
Normal file
|
@ -0,0 +1,453 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use DB, Cache;
|
||||||
|
use App\{
|
||||||
|
DiscoverCategory,
|
||||||
|
DiscoverCategoryHashtag,
|
||||||
|
Hashtag,
|
||||||
|
Media,
|
||||||
|
Profile,
|
||||||
|
Status,
|
||||||
|
StatusHashtag,
|
||||||
|
User
|
||||||
|
};
|
||||||
|
use App\Models\ConfigCache;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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['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' => 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'),
|
||||||
|
];
|
||||||
|
|
||||||
|
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['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')->store('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' => config_cache('pixelfed.open_registration'),
|
||||||
|
'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
|
||||||
|
'oauth_enabled' => 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|accepted',
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
}
|
105
app/Http/Controllers/Admin/AdminDiscoverController.php
Normal file
105
app/Http/Controllers/Admin/AdminDiscoverController.php
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use DB, Cache;
|
||||||
|
use App\{
|
||||||
|
DiscoverCategory,
|
||||||
|
DiscoverCategoryHashtag,
|
||||||
|
Hashtag,
|
||||||
|
Media,
|
||||||
|
Profile,
|
||||||
|
StatusHashtag
|
||||||
|
};
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
trait AdminDiscoverController
|
||||||
|
{
|
||||||
|
public function discoverHome()
|
||||||
|
{
|
||||||
|
$categories = DiscoverCategory::orderByDesc('id')->paginate(10);
|
||||||
|
return view('admin.discover.home', compact('categories'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discoverCreateCategory()
|
||||||
|
{
|
||||||
|
return view('admin.discover.create-category');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discoverCreateCategoryStore(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'name' => 'required|string|min:1',
|
||||||
|
'active' => 'required|boolean',
|
||||||
|
'media' => 'nullable|integer|min:1'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$name = $request->input('name');
|
||||||
|
$slug = str_slug($name);
|
||||||
|
$active = $request->input('active');
|
||||||
|
$media = (int) $request->input('media');
|
||||||
|
|
||||||
|
$media = Media::findOrFail($media);
|
||||||
|
|
||||||
|
$category = DiscoverCategory::firstOrNew(['slug' => $slug]);
|
||||||
|
$category->name = $name;
|
||||||
|
$category->active = $active;
|
||||||
|
$category->media_id = $media->id;
|
||||||
|
$category->save();
|
||||||
|
return $category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discoverCategoryEdit(Request $request, $id)
|
||||||
|
{
|
||||||
|
$category = DiscoverCategory::findOrFail($id);
|
||||||
|
return view('admin.discover.show', compact('category'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discoverCategoryUpdate(Request $request, $id)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'name' => 'required|string|min:1',
|
||||||
|
'active' => 'required|boolean',
|
||||||
|
'media' => 'nullable|integer|min:1',
|
||||||
|
'hashtags' => 'nullable|string'
|
||||||
|
]);
|
||||||
|
$name = $request->input('name');
|
||||||
|
$slug = str_slug($name);
|
||||||
|
$active = $request->input('active');
|
||||||
|
$media = (int) $request->input('media');
|
||||||
|
$media = Media::findOrFail($media);
|
||||||
|
|
||||||
|
$category = DiscoverCategory::findOrFail($id);
|
||||||
|
$category->name = $name;
|
||||||
|
$category->active = $active;
|
||||||
|
$category->media_id = $media->id;
|
||||||
|
$category->save();
|
||||||
|
|
||||||
|
return $category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discoveryCategoryTagStore(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'category_id' => 'required|integer|min:1',
|
||||||
|
'hashtag' => 'required|string',
|
||||||
|
'action' => 'required|string|min:1|max:6'
|
||||||
|
]);
|
||||||
|
$category_id = $request->input('category_id');
|
||||||
|
$category = DiscoverCategory::findOrFail($category_id);
|
||||||
|
$hashtag = Hashtag::whereName($request->input('hashtag'))->firstOrFail();
|
||||||
|
|
||||||
|
$tag = DiscoverCategoryHashtag::firstOrCreate([
|
||||||
|
'hashtag_id' => $hashtag->id,
|
||||||
|
'discover_category_id' => $category->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
if($request->input('action') == 'delete') {
|
||||||
|
$tag->delete();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return $tag;
|
||||||
|
}
|
||||||
|
}
|
102
app/Http/Controllers/Admin/AdminHashtagsController.php
Normal file
102
app/Http/Controllers/Admin/AdminHashtagsController.php
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<?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 [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue