mirror of https://github.com/pixelfed/pixelfed.git
merge in staging
This commit is contained in:
commit
60e97d36b0
|
@ -4,4 +4,4 @@
|
|||
## Usage: redis-cli [flags] [args]
|
||||
## Example: "redis-cli KEYS *" or "ddev redis-cli INFO" or "ddev redis-cli --version"
|
||||
|
||||
redis-cli -p 6379 -h redis $@
|
||||
exec redis-cli -p 6379 -h redis "$@"
|
||||
|
|
|
@ -1,8 +1,30 @@
|
|||
data
|
||||
Dockerfile
|
||||
contrib/docker/Dockerfile.*
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.DS_Store
|
||||
/.bash_history
|
||||
/.bash_profile
|
||||
/.bashrc
|
||||
/.composer
|
||||
/.env
|
||||
/.env.dottie-backup
|
||||
/.git
|
||||
/.git-credentials
|
||||
/.gitconfig
|
||||
/.gitignore
|
||||
/.idea
|
||||
/.vagrant
|
||||
/bootstrap/cache
|
||||
/docker-compose-state/
|
||||
/Homestead.json
|
||||
/Homestead.yaml
|
||||
/node_modules
|
||||
/npm-debug.log
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/vendor/horizon
|
||||
/storage/*.key
|
||||
/storage/docker
|
||||
/vendor
|
||||
/yarn-error.log
|
||||
|
||||
# Exceptions - these *MUST* be last
|
||||
!/bootstrap/cache/.gitignore
|
||||
!/public/vendor/horizon/.gitignore
|
||||
|
|
|
@ -11,8 +11,23 @@ trim_trailing_whitespace = true
|
|||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{sh,envsh,env,env*}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# ShellCheck config
|
||||
shell_variant = bash # like -ln=bash
|
||||
binary_next_line = true # like -bn
|
||||
switch_case_indent = true # like -ci
|
||||
space_redirects = false # like -sr
|
||||
keep_padding = false # like -kp
|
||||
function_next_line = true # like -fn
|
||||
never_split = true # like -ns
|
||||
simplify = true
|
||||
|
|
1375
.env.docker
1375
.env.docker
File diff suppressed because it is too large
Load Diff
78
.env.example
78
.env.example
|
@ -1,78 +0,0 @@
|
|||
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
|
|
@ -1,3 +1,5 @@
|
|||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
APP_NAME="Pixelfed Test"
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E=
|
||||
|
@ -62,8 +64,8 @@ CS_BLOCKED_DOMAINS='example.org,example.net,example.com'
|
|||
CS_CW_DOMAINS='example.org,example.net,example.com'
|
||||
CS_UNLISTED_DOMAINS='example.org,example.net,example.com'
|
||||
|
||||
## Optional
|
||||
## Optional
|
||||
#HORIZON_DARKMODE=false # Horizon theme darkmode
|
||||
#HORIZON_EMBED=false # Single Docker Container mode
|
||||
#HORIZON_EMBED=false # Single Docker Container mode
|
||||
|
||||
ENABLE_CONFIG_CACHE=false
|
||||
|
|
|
@ -9,3 +9,10 @@
|
|||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
# Collapse diffs for generated files:
|
||||
public/**/*.js text -diff
|
||||
public/**/*.json text -diff
|
||||
public/**/*.css text -diff
|
||||
public/img/* binary -diff
|
||||
public/fonts/* binary -diff
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
---
|
||||
name: Build Docker image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/build-docker.yml
|
||||
- contrib/docker/Dockerfile.apache
|
||||
- contrib/docker/Dockerfile.fpm
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-docker-apache:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker Lint
|
||||
uses: hadolint/hadolint-action@v3.0.0
|
||||
with:
|
||||
dockerfile: contrib/docker/Dockerfile.apache
|
||||
failure-threshold: error
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
secrets: inherit
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Fetch tags
|
||||
uses: docker/metadata-action@v4
|
||||
secrets: inherit
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-apache
|
||||
tags: |
|
||||
type=edge,branch=dev
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=pr
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: contrib/docker/Dockerfile.apache
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-docker-fpm:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker Lint
|
||||
uses: hadolint/hadolint-action@v3.0.0
|
||||
with:
|
||||
dockerfile: contrib/docker/Dockerfile.fpm
|
||||
failure-threshold: error
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
secrets: inherit
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Fetch tags
|
||||
uses: docker/metadata-action@v4
|
||||
secrets: inherit
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
|
||||
flavor: |
|
||||
suffix=-fpm
|
||||
tags: |
|
||||
type=edge,branch=dev
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=pr
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: contrib/docker/Dockerfile.fpm
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
|
@ -0,0 +1,230 @@
|
|||
---
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
|
||||
workflow_dispatch:
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: hadolint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Lint
|
||||
uses: hadolint/hadolint-action@v3.1.0
|
||||
with:
|
||||
dockerfile: Dockerfile
|
||||
failure-threshold: error
|
||||
|
||||
shellcheck:
|
||||
name: ShellCheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run ShellCheck
|
||||
uses: ludeeus/action-shellcheck@master
|
||||
env:
|
||||
SHELLCHECK_OPTS: --shell=bash --external-sources
|
||||
with:
|
||||
version: v0.9.0
|
||||
additional_files: "*.envsh .env .env.docker .env.example .env.testing"
|
||||
|
||||
bats:
|
||||
name: Bats Testing
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run bats
|
||||
run: docker run -v "$PWD:/var/www" bats/bats:latest /var/www/tests/bats
|
||||
|
||||
build:
|
||||
name: Build, Test, and Push
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
|
||||
matrix:
|
||||
php_version:
|
||||
- 8.2
|
||||
- 8.3
|
||||
target_runtime:
|
||||
- apache
|
||||
- fpm
|
||||
- nginx
|
||||
php_base:
|
||||
- apache
|
||||
- fpm
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#excluding-matrix-configurations
|
||||
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixexclude
|
||||
exclude:
|
||||
# targeting [apache] runtime with [fpm] base type doesn't make sense
|
||||
- target_runtime: apache
|
||||
php_base: fpm
|
||||
|
||||
# targeting [fpm] runtime with [apache] base type doesn't make sense
|
||||
- target_runtime: fpm
|
||||
php_base: apache
|
||||
|
||||
# targeting [nginx] runtime with [apache] base type doesn't make sense
|
||||
- target_runtime: nginx
|
||||
php_base: apache
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-concurrency-and-the-default-behavior
|
||||
concurrency:
|
||||
group: docker-build-${{ github.ref }}-${{ matrix.php_base }}-${{ matrix.php_version }}-${{ matrix.target_runtime }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
# Set the repo variable [DOCKER_HUB_USERNAME] to override the default
|
||||
# at https://github.com/<user>/<project>/settings/variables/actions
|
||||
DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'pixelfed' }}
|
||||
|
||||
# Set the repo variable [DOCKER_HUB_ORGANISATION] to override the default
|
||||
# at https://github.com/<user>/<project>/settings/variables/actions
|
||||
DOCKER_HUB_ORGANISATION: ${{ vars.DOCKER_HUB_ORGANISATION || 'pixelfed' }}
|
||||
|
||||
# Set the repo variable [DOCKER_HUB_REPO] to override the default
|
||||
# at https://github.com/<user>/<project>/settings/variables/actions
|
||||
DOCKER_HUB_REPO: ${{ vars.DOCKER_HUB_REPO || 'pixelfed' }}
|
||||
|
||||
# For Docker Hub pushing to work, you need the secret [DOCKER_HUB_TOKEN]
|
||||
# set to your Personal Access Token at https://github.com/<user>/<project>/settings/secrets/actions
|
||||
#
|
||||
# ! NOTE: no [login] or [push] will happen to Docker Hub until this secret is set!
|
||||
HAS_DOCKER_HUB_CONFIGURED: ${{ secrets.DOCKER_HUB_TOKEN != '' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
id: buildx
|
||||
with:
|
||||
version: v0.12.0 # *or* newer, needed for annotations to work
|
||||
|
||||
# See: https://github.com/docker/login-action?tab=readme-ov-file#github-container-registry
|
||||
- name: Log in to the GitHub Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# See: https://github.com/docker/login-action?tab=readme-ov-file#docker-hub
|
||||
- name: Login to Docker Hub registry (conditionally)
|
||||
if: ${{ env.HAS_DOCKER_HUB_CONFIGURED == true }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ env.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Docker meta
|
||||
uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository }},enable=true
|
||||
name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }},enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }}
|
||||
tags: |
|
||||
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }}
|
||||
type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }}
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=branch,prefix=branch-
|
||||
type=ref,event=pr,prefix=pr-
|
||||
type=ref,event=tag
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
|
||||
- name: Docker meta (Cache)
|
||||
uses: docker/metadata-action@v5
|
||||
id: cache
|
||||
with:
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository }}-cache,enable=true
|
||||
name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }}-cache,enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }}
|
||||
tags: |
|
||||
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }}
|
||||
type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }}
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=branch,prefix=branch-
|
||||
type=ref,event=pr,prefix=pr-
|
||||
type=ref,event=tag
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
target: ${{ matrix.target_runtime }}-runtime
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
push: true
|
||||
sbom: true
|
||||
provenance: true
|
||||
build-args: |
|
||||
PHP_VERSION=${{ matrix.php_version }}
|
||||
PHP_BASE_TYPE=${{ matrix.php_base }}
|
||||
cache-from: |
|
||||
type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}
|
||||
${{ steps.cache.outputs.tags }}
|
||||
|
||||
# goss validate the image
|
||||
#
|
||||
# See: https://github.com/goss-org/goss
|
||||
- uses: e1himself/goss-installation-action@v1
|
||||
with:
|
||||
version: "v0.4.4"
|
||||
- name: Execute Goss tests
|
||||
run: |
|
||||
dgoss run \
|
||||
-v "./.env.testing:/var/www/.env" \
|
||||
-e "EXPECTED_PHP_VERSION=${{ matrix.php_version }}" \
|
||||
-e "PHP_BASE_TYPE=${{ matrix.php_base }}" \
|
||||
${{ steps.meta.outputs.tags }}
|
|
@ -1,22 +1,31 @@
|
|||
.DS_Store
|
||||
/.bash_history
|
||||
/.bash_profile
|
||||
/.bashrc
|
||||
/.composer
|
||||
/.env
|
||||
/.env.dottie-backup
|
||||
#/.git
|
||||
/.git-credentials
|
||||
/.gitconfig
|
||||
#/.gitignore
|
||||
/.idea
|
||||
/.vagrant
|
||||
/bootstrap/cache
|
||||
/docker-compose-state/
|
||||
/Homestead.json
|
||||
/Homestead.yaml
|
||||
/node_modules
|
||||
/npm-debug.log
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/vendor/horizon
|
||||
/storage/*.key
|
||||
/storage/docker
|
||||
/vendor
|
||||
/.idea
|
||||
/.vscode
|
||||
/.vagrant
|
||||
/docker-volumes
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.env
|
||||
.DS_Store
|
||||
.bash_profile
|
||||
.bash_history
|
||||
.bashrc
|
||||
.gitconfig
|
||||
.git-credentials
|
||||
/.composer/
|
||||
/nginx.conf
|
||||
/yarn-error.log
|
||||
/public/build
|
||||
|
||||
# Exceptions - these *MUST* be last
|
||||
!/bootstrap/cache/.gitignore
|
||||
!/public/vendor/horizon/.gitignore
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
ignored:
|
||||
- DL3002 # warning: Last USER should not be root
|
||||
- DL3008 # warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
|
||||
- SC2046 # warning: Quote this to prevent word splitting.
|
||||
- SC2086 # info: Double quote to prevent globbing and word splitting.
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"MD013": false,
|
||||
"MD014": false
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# See: https://github.com/koalaman/shellcheck/blob/master/shellcheck.1.md#rc-files
|
||||
|
||||
source-path=SCRIPTDIR
|
||||
|
||||
# Allow opening any 'source'd file, even if not specified as input
|
||||
external-sources=true
|
||||
|
||||
# Turn on warnings for unquoted variables with safe values
|
||||
enable=quote-safe-variables
|
||||
|
||||
# Turn on warnings for unassigned uppercase variables
|
||||
enable=check-unassigned-uppercase
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"foxundermoon.shell-format",
|
||||
"timonwong.shellcheck",
|
||||
"jetmartin.bats",
|
||||
"aaron-bond.better-comments",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"editorconfig.editorconfig",
|
||||
"github.vscode-github-actions",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"redhat.vscode-yaml",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"shellformat.useEditorConfig": true,
|
||||
"[shellscript]": {
|
||||
"files.eol": "\n",
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-yaml"
|
||||
},
|
||||
"[dockercompose]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-yaml",
|
||||
"editor.autoIndent": "advanced",
|
||||
},
|
||||
"yaml.schemas": {
|
||||
"https://json.schemastore.org/composer": "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json"
|
||||
},
|
||||
"files.associations": {
|
||||
".env": "shellscript",
|
||||
".env.*": "shellscript"
|
||||
}
|
||||
}
|
60
CHANGELOG.md
60
CHANGELOG.md
|
@ -1,6 +1,62 @@
|
|||
# Release Notes
|
||||
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.11...dev)
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.13...dev)
|
||||
|
||||
### Updates
|
||||
|
||||
- Update SoftwareUpdateService, add command to refresh latest versions ([632f2cb6](https://github.com/pixelfed/pixelfed/commit/632f2cb6))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13)
|
||||
|
||||
### Features
|
||||
|
||||
- Account Migrations ([#4968](https://github.com/pixelfed/pixelfed/pull/4968)) ([4a6be6212](https://github.com/pixelfed/pixelfed/pull/4968/commits/4a6be6212))
|
||||
- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
|
||||
- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4))
|
||||
- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e))
|
||||
- Improved Docker Support ([#4844](https://github.com/pixelfed/pixelfed/pull/4844)) ([d92cf7f](https://github.com/pixelfed/pixelfed/commit/d92cf7f))
|
||||
|
||||
### Updates
|
||||
|
||||
- Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad))
|
||||
- Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4))
|
||||
- Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623))
|
||||
- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9))
|
||||
- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e))
|
||||
- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d))
|
||||
- Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641))
|
||||
- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
|
||||
- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
|
||||
- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
|
||||
- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8))
|
||||
- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac))
|
||||
- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c))
|
||||
- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e))
|
||||
- Update Inbox, fix flag validation condition, allow profile reports ([402a4607](https://github.com/pixelfed/pixelfed/commit/402a4607))
|
||||
- Update AccountTransformer, fix follower/following count visibility bug ([542d1106](https://github.com/pixelfed/pixelfed/commit/542d1106))
|
||||
- Update ProfileMigration model, add target relation ([3f053997](https://github.com/pixelfed/pixelfed/commit/3f053997))
|
||||
- Update ApiV1Controller, update Notifications endpoint to filter notifications with missing activities ([a933615b](https://github.com/pixelfed/pixelfed/commit/a933615b))
|
||||
- Update ApiV1Controller, fix public timeline scope, properly support both local + remote parameters ([d6eac655](https://github.com/pixelfed/pixelfed/commit/d6eac655))
|
||||
- Update ApiV1Controller, handle public feed parameter bug to gracefully fallback to min_id=1 when max_id=0 ([e3826c58](https://github.com/pixelfed/pixelfed/commit/e3826c58))
|
||||
- Update ApiV1Controller, fix hashtag feed to include private posts from accounts you follow or your own, and your own unlisted posts ([3b5500b3](https://github.com/pixelfed/pixelfed/commit/3b5500b3))
|
||||
- Update checkpoint view, improve input autocomplete. Fixes ([#4959](https://github.com/pixelfed/pixelfed/pull/4959)) ([d18824e7](https://github.com/pixelfed/pixelfed/commit/d18824e7))
|
||||
- Update navbar.vue, removes the 50px limit ([#4969](https://github.com/pixelfed/pixelfed/pull/4969)) ([7fd5599](https://github.com/pixelfed/pixelfed/commit/7fd5599))
|
||||
- Update ComposeModal.vue, add an informative UI error message when trying to create a mixed media album ([#4886](https://github.com/pixelfed/pixelfed/pull/4886)) ([fd4f41a](https://github.com/pixelfed/pixelfed/commit/fd4f41a))
|
||||
|
||||
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)
|
||||
|
||||
### Features
|
||||
- Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a))
|
||||
- Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988))
|
||||
|
||||
### Updates
|
||||
|
||||
- Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3))
|
||||
- Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc))
|
||||
- Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3))
|
||||
- Update ActivityPubFetchService, enforce stricter Content-Type validation ([1232cfc8](https://github.com/pixelfed/pixelfed/commit/1232cfc8))
|
||||
- Update status view, fix unlisted/private scope bug ([0f3ca194](https://github.com/pixelfed/pixelfed/commit/0f3ca194))
|
||||
|
||||
## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11)
|
||||
|
||||
|
@ -27,7 +83,6 @@
|
|||
### Federation
|
||||
- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))
|
||||
- Update AP Helpers, consume actor `indexable` attribute ([fbdcdd9d](https://github.com/pixelfed/pixelfed/commit/fbdcdd9d))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
### Updates
|
||||
- Update FollowerService, add forget method to RelationshipService call to reduce load when mass purging ([347e4f59](https://github.com/pixelfed/pixelfed/commit/347e4f59))
|
||||
|
@ -112,7 +167,6 @@
|
|||
- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3))
|
||||
- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac))
|
||||
- Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! ([4d02d6f1](https://github.com/pixelfed/pixelfed/commit/4d02d6f1))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
* @dansup
|
||||
|
||||
# Docker related files
|
||||
.editorconfig @jippi @dansup
|
||||
.env @jippi @dansup
|
||||
.env.* @jippi @dansup
|
||||
.hadolint.yaml @jippi @dansup
|
||||
.shellcheckrc @jippi @dansup
|
||||
/.github/ @jippi @dansup
|
||||
/docker/ @jippi @dansup
|
||||
/tests/ @jippi @dansup
|
||||
docker-compose.migrate.yml @jippi @dansup
|
||||
docker-compose.yml @jippi @dansup
|
||||
goss.yaml @jippi @dansup
|
|
@ -0,0 +1,307 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# See https://hub.docker.com/r/docker/dockerfile
|
||||
|
||||
#######################################################
|
||||
# Configuration
|
||||
#######################################################
|
||||
|
||||
# See: https://github.com/mlocati/docker-php-extension-installer
|
||||
ARG DOCKER_PHP_EXTENSION_INSTALLER_VERSION="2.1.80"
|
||||
|
||||
# See: https://github.com/composer/composer
|
||||
ARG COMPOSER_VERSION="2.6"
|
||||
|
||||
# See: https://nginx.org/
|
||||
ARG NGINX_VERSION="1.25.3"
|
||||
|
||||
# See: https://github.com/ddollar/forego
|
||||
ARG FOREGO_VERSION="0.17.2"
|
||||
|
||||
# See: https://github.com/hairyhenderson/gomplate
|
||||
ARG GOMPLATE_VERSION="v3.11.6"
|
||||
|
||||
# See: https://github.com/jippi/dottie
|
||||
ARG DOTTIE_VERSION="v0.9.5"
|
||||
|
||||
###
|
||||
# PHP base configuration
|
||||
###
|
||||
|
||||
# See: https://hub.docker.com/_/php/tags
|
||||
ARG PHP_VERSION="8.1"
|
||||
|
||||
# See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants
|
||||
ARG PHP_BASE_TYPE="apache"
|
||||
ARG PHP_DEBIAN_RELEASE="bullseye"
|
||||
|
||||
ARG RUNTIME_UID=33 # often called 'www-data'
|
||||
ARG RUNTIME_GID=33 # often called 'www-data'
|
||||
|
||||
# APT extra packages
|
||||
ARG APT_PACKAGES_EXTRA=
|
||||
|
||||
# Extensions installed via [pecl install]
|
||||
# ! NOTE: imagick is installed from [master] branch on GitHub due to 8.3 bug on ARM that haven't
|
||||
# ! been released yet (after +10 months)!
|
||||
# ! See: https://github.com/Imagick/imagick/pull/641
|
||||
ARG PHP_PECL_EXTENSIONS="redis https://codeload.github.com/Imagick/imagick/tar.gz/28f27044e435a2b203e32675e942eb8de620ee58"
|
||||
ARG PHP_PECL_EXTENSIONS_EXTRA=
|
||||
|
||||
# Extensions installed via [docker-php-ext-install]
|
||||
ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd"
|
||||
ARG PHP_EXTENSIONS_EXTRA=""
|
||||
ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite"
|
||||
|
||||
# GPG key for nginx apt repository
|
||||
ARG NGINX_GPGKEY="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62"
|
||||
|
||||
# GPP key path for nginx apt repository
|
||||
ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg"
|
||||
|
||||
#######################################################
|
||||
# Docker "copy from" images
|
||||
#######################################################
|
||||
|
||||
# Composer docker image from Docker Hub
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM composer:${COMPOSER_VERSION} AS composer-image
|
||||
|
||||
# php-extension-installer image from Docker Hub
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM mlocati/php-extension-installer:${DOCKER_PHP_EXTENSION_INSTALLER_VERSION} AS php-extension-installer
|
||||
|
||||
# nginx webserver from Docker Hub.
|
||||
# Used to copy some docker-entrypoint files for [nginx-runtime]
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM nginx:${NGINX_VERSION} AS nginx-image
|
||||
|
||||
# Forego is a Procfile "runner" that makes it trival to run multiple
|
||||
# processes under a simple init / PID 1 process.
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
#
|
||||
# See: https://github.com/nginx-proxy/forego
|
||||
FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image
|
||||
|
||||
# Dottie makes working with .env files easier and safer
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
#
|
||||
# See: https://github.com/jippi/dottie
|
||||
FROM ghcr.io/jippi/dottie:${DOTTIE_VERSION} AS dottie-image
|
||||
|
||||
# gomplate-image grabs the gomplate binary from GitHub releases
|
||||
#
|
||||
# It's in its own layer so it can be fetched in parallel with other build steps
|
||||
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
ARG GOMPLATE_VERSION
|
||||
|
||||
RUN set -ex \
|
||||
&& curl \
|
||||
--silent \
|
||||
--show-error \
|
||||
--location \
|
||||
--output /usr/local/bin/gomplate \
|
||||
https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS}-${TARGETARCH} \
|
||||
&& chmod +x /usr/local/bin/gomplate \
|
||||
&& /usr/local/bin/gomplate --version
|
||||
|
||||
#######################################################
|
||||
# Base image
|
||||
#######################################################
|
||||
|
||||
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base
|
||||
|
||||
ARG BUILDKIT_SBOM_SCAN_STAGE="true"
|
||||
|
||||
ARG APT_PACKAGES_EXTRA
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_VERSION
|
||||
ARG RUNTIME_GID
|
||||
ARG RUNTIME_UID
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive"
|
||||
|
||||
# Ensure we run all scripts through 'bash' rather than 'sh'
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
RUN set -ex \
|
||||
&& mkdir -pv /var/www/ \
|
||||
&& chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www
|
||||
|
||||
WORKDIR /var/www/
|
||||
|
||||
ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA}
|
||||
|
||||
# Install and configure base layer
|
||||
COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh
|
||||
|
||||
RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
/docker/install/base.sh
|
||||
|
||||
#######################################################
|
||||
# PHP: extensions
|
||||
#######################################################
|
||||
|
||||
FROM base AS php-extensions
|
||||
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_EXTENSIONS
|
||||
ARG PHP_EXTENSIONS_DATABASE
|
||||
ARG PHP_EXTENSIONS_EXTRA
|
||||
ARG PHP_PECL_EXTENSIONS
|
||||
ARG PHP_PECL_EXTENSIONS_EXTRA
|
||||
ARG PHP_VERSION
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
|
||||
|
||||
COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh
|
||||
|
||||
RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/tmp/pear \
|
||||
--mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
PHP_EXTENSIONS=${PHP_EXTENSIONS} \
|
||||
PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} \
|
||||
PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} \
|
||||
PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} \
|
||||
PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} \
|
||||
/docker/install/php-extensions.sh
|
||||
|
||||
#######################################################
|
||||
# PHP: composer and source code
|
||||
#######################################################
|
||||
|
||||
FROM php-extensions AS composer-and-src
|
||||
|
||||
ARG PHP_VERSION
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG RUNTIME_UID
|
||||
ARG RUNTIME_GID
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Make sure composer cache is targeting our cache mount later
|
||||
ENV COMPOSER_CACHE_DIR="/cache/composer"
|
||||
|
||||
# Don't enforce any memory limits for composer
|
||||
ENV COMPOSER_MEMORY_LIMIT=-1
|
||||
|
||||
# Disable interactvitity from composer
|
||||
ENV COMPOSER_NO_INTERACTION=1
|
||||
|
||||
# Copy composer from https://hub.docker.com/_/composer
|
||||
COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer
|
||||
|
||||
#! Changing user to runtime user
|
||||
USER ${RUNTIME_UID}:${RUNTIME_GID}
|
||||
|
||||
# Install composer dependencies
|
||||
# NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet)
|
||||
RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION},sharing=locked,target=/cache/composer \
|
||||
--mount=type=bind,source=composer.json,target=/var/www/composer.json \
|
||||
--mount=type=bind,source=composer.lock,target=/var/www/composer.lock \
|
||||
set -ex \
|
||||
&& composer install --prefer-dist --no-autoloader --ignore-platform-reqs
|
||||
|
||||
# Copy all other files over
|
||||
COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/
|
||||
|
||||
#######################################################
|
||||
# Runtime: base
|
||||
#######################################################
|
||||
|
||||
FROM php-extensions AS shared-runtime
|
||||
|
||||
ARG RUNTIME_GID
|
||||
ARG RUNTIME_UID
|
||||
|
||||
ENV RUNTIME_UID=${RUNTIME_UID}
|
||||
ENV RUNTIME_GID=${RUNTIME_GID}
|
||||
|
||||
COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego
|
||||
COPY --link --from=dottie-image /dottie /usr/local/bin/dottie
|
||||
COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate
|
||||
COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer
|
||||
COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www
|
||||
|
||||
#! Changing user to runtime user
|
||||
USER ${RUNTIME_UID}:${RUNTIME_GID}
|
||||
|
||||
# Generate optimized autoloader now that we have all files around
|
||||
RUN set -ex \
|
||||
&& composer dump-autoload --optimize
|
||||
|
||||
USER root
|
||||
|
||||
# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862
|
||||
RUN set -ex \
|
||||
&& cp --recursive --link --preserve=all storage storage.skel \
|
||||
&& rm -rf html && ln -s public html
|
||||
|
||||
COPY docker/shared/root /
|
||||
|
||||
ENTRYPOINT ["/docker/entrypoint.sh"]
|
||||
|
||||
#######################################################
|
||||
# Runtime: apache
|
||||
#######################################################
|
||||
|
||||
FROM shared-runtime AS apache-runtime
|
||||
|
||||
COPY docker/apache/root /
|
||||
|
||||
RUN set -ex \
|
||||
&& a2enmod rewrite remoteip proxy proxy_http \
|
||||
&& a2enconf remoteip
|
||||
|
||||
CMD ["apache2-foreground"]
|
||||
|
||||
#######################################################
|
||||
# Runtime: fpm
|
||||
#######################################################
|
||||
|
||||
FROM shared-runtime AS fpm-runtime
|
||||
|
||||
COPY docker/fpm/root /
|
||||
|
||||
CMD ["php-fpm"]
|
||||
|
||||
#######################################################
|
||||
# Runtime: nginx
|
||||
#######################################################
|
||||
|
||||
FROM shared-runtime AS nginx-runtime
|
||||
|
||||
ARG NGINX_GPGKEY
|
||||
ARG NGINX_GPGKEY_PATH
|
||||
ARG NGINX_VERSION
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_VERSION
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Install nginx dependencies
|
||||
RUN --mount=type=cache,id=pixelfed-apt-lists-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt/lists \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
set -ex \
|
||||
&& gpg1 --keyserver "hkp://keyserver.ubuntu.com:80" --keyserver-options timeout=10 --recv-keys "${NGINX_GPGKEY}" \
|
||||
&& gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" \
|
||||
&& echo "deb [signed-by=${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ ${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nginx=${NGINX_VERSION}*
|
||||
|
||||
# copy docker entrypoints from the *real* nginx image directly
|
||||
COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/
|
||||
COPY docker/nginx/root /
|
||||
COPY docker/nginx/Procfile .
|
||||
|
||||
STOPSIGNAL SIGQUIT
|
||||
|
||||
CMD ["forego", "start", "-r"]
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\Internal\SoftwareUpdateService;
|
||||
use Cache;
|
||||
|
||||
class SoftwareUpdateRefresh extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:software-update-refresh';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Refresh latest software version data';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$key = SoftwareUpdateService::cacheKey();
|
||||
Cache::forget($key);
|
||||
Cache::remember($key, 1209600, function() {
|
||||
return SoftwareUpdateService::fetchLatest();
|
||||
});
|
||||
$this->info('Succesfully updated software versions!');
|
||||
}
|
||||
}
|
|
@ -25,31 +25,32 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('media:optimize')->hourlyAt(40);
|
||||
$schedule->command('media:gc')->hourlyAt(5);
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->command('story:gc')->everyFiveMinutes();
|
||||
$schedule->command('gc:failedjobs')->dailyAt(3);
|
||||
$schedule->command('gc:passwordreset')->dailyAt('09:41');
|
||||
$schedule->command('gc:sessions')->twiceDaily(13, 23);
|
||||
$schedule->command('media:optimize')->hourlyAt(40)->onOneServer();
|
||||
$schedule->command('media:gc')->hourlyAt(5)->onOneServer();
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
|
||||
$schedule->command('story:gc')->everyFiveMinutes()->onOneServer();
|
||||
$schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer();
|
||||
$schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer();
|
||||
$schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer();
|
||||
|
||||
if(in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) {
|
||||
if (in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) {
|
||||
$schedule->command('media:s3gc')->hourlyAt(15);
|
||||
}
|
||||
|
||||
if(config('import.instagram.enabled')) {
|
||||
$schedule->command('app:transform-imports')->everyTenMinutes();
|
||||
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51);
|
||||
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37);
|
||||
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32);
|
||||
if (config('import.instagram.enabled')) {
|
||||
$schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer();
|
||||
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer();
|
||||
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer();
|
||||
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer();
|
||||
|
||||
if(config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39);
|
||||
if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer();
|
||||
}
|
||||
}
|
||||
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21');
|
||||
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25);
|
||||
$schedule->command('app:account-post-count-stat-update')->everySixHours(25);
|
||||
|
||||
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer();
|
||||
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer();
|
||||
$schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,7 +60,7 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ trait AdminDirectoryController
|
|||
}
|
||||
|
||||
$res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
|
||||
$res['curated_onboarding'] = (bool) config_cache('instance.curated_registration.enabled');
|
||||
$res['open_registration'] = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
|
||||
|
||||
|
@ -124,7 +125,7 @@ trait AdminDirectoryController
|
|||
|
||||
$res['requirements_validator'] = $validator->errors();
|
||||
|
||||
$res['is_eligible'] = $res['open_registration'] &&
|
||||
$res['is_eligible'] = ($res['open_registration'] || $res['curated_onboarding']) &&
|
||||
$res['oauth_enabled'] &&
|
||||
$res['activitypub_enabled'] &&
|
||||
count($res['requirements_validator']) === 0 &&
|
||||
|
@ -227,7 +228,7 @@ trait AdminDirectoryController
|
|||
->each(function($name) {
|
||||
Storage::delete($name);
|
||||
});
|
||||
$path = $request->file('banner_image')->store('public/headers');
|
||||
$path = $request->file('banner_image')->storePublicly('public/headers');
|
||||
$res['banner_image'] = $path;
|
||||
ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
|
||||
|
||||
|
@ -249,7 +250,8 @@ trait AdminDirectoryController
|
|||
{
|
||||
$reqs = [];
|
||||
$reqs['feature_config'] = [
|
||||
'open_registration' => config_cache('pixelfed.open_registration'),
|
||||
'open_registration' => (bool) config_cache('pixelfed.open_registration'),
|
||||
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
|
||||
'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
|
||||
'oauth_enabled' => config_cache('pixelfed.oauth_enabled'),
|
||||
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
|
||||
|
@ -265,7 +267,8 @@ trait AdminDirectoryController
|
|||
];
|
||||
|
||||
$validator = Validator::make($reqs['feature_config'], [
|
||||
'open_registration' => 'required|accepted',
|
||||
'open_registration' => 'required_unless:curated_onboarding,true',
|
||||
'curated_onboarding' => 'required_unless:open_registration,true',
|
||||
'activitypub_enabled' => 'required|accepted',
|
||||
'oauth_enabled' => 'required|accepted',
|
||||
'media_types' => [
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,284 +2,312 @@
|
|||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use Artisan, Cache, DB;
|
||||
use Illuminate\Http\Request;
|
||||
use Carbon\Carbon;
|
||||
use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
|
||||
use App\Models\InstanceActor;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use App\Models\ConfigCache;
|
||||
use App\Models\InstanceActor;
|
||||
use App\Page;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\User;
|
||||
use App\Util\Site\Config;
|
||||
use Illuminate\Support\Str;
|
||||
use Artisan;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait AdminSettingsController
|
||||
{
|
||||
public function settings(Request $request)
|
||||
{
|
||||
$cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage');
|
||||
$cloud_disk = config('filesystems.cloud');
|
||||
$cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret'));
|
||||
$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
|
||||
$rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null;
|
||||
$jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types);
|
||||
$png = in_array('image/png', $types);
|
||||
$gif = in_array('image/gif', $types);
|
||||
$mp4 = in_array('video/mp4', $types);
|
||||
$webp = in_array('image/webp', $types);
|
||||
public function settings(Request $request)
|
||||
{
|
||||
$cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage');
|
||||
$cloud_disk = config('filesystems.cloud');
|
||||
$cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
|
||||
$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
|
||||
$rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null;
|
||||
$jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types);
|
||||
$png = in_array('image/png', $types);
|
||||
$gif = in_array('image/gif', $types);
|
||||
$mp4 = in_array('video/mp4', $types);
|
||||
$webp = in_array('image/webp', $types);
|
||||
|
||||
$availableAdmins = User::whereIsAdmin(true)->get();
|
||||
$currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null;
|
||||
$availableAdmins = User::whereIsAdmin(true)->get();
|
||||
$currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null;
|
||||
$openReg = (bool) config_cache('pixelfed.open_registration');
|
||||
$curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
|
||||
$regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed');
|
||||
$accountMigration = (bool) config_cache('federation.migration');
|
||||
|
||||
// $system = [
|
||||
// 'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')),
|
||||
// 'max_upload_size' => ini_get('post_max_size'),
|
||||
// 'image_driver' => config('image.driver'),
|
||||
// 'image_driver_loaded' => extension_loaded(config('image.driver'))
|
||||
// ];
|
||||
return view('admin.settings.home', compact(
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'mp4',
|
||||
'webp',
|
||||
'rules',
|
||||
'cloud_storage',
|
||||
'cloud_disk',
|
||||
'cloud_ready',
|
||||
'availableAdmins',
|
||||
'currentAdmin',
|
||||
'regState',
|
||||
'accountMigration'
|
||||
));
|
||||
}
|
||||
|
||||
return view('admin.settings.home', compact(
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'mp4',
|
||||
'webp',
|
||||
'rules',
|
||||
'cloud_storage',
|
||||
'cloud_disk',
|
||||
'cloud_ready',
|
||||
'availableAdmins',
|
||||
'currentAdmin'
|
||||
// 'system'
|
||||
));
|
||||
}
|
||||
public function settingsHomeStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'nullable|string',
|
||||
'short_description' => 'nullable',
|
||||
'long_description' => 'nullable',
|
||||
'max_photo_size' => 'nullable|integer|min:1',
|
||||
'max_album_length' => 'nullable|integer|min:1|max:100',
|
||||
'image_quality' => 'nullable|integer|min:1|max:100',
|
||||
'type_jpeg' => 'nullable',
|
||||
'type_png' => 'nullable',
|
||||
'type_gif' => 'nullable',
|
||||
'type_mp4' => 'nullable',
|
||||
'type_webp' => 'nullable',
|
||||
'admin_account_id' => 'nullable',
|
||||
'regs' => 'required|in:open,filtered,closed',
|
||||
'account_migration' => 'nullable',
|
||||
]);
|
||||
|
||||
public function settingsHomeStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'nullable|string',
|
||||
'short_description' => 'nullable',
|
||||
'long_description' => 'nullable',
|
||||
'max_photo_size' => 'nullable|integer|min:1',
|
||||
'max_album_length' => 'nullable|integer|min:1|max:100',
|
||||
'image_quality' => 'nullable|integer|min:1|max:100',
|
||||
'type_jpeg' => 'nullable',
|
||||
'type_png' => 'nullable',
|
||||
'type_gif' => 'nullable',
|
||||
'type_mp4' => 'nullable',
|
||||
'type_webp' => 'nullable',
|
||||
'admin_account_id' => 'nullable',
|
||||
]);
|
||||
$orb = false;
|
||||
$cob = false;
|
||||
switch ($request->input('regs')) {
|
||||
case 'open':
|
||||
$orb = true;
|
||||
$cob = false;
|
||||
break;
|
||||
|
||||
if($request->filled('admin_account_id')) {
|
||||
ConfigCacheService::put('instance.admin.pid', $request->admin_account_id);
|
||||
Cache::forget('api:v1:instance-data:contact');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
}
|
||||
if($request->filled('rule_delete')) {
|
||||
$index = (int) $request->input('rule_delete');
|
||||
$rules = ConfigCacheService::get('app.rules');
|
||||
$json = json_decode($rules, true);
|
||||
if(!$rules || empty($json)) {
|
||||
return;
|
||||
}
|
||||
unset($json[$index]);
|
||||
$json = json_encode(array_values($json));
|
||||
ConfigCacheService::put('app.rules', $json);
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
return 200;
|
||||
}
|
||||
case 'filtered':
|
||||
$orb = false;
|
||||
$cob = true;
|
||||
break;
|
||||
|
||||
$media_types = explode(',', config_cache('pixelfed.media_types'));
|
||||
$media_types_original = $media_types;
|
||||
case 'closed':
|
||||
$orb = false;
|
||||
$cob = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$mimes = [
|
||||
'type_jpeg' => 'image/jpeg',
|
||||
'type_png' => 'image/png',
|
||||
'type_gif' => 'image/gif',
|
||||
'type_mp4' => 'video/mp4',
|
||||
'type_webp' => 'image/webp',
|
||||
];
|
||||
ConfigCacheService::put('pixelfed.open_registration', (bool) $orb);
|
||||
ConfigCacheService::put('instance.curated_registration.enabled', (bool) $cob);
|
||||
|
||||
foreach ($mimes as $key => $value) {
|
||||
if($request->input($key) == 'on') {
|
||||
if(!in_array($value, $media_types)) {
|
||||
array_push($media_types, $value);
|
||||
}
|
||||
} else {
|
||||
$media_types = array_diff($media_types, [$value]);
|
||||
}
|
||||
}
|
||||
if ($request->filled('admin_account_id')) {
|
||||
ConfigCacheService::put('instance.admin.pid', $request->admin_account_id);
|
||||
Cache::forget('api:v1:instance-data:contact');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
}
|
||||
if ($request->filled('rule_delete')) {
|
||||
$index = (int) $request->input('rule_delete');
|
||||
$rules = ConfigCacheService::get('app.rules');
|
||||
$json = json_decode($rules, true);
|
||||
if (! $rules || empty($json)) {
|
||||
return;
|
||||
}
|
||||
unset($json[$index]);
|
||||
$json = json_encode(array_values($json));
|
||||
ConfigCacheService::put('app.rules', $json);
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
|
||||
if($media_types !== $media_types_original) {
|
||||
ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types)));
|
||||
}
|
||||
return 200;
|
||||
}
|
||||
|
||||
$keys = [
|
||||
'name' => 'app.name',
|
||||
'short_description' => 'app.short_description',
|
||||
'long_description' => 'app.description',
|
||||
'max_photo_size' => 'pixelfed.max_photo_size',
|
||||
'max_album_length' => 'pixelfed.max_album_length',
|
||||
'image_quality' => 'pixelfed.image_quality',
|
||||
'account_limit' => 'pixelfed.max_account_size',
|
||||
'custom_css' => 'uikit.custom.css',
|
||||
'custom_js' => 'uikit.custom.js',
|
||||
'about_title' => 'about.title'
|
||||
];
|
||||
$media_types = explode(',', config_cache('pixelfed.media_types'));
|
||||
$media_types_original = $media_types;
|
||||
|
||||
foreach ($keys as $key => $value) {
|
||||
$cc = ConfigCache::whereK($value)->first();
|
||||
$val = $request->input($key);
|
||||
if($cc && $cc->v != $val) {
|
||||
ConfigCacheService::put($value, $val);
|
||||
} else if(!empty($val)) {
|
||||
ConfigCacheService::put($value, $val);
|
||||
}
|
||||
}
|
||||
$mimes = [
|
||||
'type_jpeg' => 'image/jpeg',
|
||||
'type_png' => 'image/png',
|
||||
'type_gif' => 'image/gif',
|
||||
'type_mp4' => 'video/mp4',
|
||||
'type_webp' => 'image/webp',
|
||||
];
|
||||
|
||||
$bools = [
|
||||
'activitypub' => 'federation.activitypub.enabled',
|
||||
'open_registration' => 'pixelfed.open_registration',
|
||||
'mobile_apis' => 'pixelfed.oauth_enabled',
|
||||
'stories' => 'instance.stories.enabled',
|
||||
'ig_import' => 'pixelfed.import.instagram.enabled',
|
||||
'spam_detection' => 'pixelfed.bouncer.enabled',
|
||||
'require_email_verification' => 'pixelfed.enforce_email_verification',
|
||||
'enforce_account_limit' => 'pixelfed.enforce_account_limit',
|
||||
'show_custom_css' => 'uikit.show_custom.css',
|
||||
'show_custom_js' => 'uikit.show_custom.js',
|
||||
'cloud_storage' => 'pixelfed.cloud_storage',
|
||||
'account_autofollow' => 'account.autofollow',
|
||||
'show_directory' => 'instance.landing.show_directory',
|
||||
'show_explore_feed' => 'instance.landing.show_explore',
|
||||
];
|
||||
foreach ($mimes as $key => $value) {
|
||||
if ($request->input($key) == 'on') {
|
||||
if (! in_array($value, $media_types)) {
|
||||
array_push($media_types, $value);
|
||||
}
|
||||
} else {
|
||||
$media_types = array_diff($media_types, [$value]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($bools as $key => $value) {
|
||||
$active = $request->input($key) == 'on';
|
||||
if ($media_types !== $media_types_original) {
|
||||
ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types)));
|
||||
}
|
||||
|
||||
if($key == 'activitypub' && $active && !InstanceActor::exists()) {
|
||||
Artisan::call('instance:actor');
|
||||
}
|
||||
$keys = [
|
||||
'name' => 'app.name',
|
||||
'short_description' => 'app.short_description',
|
||||
'long_description' => 'app.description',
|
||||
'max_photo_size' => 'pixelfed.max_photo_size',
|
||||
'max_album_length' => 'pixelfed.max_album_length',
|
||||
'image_quality' => 'pixelfed.image_quality',
|
||||
'account_limit' => 'pixelfed.max_account_size',
|
||||
'custom_css' => 'uikit.custom.css',
|
||||
'custom_js' => 'uikit.custom.js',
|
||||
'about_title' => 'about.title',
|
||||
];
|
||||
|
||||
if( $key == 'mobile_apis' &&
|
||||
$active &&
|
||||
!file_exists(storage_path('oauth-public.key')) &&
|
||||
!file_exists(storage_path('oauth-private.key'))
|
||||
) {
|
||||
Artisan::call('passport:keys');
|
||||
Artisan::call('route:cache');
|
||||
}
|
||||
foreach ($keys as $key => $value) {
|
||||
$cc = ConfigCache::whereK($value)->first();
|
||||
$val = $request->input($key);
|
||||
if ($cc && $cc->v != $val) {
|
||||
ConfigCacheService::put($value, $val);
|
||||
} elseif (! empty($val)) {
|
||||
ConfigCacheService::put($value, $val);
|
||||
}
|
||||
}
|
||||
|
||||
if(config_cache($value) !== $active) {
|
||||
ConfigCacheService::put($value, (bool) $active);
|
||||
}
|
||||
}
|
||||
$bools = [
|
||||
'activitypub' => 'federation.activitypub.enabled',
|
||||
// 'open_registration' => 'pixelfed.open_registration',
|
||||
'mobile_apis' => 'pixelfed.oauth_enabled',
|
||||
'stories' => 'instance.stories.enabled',
|
||||
'ig_import' => 'pixelfed.import.instagram.enabled',
|
||||
'spam_detection' => 'pixelfed.bouncer.enabled',
|
||||
'require_email_verification' => 'pixelfed.enforce_email_verification',
|
||||
'enforce_account_limit' => 'pixelfed.enforce_account_limit',
|
||||
'show_custom_css' => 'uikit.show_custom.css',
|
||||
'show_custom_js' => 'uikit.show_custom.js',
|
||||
'cloud_storage' => 'pixelfed.cloud_storage',
|
||||
'account_autofollow' => 'account.autofollow',
|
||||
'show_directory' => 'instance.landing.show_directory',
|
||||
'show_explore_feed' => 'instance.landing.show_explore',
|
||||
'account_migration' => 'federation.migration',
|
||||
];
|
||||
|
||||
if($request->filled('new_rule')) {
|
||||
$rules = ConfigCacheService::get('app.rules');
|
||||
$val = $request->input('new_rule');
|
||||
if(!$rules) {
|
||||
ConfigCacheService::put('app.rules', json_encode([$val]));
|
||||
} else {
|
||||
$json = json_decode($rules, true);
|
||||
$json[] = $val;
|
||||
ConfigCacheService::put('app.rules', json_encode(array_values($json)));
|
||||
}
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
}
|
||||
foreach ($bools as $key => $value) {
|
||||
$active = $request->input($key) == 'on';
|
||||
|
||||
if($request->filled('account_autofollow_usernames')) {
|
||||
$usernames = explode(',', $request->input('account_autofollow_usernames'));
|
||||
$names = [];
|
||||
if ($key == 'activitypub' && $active && ! InstanceActor::exists()) {
|
||||
Artisan::call('instance:actor');
|
||||
}
|
||||
|
||||
foreach($usernames as $n) {
|
||||
$p = Profile::whereUsername($n)->first();
|
||||
if(!$p) {
|
||||
continue;
|
||||
}
|
||||
array_push($names, $p->username);
|
||||
}
|
||||
if ($key == 'mobile_apis' &&
|
||||
$active &&
|
||||
! file_exists(storage_path('oauth-public.key')) &&
|
||||
! file_exists(storage_path('oauth-private.key'))
|
||||
) {
|
||||
Artisan::call('passport:keys');
|
||||
Artisan::call('route:cache');
|
||||
}
|
||||
|
||||
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
|
||||
}
|
||||
if (config_cache($value) !== $active) {
|
||||
ConfigCacheService::put($value, (bool) $active);
|
||||
}
|
||||
}
|
||||
|
||||
Cache::forget(Config::CACHE_KEY);
|
||||
if ($request->filled('new_rule')) {
|
||||
$rules = ConfigCacheService::get('app.rules');
|
||||
$val = $request->input('new_rule');
|
||||
if (! $rules) {
|
||||
ConfigCacheService::put('app.rules', json_encode([$val]));
|
||||
} else {
|
||||
$json = json_decode($rules, true);
|
||||
$json[] = $val;
|
||||
ConfigCacheService::put('app.rules', json_encode(array_values($json)));
|
||||
}
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
}
|
||||
|
||||
return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!');
|
||||
}
|
||||
if ($request->filled('account_autofollow_usernames')) {
|
||||
$usernames = explode(',', $request->input('account_autofollow_usernames'));
|
||||
$names = [];
|
||||
|
||||
public function settingsBackups(Request $request)
|
||||
{
|
||||
$path = storage_path('app/'.config('app.name'));
|
||||
$files = is_dir($path) ? new \DirectoryIterator($path) : [];
|
||||
return view('admin.settings.backups', compact('files'));
|
||||
}
|
||||
foreach ($usernames as $n) {
|
||||
$p = Profile::whereUsername($n)->first();
|
||||
if (! $p) {
|
||||
continue;
|
||||
}
|
||||
array_push($names, $p->username);
|
||||
}
|
||||
|
||||
public function settingsMaintenance(Request $request)
|
||||
{
|
||||
return view('admin.settings.maintenance');
|
||||
}
|
||||
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
|
||||
}
|
||||
|
||||
public function settingsStorage(Request $request)
|
||||
{
|
||||
$storage = [];
|
||||
return view('admin.settings.storage', compact('storage'));
|
||||
}
|
||||
Cache::forget(Config::CACHE_KEY);
|
||||
|
||||
public function settingsFeatures(Request $request)
|
||||
{
|
||||
return view('admin.settings.features');
|
||||
}
|
||||
return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!');
|
||||
}
|
||||
|
||||
public function settingsPages(Request $request)
|
||||
{
|
||||
$pages = Page::orderByDesc('updated_at')->paginate(10);
|
||||
return view('admin.pages.home', compact('pages'));
|
||||
}
|
||||
public function settingsBackups(Request $request)
|
||||
{
|
||||
$path = storage_path('app/'.config('app.name'));
|
||||
$files = is_dir($path) ? new \DirectoryIterator($path) : [];
|
||||
|
||||
public function settingsPageEdit(Request $request)
|
||||
{
|
||||
return view('admin.pages.edit');
|
||||
}
|
||||
return view('admin.settings.backups', compact('files'));
|
||||
}
|
||||
|
||||
public function settingsSystem(Request $request)
|
||||
{
|
||||
$sys = [
|
||||
'pixelfed' => config('pixelfed.version'),
|
||||
'php' => phpversion(),
|
||||
'laravel' => app()->version(),
|
||||
];
|
||||
switch (config('database.default')) {
|
||||
case 'pgsql':
|
||||
$exp = DB::raw('select version();');
|
||||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||
$sys['database'] = [
|
||||
'name' => 'Postgres',
|
||||
'version' => explode(' ', DB::select($expQuery)[0]->version)[1]
|
||||
];
|
||||
break;
|
||||
public function settingsMaintenance(Request $request)
|
||||
{
|
||||
return view('admin.settings.maintenance');
|
||||
}
|
||||
|
||||
case 'mysql':
|
||||
$exp = DB::raw('select version()');
|
||||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||
$sys['database'] = [
|
||||
'name' => 'MySQL',
|
||||
'version' => DB::select($expQuery)[0]->{'version()'}
|
||||
];
|
||||
break;
|
||||
public function settingsStorage(Request $request)
|
||||
{
|
||||
$storage = [];
|
||||
|
||||
default:
|
||||
$sys['database'] = [
|
||||
'name' => 'Unknown',
|
||||
'version' => '?'
|
||||
];
|
||||
break;
|
||||
}
|
||||
return view('admin.settings.system', compact('sys'));
|
||||
}
|
||||
return view('admin.settings.storage', compact('storage'));
|
||||
}
|
||||
|
||||
public function settingsFeatures(Request $request)
|
||||
{
|
||||
return view('admin.settings.features');
|
||||
}
|
||||
|
||||
public function settingsPages(Request $request)
|
||||
{
|
||||
$pages = Page::orderByDesc('updated_at')->paginate(10);
|
||||
|
||||
return view('admin.pages.home', compact('pages'));
|
||||
}
|
||||
|
||||
public function settingsPageEdit(Request $request)
|
||||
{
|
||||
return view('admin.pages.edit');
|
||||
}
|
||||
|
||||
public function settingsSystem(Request $request)
|
||||
{
|
||||
$sys = [
|
||||
'pixelfed' => config('pixelfed.version'),
|
||||
'php' => phpversion(),
|
||||
'laravel' => app()->version(),
|
||||
];
|
||||
switch (config('database.default')) {
|
||||
case 'pgsql':
|
||||
$exp = DB::raw('select version();');
|
||||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||
$sys['database'] = [
|
||||
'name' => 'Postgres',
|
||||
'version' => explode(' ', DB::select($expQuery)[0]->version)[1],
|
||||
];
|
||||
break;
|
||||
|
||||
case 'mysql':
|
||||
$exp = DB::raw('select version()');
|
||||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||
$sys['database'] = [
|
||||
'name' => 'MySQL',
|
||||
'version' => DB::select($expQuery)[0]->{'version()'},
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
$sys['database'] = [
|
||||
'name' => 'Unknown',
|
||||
'version' => '?',
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return view('admin.settings.system', compact('sys'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,335 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Mail\CuratedRegisterAcceptUser;
|
||||
use App\Mail\CuratedRegisterRejectUser;
|
||||
use App\Mail\CuratedRegisterRequestDetailsFromUser;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\Models\CuratedRegisterActivity;
|
||||
use App\Models\CuratedRegisterTemplate;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdminCuratedRegisterController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(['auth', 'admin']);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses',
|
||||
'sort' => 'sometimes|in:asc,desc',
|
||||
]);
|
||||
$filter = $request->input('filter', 'open');
|
||||
$sort = $request->input('sort', 'asc');
|
||||
$records = CuratedRegister::when($filter, function ($q, $filter) {
|
||||
if ($filter === 'open') {
|
||||
return $q->where('is_rejected', false)
|
||||
->where(function ($query) {
|
||||
return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false);
|
||||
})
|
||||
->whereNotNull('email_verified_at')
|
||||
->whereIsClosed(false);
|
||||
} elseif ($filter === 'all') {
|
||||
return $q;
|
||||
} elseif ($filter === 'responses') {
|
||||
return $q->whereIsClosed(false)
|
||||
->whereNotNull('email_verified_at')
|
||||
->where('user_has_responded', true)
|
||||
->where('is_awaiting_more_info', true);
|
||||
} elseif ($filter === 'awaiting') {
|
||||
return $q->whereIsClosed(false)
|
||||
->where('is_rejected', false)
|
||||
->where('is_approved', false)
|
||||
->where('user_has_responded', false)
|
||||
->where('is_awaiting_more_info', true);
|
||||
} elseif ($filter === 'approved') {
|
||||
return $q->whereIsClosed(true)->whereIsApproved(true);
|
||||
} elseif ($filter === 'rejected') {
|
||||
return $q->whereIsClosed(true)->whereIsRejected(true);
|
||||
}
|
||||
})
|
||||
->when($sort, function ($query, $sort) {
|
||||
return $query->orderBy('id', $sort);
|
||||
})
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.curated-register.index', compact('records', 'filter'));
|
||||
}
|
||||
|
||||
public function show(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
|
||||
return view('admin.curated-register.show', compact('record'));
|
||||
}
|
||||
|
||||
public function apiActivityLog(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
|
||||
$res = collect([
|
||||
[
|
||||
'id' => 1,
|
||||
'action' => 'created',
|
||||
'title' => 'Onboarding application created',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->created_at,
|
||||
],
|
||||
]);
|
||||
|
||||
if ($record->email_verified_at) {
|
||||
$res->push([
|
||||
'id' => 3,
|
||||
'action' => 'email_verified_at',
|
||||
'title' => 'Applicant successfully verified email address',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->email_verified_at,
|
||||
]);
|
||||
}
|
||||
|
||||
$activities = CuratedRegisterActivity::whereRegisterId($record->id)->get();
|
||||
|
||||
$idx = 4;
|
||||
$userResponses = collect([]);
|
||||
|
||||
foreach ($activities as $activity) {
|
||||
$idx++;
|
||||
|
||||
if ($activity->type === 'user_resend_email_confirmation') {
|
||||
continue;
|
||||
}
|
||||
if ($activity->from_user) {
|
||||
$userResponses->push($activity);
|
||||
|
||||
continue;
|
||||
}
|
||||
$res->push([
|
||||
'id' => $idx,
|
||||
'aid' => $activity->id,
|
||||
'action' => $activity->type,
|
||||
'title' => $activity->from_admin ? 'Admin requested info' : 'User responded',
|
||||
'message' => $activity->message,
|
||||
'link' => $activity->adminReviewUrl(),
|
||||
'timestamp' => $activity->created_at,
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($userResponses as $ur) {
|
||||
$res = $res->map(function ($r) use ($ur) {
|
||||
if (! isset($r['aid'])) {
|
||||
return $r;
|
||||
}
|
||||
if ($ur->reply_to_id === $r['aid']) {
|
||||
$r['user_response'] = $ur;
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
return $r;
|
||||
});
|
||||
}
|
||||
|
||||
if ($record->is_approved) {
|
||||
$idx++;
|
||||
$res->push([
|
||||
'id' => $idx,
|
||||
'action' => 'approved',
|
||||
'title' => 'Application Approved',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->action_taken_at,
|
||||
]);
|
||||
} elseif ($record->is_rejected) {
|
||||
$idx++;
|
||||
$res->push([
|
||||
'id' => $idx,
|
||||
'action' => 'rejected',
|
||||
'title' => 'Application Rejected',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->action_taken_at,
|
||||
]);
|
||||
}
|
||||
|
||||
return $res->reverse()->values();
|
||||
}
|
||||
|
||||
public function apiMessagePreviewStore(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function apiMessageSendStore(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'message' => 'required|string|min:5|max:1000',
|
||||
]);
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$activity = new CuratedRegisterActivity;
|
||||
$activity->register_id = $record->id;
|
||||
$activity->admin_id = $request->user()->id;
|
||||
$activity->secret_code = Str::random(32);
|
||||
$activity->type = 'request_details';
|
||||
$activity->from_admin = true;
|
||||
$activity->message = $request->input('message');
|
||||
$activity->save();
|
||||
$record->is_awaiting_more_info = true;
|
||||
$record->user_has_responded = false;
|
||||
$record->save();
|
||||
Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity));
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function previewDetailsMessageShow(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$activity = new CuratedRegisterActivity;
|
||||
$activity->message = $request->input('message');
|
||||
|
||||
return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity);
|
||||
}
|
||||
|
||||
public function previewMessageShow(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$record->message = $request->input('message');
|
||||
|
||||
return new \App\Mail\CuratedRegisterSendMessage($record);
|
||||
}
|
||||
|
||||
public function apiHandleReject(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:reject-email,reject-silent',
|
||||
]);
|
||||
$action = $request->input('action');
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
|
||||
$record->is_rejected = true;
|
||||
$record->is_closed = true;
|
||||
$record->action_taken_at = now();
|
||||
$record->save();
|
||||
if ($action === 'reject-email') {
|
||||
Mail::to($record->email)->send(new CuratedRegisterRejectUser($record));
|
||||
}
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function apiHandleApprove(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
|
||||
$record->is_approved = true;
|
||||
$record->is_closed = true;
|
||||
$record->action_taken_at = now();
|
||||
$record->save();
|
||||
$user = User::create([
|
||||
'name' => $record->username,
|
||||
'username' => $record->username,
|
||||
'email' => $record->email,
|
||||
'password' => $record->password,
|
||||
'app_register_ip' => $record->ip_address,
|
||||
'email_verified_at' => now(),
|
||||
'register_source' => 'cur_onboarding',
|
||||
]);
|
||||
|
||||
Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record));
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function templates(Request $request)
|
||||
{
|
||||
$templates = CuratedRegisterTemplate::paginate(10);
|
||||
|
||||
return view('admin.curated-register.templates', compact('templates'));
|
||||
}
|
||||
|
||||
public function templateCreate(Request $request)
|
||||
{
|
||||
return view('admin.curated-register.template-create');
|
||||
}
|
||||
|
||||
public function templateEdit(Request $request, $id)
|
||||
{
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
|
||||
return view('admin.curated-register.template-edit', compact('template'));
|
||||
}
|
||||
|
||||
public function templateEditStore(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:30',
|
||||
'content' => 'required|string|min:5|max:3000',
|
||||
'description' => 'nullable|sometimes|string|max:1000',
|
||||
'active' => 'sometimes',
|
||||
]);
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
$template->name = $request->input('name');
|
||||
$template->content = $request->input('content');
|
||||
$template->description = $request->input('description');
|
||||
$template->is_active = $request->boolean('active');
|
||||
$template->save();
|
||||
|
||||
return redirect()->back()->with('status', 'Successfully updated template!');
|
||||
}
|
||||
|
||||
public function templateDelete(Request $request, $id)
|
||||
{
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
$template->delete();
|
||||
|
||||
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully deleted template!');
|
||||
}
|
||||
|
||||
public function templateStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:30',
|
||||
'content' => 'required|string|min:5|max:3000',
|
||||
'description' => 'nullable|sometimes|string|max:1000',
|
||||
'active' => 'sometimes',
|
||||
]);
|
||||
CuratedRegisterTemplate::create([
|
||||
'name' => $request->input('name'),
|
||||
'content' => $request->input('content'),
|
||||
'description' => $request->input('description'),
|
||||
'is_active' => $request->boolean('active'),
|
||||
]);
|
||||
|
||||
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully created new template!');
|
||||
}
|
||||
|
||||
public function getActiveTemplates(Request $request)
|
||||
{
|
||||
$templates = CuratedRegisterTemplate::whereIsActive(true)
|
||||
->orderBy('order')
|
||||
->get()
|
||||
->map(function ($tmp) {
|
||||
return [
|
||||
'name' => $tmp->name,
|
||||
'content' => $tmp->content,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($templates);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -71,72 +71,77 @@ class ApiV2Controller extends Controller
|
|||
->toArray() : [];
|
||||
});
|
||||
|
||||
$res = [
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'title' => config_cache('app.name'),
|
||||
'version' => config('pixelfed.version'),
|
||||
'source_url' => 'https://github.com/pixelfed/pixelfed',
|
||||
'description' => config_cache('app.short_description'),
|
||||
'usage' => [
|
||||
'users' => [
|
||||
'active_month' => (int) Nodeinfo::activeUsersMonthly()
|
||||
]
|
||||
],
|
||||
'thumbnail' => [
|
||||
'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'blurhash' => InstanceService::headerBlurhash(),
|
||||
'versions' => [
|
||||
'@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
|
||||
]
|
||||
],
|
||||
'languages' => [config('app.locale')],
|
||||
'configuration' => [
|
||||
'urls' => [
|
||||
'streaming' => 'wss://' . config('pixelfed.domain.app'),
|
||||
'status' => null
|
||||
$res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use($contact, $rules) {
|
||||
return [
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'title' => config_cache('app.name'),
|
||||
'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')',
|
||||
'source_url' => 'https://github.com/pixelfed/pixelfed',
|
||||
'description' => config_cache('app.short_description'),
|
||||
'usage' => [
|
||||
'users' => [
|
||||
'active_month' => (int) Nodeinfo::activeUsersMonthly()
|
||||
]
|
||||
],
|
||||
'vapid' => [
|
||||
'public_key' => config('webpush.vapid.public_key'),
|
||||
'thumbnail' => [
|
||||
'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'blurhash' => InstanceService::headerBlurhash(),
|
||||
'versions' => [
|
||||
'@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
|
||||
]
|
||||
],
|
||||
'accounts' => [
|
||||
'max_featured_tags' => 0,
|
||||
'languages' => [config('app.locale')],
|
||||
'configuration' => [
|
||||
'urls' => [
|
||||
'streaming' => null,
|
||||
'status' => null
|
||||
],
|
||||
'vapid' => [
|
||||
'public_key' => config('webpush.vapid.public_key'),
|
||||
],
|
||||
'accounts' => [
|
||||
'max_featured_tags' => 0,
|
||||
],
|
||||
'statuses' => [
|
||||
'max_characters' => (int) config('pixelfed.max_caption_length'),
|
||||
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
|
||||
'characters_reserved_per_url' => 23
|
||||
],
|
||||
'media_attachments' => [
|
||||
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
|
||||
'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'image_matrix_limit' => 3686400,
|
||||
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'video_frame_rate_limit' => 240,
|
||||
'video_matrix_limit' => 3686400
|
||||
],
|
||||
'polls' => [
|
||||
'max_options' => 0,
|
||||
'max_characters_per_option' => 0,
|
||||
'min_expiration' => 0,
|
||||
'max_expiration' => 0,
|
||||
],
|
||||
'translation' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
],
|
||||
'statuses' => [
|
||||
'max_characters' => (int) config('pixelfed.max_caption_length'),
|
||||
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
|
||||
'characters_reserved_per_url' => 23
|
||||
'registrations' => [
|
||||
'enabled' => null,
|
||||
'approval_required' => false,
|
||||
'message' => null,
|
||||
'url' => null,
|
||||
],
|
||||
'media_attachments' => [
|
||||
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
|
||||
'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'image_matrix_limit' => 3686400,
|
||||
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'video_frame_rate_limit' => 240,
|
||||
'video_matrix_limit' => 3686400
|
||||
'contact' => [
|
||||
'email' => config('instance.email'),
|
||||
'account' => $contact
|
||||
],
|
||||
'polls' => [
|
||||
'max_options' => 4,
|
||||
'max_characters_per_option' => 50,
|
||||
'min_expiration' => 300,
|
||||
'max_expiration' => 2629746,
|
||||
],
|
||||
'translation' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
],
|
||||
'registrations' => [
|
||||
'enabled' => (bool) config_cache('pixelfed.open_registration'),
|
||||
'approval_required' => false,
|
||||
'message' => null
|
||||
],
|
||||
'contact' => [
|
||||
'email' => config('instance.email'),
|
||||
'account' => $contact
|
||||
],
|
||||
'rules' => $rules
|
||||
];
|
||||
'rules' => $rules
|
||||
];
|
||||
});
|
||||
|
||||
$res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled');
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
|
|
|
@ -174,7 +174,7 @@ class RegisterController extends Controller
|
|||
*/
|
||||
public function showRegistrationForm()
|
||||
{
|
||||
if(config_cache('pixelfed.open_registration')) {
|
||||
if((bool) config_cache('pixelfed.open_registration')) {
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp(request()->ip()), 404);
|
||||
}
|
||||
|
@ -191,7 +191,11 @@ class RegisterController extends Controller
|
|||
return view('auth.register');
|
||||
}
|
||||
} else {
|
||||
abort(404);
|
||||
if((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) {
|
||||
return redirect('/auth/sign_up');
|
||||
} else {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,399 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\User;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\Models\CuratedRegisterActivity;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\BouncerService;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use App\Mail\CuratedRegisterConfirmEmail;
|
||||
use App\Mail\CuratedRegisterNotifyAdmin;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Jobs\CuratedOnboarding\CuratedOnboardingNotifyAdminNewApplicationPipeline;
|
||||
|
||||
class CuratedRegisterController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
abort_unless((bool) config_cache('instance.curated_registration.enabled'), 404);
|
||||
|
||||
if((bool) config_cache('pixelfed.open_registration')) {
|
||||
abort_if(config('instance.curated_registration.state.only_enabled_on_closed_reg'), 404);
|
||||
} else {
|
||||
abort_unless(config('instance.curated_registration.state.fallback_on_closed_reg'), 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
return view('auth.curated-register.index', ['step' => 1]);
|
||||
}
|
||||
|
||||
public function concierge(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
$emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') &&
|
||||
$request->has('next') &&
|
||||
$request->session()->has('cur-reg-con.cr-id');
|
||||
return view('auth.curated-register.concierge', compact('emailConfirmed'));
|
||||
}
|
||||
|
||||
public function conciergeResponseSent(Request $request)
|
||||
{
|
||||
return view('auth.curated-register.user_response_sent');
|
||||
}
|
||||
|
||||
public function conciergeFormShow(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
abort_unless(
|
||||
$request->session()->has('cur-reg-con.email-confirmed') &&
|
||||
$request->session()->has('cur-reg-con.cr-id') &&
|
||||
$request->session()->has('cur-reg-con.ac-id'), 404);
|
||||
$crid = $request->session()->get('cur-reg-con.cr-id');
|
||||
$arid = $request->session()->get('cur-reg-con.ac-id');
|
||||
$showCaptcha = config('instance.curated_registration.captcha_enabled');
|
||||
if($attempts = $request->session()->get('cur-reg-con-attempt')) {
|
||||
$showCaptcha = $attempts && $attempts >= 2;
|
||||
} else {
|
||||
$showCaptcha = false;
|
||||
}
|
||||
$activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid);
|
||||
return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha'));
|
||||
}
|
||||
|
||||
public function conciergeFormStore(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
$request->session()->increment('cur-reg-con-attempt');
|
||||
abort_unless(
|
||||
$request->session()->has('cur-reg-con.email-confirmed') &&
|
||||
$request->session()->has('cur-reg-con.cr-id') &&
|
||||
$request->session()->has('cur-reg-con.ac-id'), 404);
|
||||
$attempts = $request->session()->get('cur-reg-con-attempt');
|
||||
$messages = [];
|
||||
$rules = [
|
||||
'response' => 'required|string|min:5|max:1000',
|
||||
'crid' => 'required|integer|min:1',
|
||||
'acid' => 'required|integer|min:1'
|
||||
];
|
||||
if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
$this->validate($request, $rules, $messages);
|
||||
$crid = $request->session()->get('cur-reg-con.cr-id');
|
||||
$acid = $request->session()->get('cur-reg-con.ac-id');
|
||||
abort_if((string) $crid !== $request->input('crid'), 404);
|
||||
abort_if((string) $acid !== $request->input('acid'), 404);
|
||||
|
||||
if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) {
|
||||
return redirect()->back()->withErrors(['code' => 'You already replied to this request.']);
|
||||
}
|
||||
|
||||
$act = CuratedRegisterActivity::create([
|
||||
'register_id' => $crid,
|
||||
'reply_to_id' => $acid,
|
||||
'type' => 'user_response',
|
||||
'message' => $request->input('response'),
|
||||
'from_user' => true,
|
||||
'action_required' => true,
|
||||
]);
|
||||
|
||||
CuratedRegister::findOrFail($crid)->update(['user_has_responded' => true]);
|
||||
$request->session()->pull('cur-reg-con');
|
||||
$request->session()->pull('cur-reg-con-attempt');
|
||||
|
||||
return view('auth.curated-register.user_response_sent');
|
||||
}
|
||||
|
||||
public function conciergeStore(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
$rules = [
|
||||
'sid' => 'required_if:action,email|integer|min:1|max:20000000',
|
||||
'id' => 'required_if:action,email|integer|min:1|max:20000000',
|
||||
'code' => 'required_if:action,email',
|
||||
'action' => 'required|string|in:email,message',
|
||||
'email' => 'required_if:action,email|email',
|
||||
'response' => 'required_if:action,message|string|min:20|max:1000',
|
||||
];
|
||||
$messages = [];
|
||||
if(config('instance.curated_registration.captcha_enabled')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
$this->validate($request, $rules, $messages);
|
||||
|
||||
$action = $request->input('action');
|
||||
$sid = $request->input('sid');
|
||||
$id = $request->input('id');
|
||||
$code = $request->input('code');
|
||||
$email = $request->input('email');
|
||||
|
||||
$cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid);
|
||||
$ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id);
|
||||
|
||||
if(!hash_equals($ac->secret_code, $code)) {
|
||||
return redirect()->back()->withErrors(['code' => 'Invalid code']);
|
||||
}
|
||||
|
||||
if(!hash_equals($cr->email, $email)) {
|
||||
return redirect()->back()->withErrors(['email' => 'Invalid email']);
|
||||
}
|
||||
|
||||
$request->session()->put('cur-reg-con.email-confirmed', true);
|
||||
$request->session()->put('cur-reg-con.cr-id', $cr->id);
|
||||
$request->session()->put('cur-reg-con.ac-id', $ac->id);
|
||||
$emailConfirmed = true;
|
||||
return redirect('/auth/sign_up/concierge/form');
|
||||
}
|
||||
|
||||
public function confirmEmail(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
return view('auth.curated-register.confirm_email');
|
||||
}
|
||||
|
||||
public function emailConfirmed(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
return view('auth.curated-register.email_confirmed');
|
||||
}
|
||||
|
||||
public function resendConfirmation(Request $request)
|
||||
{
|
||||
return view('auth.curated-register.resend-confirmation');
|
||||
}
|
||||
|
||||
public function resendConfirmationProcess(Request $request)
|
||||
{
|
||||
$rules = [
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
|
||||
'exists:curated_registers',
|
||||
]
|
||||
];
|
||||
|
||||
$messages = [];
|
||||
|
||||
if(config('instance.curated_registration.captcha_enabled')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
|
||||
$this->validate($request, $rules, $messages);
|
||||
|
||||
$cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first();
|
||||
if(!$cur) {
|
||||
return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']);
|
||||
}
|
||||
|
||||
$totalCount = CuratedRegisterActivity::whereRegisterId($cur->id)
|
||||
->whereType('user_resend_email_confirmation')
|
||||
->count();
|
||||
|
||||
if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) {
|
||||
return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please <a href="/site/contact" class="text-white" style="text-decoration: underline;">contact the admin team</a>.']);
|
||||
}
|
||||
|
||||
$count = CuratedRegisterActivity::whereRegisterId($cur->id)
|
||||
->whereType('user_resend_email_confirmation')
|
||||
->where('created_at', '>', now()->subHours(12))
|
||||
->count();
|
||||
|
||||
if($count) {
|
||||
return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']);
|
||||
}
|
||||
|
||||
CuratedRegisterActivity::create([
|
||||
'register_id' => $cur->id,
|
||||
'type' => 'user_resend_email_confirmation',
|
||||
'admin_only_view' => true,
|
||||
'from_admin' => false,
|
||||
'from_user' => false,
|
||||
'action_required' => false,
|
||||
]);
|
||||
|
||||
Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur));
|
||||
return view('auth.curated-register.resent-confirmation');
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function confirmEmailHandle(Request $request)
|
||||
{
|
||||
$rules = [
|
||||
'sid' => 'required',
|
||||
'code' => 'required'
|
||||
];
|
||||
$messages = [];
|
||||
if(config('instance.curated_registration.captcha_enabled')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
$this->validate($request, $rules, $messages);
|
||||
|
||||
$cr = CuratedRegister::whereNull('email_verified_at')
|
||||
->where('created_at', '>', now()->subHours(24))
|
||||
->find($request->input('sid'));
|
||||
if(!$cr) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
if(!hash_equals($cr->verify_code, $request->input('code'))) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
$cr->email_verified_at = now();
|
||||
$cr->save();
|
||||
|
||||
if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
|
||||
CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr);
|
||||
}
|
||||
return view('auth.curated-register.email_confirmed');
|
||||
}
|
||||
|
||||
public function proceed(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'step' => 'required|integer|in:1,2,3,4'
|
||||
]);
|
||||
$step = $request->input('step');
|
||||
|
||||
switch($step) {
|
||||
case 1:
|
||||
$step = 2;
|
||||
$request->session()->put('cur-step', 1);
|
||||
return view('auth.curated-register.index', compact('step'));
|
||||
break;
|
||||
|
||||
case 2:
|
||||
$this->stepTwo($request);
|
||||
$step = 3;
|
||||
$request->session()->put('cur-step', 2);
|
||||
return view('auth.curated-register.index', compact('step'));
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$this->stepThree($request);
|
||||
$step = 3;
|
||||
$request->session()->put('cur-step', 3);
|
||||
$verifiedEmail = true;
|
||||
$request->session()->pull('cur-reg');
|
||||
return view('auth.curated-register.index', compact('step', 'verifiedEmail'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function stepTwo($request)
|
||||
{
|
||||
if($request->filled('reason')) {
|
||||
$request->session()->put('cur-reg.form-reason', $request->input('reason'));
|
||||
}
|
||||
if($request->filled('username')) {
|
||||
$request->session()->put('cur-reg.form-username', $request->input('username'));
|
||||
}
|
||||
if($request->filled('email')) {
|
||||
$request->session()->put('cur-reg.form-email', $request->input('email'));
|
||||
}
|
||||
$this->validate($request, [
|
||||
'username' => [
|
||||
'required',
|
||||
'min:2',
|
||||
'max:15',
|
||||
'unique:curated_registers',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$dash = substr_count($value, '-');
|
||||
$underscore = substr_count($value, '_');
|
||||
$period = substr_count($value, '.');
|
||||
|
||||
if(ends_with($value, ['.php', '.js', '.css'])) {
|
||||
return $fail('Username is invalid.');
|
||||
}
|
||||
|
||||
if(($dash + $underscore + $period) > 1) {
|
||||
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[0])) {
|
||||
return $fail('Username is invalid. Must start with a letter or number.');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[strlen($value) - 1])) {
|
||||
return $fail('Username is invalid. Must end with a letter or number.');
|
||||
}
|
||||
|
||||
$val = str_replace(['_', '.', '-'], '', $value);
|
||||
if(!ctype_alnum($val)) {
|
||||
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
|
||||
}
|
||||
|
||||
$restricted = RestrictedNames::get();
|
||||
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
|
||||
return $fail('Username cannot be used.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
'unique:curated_registers',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'password' => 'required|min:8',
|
||||
'password_confirmation' => 'required|same:password',
|
||||
'reason' => 'required|min:20|max:1000',
|
||||
'agree' => 'required|accepted'
|
||||
]);
|
||||
$request->session()->put('cur-reg.form-email', $request->input('email'));
|
||||
$request->session()->put('cur-reg.form-password', $request->input('password'));
|
||||
}
|
||||
|
||||
protected function stepThree($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
'unique:curated_registers',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
]
|
||||
]);
|
||||
$cr = new CuratedRegister;
|
||||
$cr->email = $request->email;
|
||||
$cr->username = $request->session()->get('cur-reg.form-username');
|
||||
$cr->password = bcrypt($request->session()->get('cur-reg.form-password'));
|
||||
$cr->ip_address = $request->ip();
|
||||
$cr->reason_to_join = $request->session()->get('cur-reg.form-reason');
|
||||
$cr->verify_code = Str::random(40);
|
||||
$cr->save();
|
||||
|
||||
Mail::to($cr->email)->send(new CuratedRegisterConfirmEmail($cr));
|
||||
}
|
||||
}
|
|
@ -253,7 +253,7 @@ class FederationController extends Controller
|
|||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $account['following_count'] ?? 0,
|
||||
];
|
||||
return response()->json($obj);
|
||||
return response()->json($obj)->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
|
||||
public function userFollowers(Request $request, $username)
|
||||
|
@ -269,6 +269,6 @@ class FederationController extends Controller
|
|||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $account['followers_count'] ?? 0,
|
||||
];
|
||||
return response()->json($obj);
|
||||
return response()->json($obj)->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,10 +78,11 @@ class PixelfedDirectoryController extends Controller
|
|||
$res['community_guidelines'] = json_decode($guidelines->v, true);
|
||||
}
|
||||
|
||||
$openRegistration = ConfigCache::whereK('pixelfed.open_registration')->first();
|
||||
if($openRegistration) {
|
||||
$res['open_registration'] = (bool) $openRegistration;
|
||||
}
|
||||
$openRegistration = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['open_registration'] = $openRegistration;
|
||||
|
||||
$curatedOnboarding = (bool) config_cache('instance.curated_registration.enabled');
|
||||
$res['curated_onboarding'] = $curatedOnboarding;
|
||||
|
||||
$oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first();
|
||||
if($oauthEnabled) {
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\WebfingerUrl;
|
||||
use App\Models\ProfileAlias;
|
||||
use App\Models\ProfileMigration;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\WebfingerService;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProfileAliasController extends Controller
|
||||
{
|
||||
|
@ -18,31 +20,47 @@ class ProfileAliasController extends Controller
|
|||
public function index(Request $request)
|
||||
{
|
||||
$aliases = $request->user()->profile->aliases;
|
||||
|
||||
return view('settings.aliases.index', compact('aliases'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'acct' => 'required'
|
||||
'acct' => 'required',
|
||||
]);
|
||||
|
||||
$acct = $request->input('acct');
|
||||
|
||||
if($request->user()->profile->aliases->count() >= 3) {
|
||||
$nn = Nickname::normalizeProfileUrl($acct);
|
||||
if (! $nn) {
|
||||
return back()->with('error', 'Invalid account alias.');
|
||||
}
|
||||
|
||||
if ($nn['domain'] === config('pixelfed.domain.app')) {
|
||||
if (strtolower($nn['username']) == ($request->user()->username)) {
|
||||
return back()->with('error', 'You cannot add an alias to your own account.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->user()->profile->aliases->count() >= 3) {
|
||||
return back()->with('error', 'You can only add 3 account aliases.');
|
||||
}
|
||||
|
||||
$webfingerService = WebfingerService::lookup($acct);
|
||||
if(!$webfingerService || !isset($webfingerService['url'])) {
|
||||
$webfingerUrl = WebfingerService::rawGet($acct);
|
||||
|
||||
if (! $webfingerService || ! isset($webfingerService['url']) || ! $webfingerUrl || empty($webfingerUrl)) {
|
||||
return back()->with('error', 'Invalid account, cannot add alias at this time.');
|
||||
}
|
||||
$alias = new ProfileAlias;
|
||||
$alias->profile_id = $request->user()->profile_id;
|
||||
$alias->acct = $acct;
|
||||
$alias->uri = $webfingerService['url'];
|
||||
$alias->uri = $webfingerUrl;
|
||||
$alias->save();
|
||||
|
||||
Cache::forget('pf:activitypub:user-object:by-id:'.$request->user()->profile_id);
|
||||
|
||||
return back()->with('status', 'Successfully added alias!');
|
||||
}
|
||||
|
||||
|
@ -50,14 +68,25 @@ class ProfileAliasController extends Controller
|
|||
{
|
||||
$this->validate($request, [
|
||||
'acct' => 'required',
|
||||
'id' => 'required|exists:profile_aliases'
|
||||
'id' => 'required|exists:profile_aliases',
|
||||
]);
|
||||
|
||||
$alias = ProfileAlias::where('profile_id', $request->user()->profile_id)
|
||||
->where('acct', $request->input('acct'))
|
||||
$pid = $request->user()->profile_id;
|
||||
$acct = $request->input('acct');
|
||||
$alias = ProfileAlias::where('profile_id', $pid)
|
||||
->where('acct', $acct)
|
||||
->findOrFail($request->input('id'));
|
||||
$migration = ProfileMigration::whereProfileId($pid)
|
||||
->whereAcct($acct)
|
||||
->first();
|
||||
if ($migration) {
|
||||
$request->user()->profile->update([
|
||||
'moved_to_profile_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$alias->delete();
|
||||
Cache::forget('pf:activitypub:user-object:by-id:'.$pid);
|
||||
AccountService::del($pid);
|
||||
|
||||
return back()->with('status', 'Successfully deleted alias!');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ProfileMigrationStoreRequest;
|
||||
use App\Jobs\ProfilePipeline\ProfileMigrationDeliverMoveActivityPipeline;
|
||||
use App\Jobs\ProfilePipeline\ProfileMigrationMoveFollowersPipeline;
|
||||
use App\Models\ProfileAlias;
|
||||
use App\Models\ProfileMigration;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\WebfingerService;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
class ProfileMigrationController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if((bool) config_cache('federation.activitypub.enabled') === false, 404);
|
||||
if ((bool) config_cache('federation.migration') === false) {
|
||||
return redirect(route('help.account-migration'));
|
||||
}
|
||||
$hasExistingMigration = ProfileMigration::whereProfileId($request->user()->profile_id)
|
||||
->where('created_at', '>', now()->subDays(30))
|
||||
->exists();
|
||||
|
||||
return view('settings.migration.index', compact('hasExistingMigration'));
|
||||
}
|
||||
|
||||
public function store(ProfileMigrationStoreRequest $request)
|
||||
{
|
||||
abort_if((bool) config_cache('federation.activitypub.enabled') === false, 404);
|
||||
$acct = WebfingerService::rawGet($request->safe()->acct);
|
||||
if (! $acct) {
|
||||
return redirect()->back()->withErrors(['acct' => 'The new account you provided is not responding to our requests.']);
|
||||
}
|
||||
$newAccount = Helpers::profileFetch($acct);
|
||||
if (! $newAccount) {
|
||||
return redirect()->back()->withErrors(['acct' => 'An error occured, please try again later. Code: res-failed-account-fetch']);
|
||||
}
|
||||
$user = $request->user();
|
||||
ProfileAlias::updateOrCreate([
|
||||
'profile_id' => $user->profile_id,
|
||||
'acct' => $request->safe()->acct,
|
||||
'uri' => $acct,
|
||||
]);
|
||||
$migration = ProfileMigration::create([
|
||||
'profile_id' => $request->user()->profile_id,
|
||||
'acct' => $request->safe()->acct,
|
||||
'followers_count' => $request->user()->profile->followers_count,
|
||||
'target_profile_id' => $newAccount['id'],
|
||||
]);
|
||||
$user->profile->update([
|
||||
'moved_to_profile_id' => $newAccount->id,
|
||||
'indexable' => false,
|
||||
]);
|
||||
AccountService::del($user->profile_id);
|
||||
|
||||
Bus::batch([
|
||||
new ProfileMigrationDeliverMoveActivityPipeline($migration, $user->profile, $newAccount),
|
||||
new ProfileMigrationMoveFollowersPipeline($user->profile_id, $newAccount->id),
|
||||
])->onQueue('follow')->dispatch();
|
||||
|
||||
return redirect()->back()->with(['status' => 'Succesfully migrated account!']);
|
||||
}
|
||||
}
|
|
@ -84,14 +84,17 @@ trait PrivacySettings
|
|||
}
|
||||
$settings->save();
|
||||
}
|
||||
Cache::forget('profile:settings:' . $profile->id);
|
||||
$pid = $profile->id;
|
||||
Cache::forget('profile:settings:' . $pid);
|
||||
Cache::forget('user:account:id:' . $profile->user_id);
|
||||
Cache::forget('profile:follower_count:' . $profile->id);
|
||||
Cache::forget('profile:following_count:' . $profile->id);
|
||||
Cache::forget('profile:atom:enabled:' . $profile->id);
|
||||
Cache::forget('profile:embed:' . $profile->id);
|
||||
Cache::forget('pf:acct:settings:hidden-followers:' . $profile->id);
|
||||
Cache::forget('pf:acct:settings:hidden-following:' . $profile->id);
|
||||
Cache::forget('profile:follower_count:' . $pid);
|
||||
Cache::forget('profile:following_count:' . $pid);
|
||||
Cache::forget('profile:atom:enabled:' . $pid);
|
||||
Cache::forget('profile:embed:' . $pid);
|
||||
Cache::forget('pf:acct:settings:hidden-followers:' . $pid);
|
||||
Cache::forget('pf:acct:settings:hidden-following:' . $pid);
|
||||
Cache::forget('pf:acct-trans:hideFollowing:' . $pid);
|
||||
Cache::forget('pf:acct-trans:hideFollowers:' . $pid);
|
||||
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\Internal\SoftwareUpdateService;
|
||||
|
||||
class SoftwareUpdateController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware('admin');
|
||||
}
|
||||
|
||||
public function getSoftwareUpdateCheck(Request $request)
|
||||
{
|
||||
$res = SoftwareUpdateService::get();
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\ProfileMigration;
|
||||
use App\Services\FetchCacheService;
|
||||
use App\Services\WebfingerService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class ProfileMigrationStoreRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
if ((bool) config_cache('federation.activitypub.enabled') === false ||
|
||||
(bool) config_cache('federation.migration') === false) {
|
||||
return false;
|
||||
}
|
||||
if (! $this->user() || $this->user()->status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'acct' => 'required|email',
|
||||
'password' => 'required|current_password',
|
||||
];
|
||||
}
|
||||
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (Validator $validator) {
|
||||
$err = $this->validateNewAccount();
|
||||
if ($err !== 'noerr') {
|
||||
$validator->errors()->add(
|
||||
'acct',
|
||||
$err
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
protected function validateNewAccount()
|
||||
{
|
||||
if (ProfileMigration::whereProfileId($this->user()->profile_id)->where('created_at', '>', now()->subDays(30))->exists()) {
|
||||
return 'Error - You have migrated your account in the past 30 days, you can only perform a migration once per 30 days.';
|
||||
}
|
||||
$acct = WebfingerService::rawGet($this->acct);
|
||||
if (! $acct) {
|
||||
return 'The new account you provided is not responding to our requests.';
|
||||
}
|
||||
$pr = FetchCacheService::getJson($acct);
|
||||
if (! $pr || ! isset($pr['alsoKnownAs'])) {
|
||||
return 'Invalid account lookup response.';
|
||||
}
|
||||
if (! count($pr['alsoKnownAs']) || ! is_array($pr['alsoKnownAs'])) {
|
||||
return 'The new account does not contain an alias to your current account.';
|
||||
}
|
||||
$curAcctUrl = $this->user()->profile->permalink();
|
||||
if (! in_array($curAcctUrl, $pr['alsoKnownAs'])) {
|
||||
return 'The new account does not contain an alias to your current account.';
|
||||
}
|
||||
|
||||
return 'noerr';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use App\Instance;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class AdminRemoteReport extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$instance = parse_url($this->uri, PHP_URL_HOST);
|
||||
$statuses = [];
|
||||
if($this->status_ids && count($this->status_ids)) {
|
||||
foreach($this->status_ids as $sid) {
|
||||
$s = StatusService::get($sid, false);
|
||||
if($s && $s['in_reply_to_id'] != null) {
|
||||
$parent = StatusService::get($s['in_reply_to_id'], false);
|
||||
if($parent) {
|
||||
$s['parent'] = $parent;
|
||||
}
|
||||
}
|
||||
if($s) {
|
||||
$statuses[] = $s;
|
||||
}
|
||||
}
|
||||
}
|
||||
$res = [
|
||||
'id' => $this->id,
|
||||
'instance' => $instance,
|
||||
'reported' => AccountService::get($this->account_id, true),
|
||||
'status_ids' => $this->status_ids,
|
||||
'statuses' => $statuses,
|
||||
'message' => $this->comment,
|
||||
'report_meta' => $this->report_meta,
|
||||
'created_at' => optional($this->created_at)->format('c'),
|
||||
'action_taken_at' => optional($this->action_taken_at)->format('c'),
|
||||
];
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\CuratedOnboarding;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Mail\CuratedRegisterNotifyAdmin;
|
||||
|
||||
class CuratedOnboardingNotifyAdminNewApplicationPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $cr;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(CuratedRegister $cr)
|
||||
{
|
||||
$this->cr = $cr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if(!config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
config('instance.curated_registration.notify.admin.on_verify_email.bundle') ?
|
||||
$this->handleBundled() :
|
||||
$this->handleUnbundled();
|
||||
}
|
||||
|
||||
protected function handleBundled()
|
||||
{
|
||||
$cr = $this->cr;
|
||||
Storage::append('conanap.json', json_encode([
|
||||
'id' => $cr->id,
|
||||
'email' => $cr->email,
|
||||
'created_at' => $cr->created_at,
|
||||
'updated_at' => $cr->updated_at,
|
||||
]));
|
||||
}
|
||||
|
||||
protected function handleUnbundled()
|
||||
{
|
||||
$cr = $this->cr;
|
||||
if($aid = config_cache('instance.admin.pid')) {
|
||||
$admin = User::whereProfileId($aid)->first();
|
||||
if($admin && $admin->email) {
|
||||
Mail::to($admin->email)->send(new CuratedRegisterNotifyAdmin($cr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\ProfilePipeline;
|
||||
|
||||
use App\Transformer\ActivityPub\Verb\Move;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Pool;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class ProfileMigrationDeliverMoveActivityPipeline implements ShouldBeUniqueUntilProcessing, ShouldQueue
|
||||
{
|
||||
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $migration;
|
||||
|
||||
public $oldAccount;
|
||||
|
||||
public $newAccount;
|
||||
|
||||
public $timeout = 1400;
|
||||
|
||||
public $tries = 3;
|
||||
|
||||
public $maxExceptions = 1;
|
||||
|
||||
public $failOnTimeout = true;
|
||||
|
||||
/**
|
||||
* The number of seconds after which the job's unique lock will be released.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $uniqueFor = 3600;
|
||||
|
||||
/**
|
||||
* Get the unique ID for the job.
|
||||
*/
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return 'profile:migration:deliver-move-followers:id:'.$this->migration->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the middleware the job should pass through.
|
||||
*
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('profile:migration:deliver-move-followers:id:'.$this->migration->id))->shared()->dontRelease()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($migration, $oldAccount, $newAccount)
|
||||
{
|
||||
$this->migration = $migration;
|
||||
$this->oldAccount = $oldAccount;
|
||||
$this->newAccount = $newAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->batch()->cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$migration = $this->migration;
|
||||
$profile = $this->oldAccount;
|
||||
$newAccount = $this->newAccount;
|
||||
|
||||
if ($profile->domain || ! $profile->private_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
$audience = $profile->getAudienceInbox();
|
||||
$activitypubObject = new Move();
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($migration, $activitypubObject);
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout'),
|
||||
]);
|
||||
|
||||
$version = config('pixelfed.version');
|
||||
$appUrl = config('app.url');
|
||||
$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
|
||||
|
||||
$requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
|
||||
foreach ($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity, [
|
||||
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => $userAgent,
|
||||
]);
|
||||
yield function () use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
],
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => config('federation.activitypub.delivery.concurrency'),
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
},
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
|
||||
$promise->wait();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\ProfilePipeline;
|
||||
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProfileMigrationMoveFollowersPipeline implements ShouldBeUniqueUntilProcessing, ShouldQueue
|
||||
{
|
||||
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $oldPid;
|
||||
|
||||
public $newPid;
|
||||
|
||||
public $timeout = 1400;
|
||||
|
||||
public $tries = 3;
|
||||
|
||||
public $maxExceptions = 1;
|
||||
|
||||
public $failOnTimeout = true;
|
||||
|
||||
/**
|
||||
* The number of seconds after which the job's unique lock will be released.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $uniqueFor = 3600;
|
||||
|
||||
/**
|
||||
* Get the unique ID for the job.
|
||||
*/
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return 'profile:migration:move-followers:oldpid-'.$this->oldPid.':newpid-'.$this->newPid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the middleware the job should pass through.
|
||||
*
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('profile:migration:move-followers:oldpid-'.$this->oldPid.':newpid-'.$this->newPid))->shared()->dontRelease()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($oldPid, $newPid)
|
||||
{
|
||||
$this->oldPid = $oldPid;
|
||||
$this->newPid = $newPid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->batch()->cancelled()) {
|
||||
return;
|
||||
}
|
||||
$og = Profile::find($this->oldPid);
|
||||
$ne = Profile::find($this->newPid);
|
||||
if (! $og || ! $ne || $og == $ne) {
|
||||
return;
|
||||
}
|
||||
$ne->followers_count = $og->followers_count;
|
||||
$ne->save();
|
||||
$og->followers_count = 0;
|
||||
$og->save();
|
||||
foreach (Follower::whereFollowingId($this->oldPid)->lazyById(200, 'id') as $follower) {
|
||||
try {
|
||||
$follower->following_id = $this->newPid;
|
||||
$follower->save();
|
||||
} catch (Exception $e) {
|
||||
$follower->delete();
|
||||
}
|
||||
}
|
||||
AccountService::del($this->oldPid);
|
||||
AccountService::del($this->newPid);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CuratedRegisterAcceptUser extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $verify;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.request-accepted',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\CuratedRegister;
|
||||
|
||||
class CuratedRegisterConfirmEmail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
public $verify;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(CuratedRegister $verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Welcome to Pixelfed! Please Confirm Your Email',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.confirm_email',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\CuratedRegister;
|
||||
|
||||
class CuratedRegisterNotifyAdmin extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
public $verify;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(CuratedRegister $verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: '[Requires Action]: New Curated Onboarding Application',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.admin_notify',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CuratedRegisterNotifyAdminUserResponse extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $activity;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Curated Register Notify Admin User Response',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.admin_notify_user_response',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CuratedRegisterRejectUser extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $verify;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.request-rejected',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\Models\CuratedRegisterActivity;
|
||||
|
||||
class CuratedRegisterRequestDetailsFromUser extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
public $verify;
|
||||
public $activity;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(CuratedRegister $verify, CuratedRegisterActivity $activity)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
$this->activity = $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: '[Action Needed]: Additional information requested',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.request-details-from-user',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CuratedRegisterSendMessage extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $verify;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.message-from-admin',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -8,8 +8,14 @@ class MediaTag extends Model
|
|||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected $visible = [
|
||||
'status_id',
|
||||
'profile_id',
|
||||
'tagged_username',
|
||||
];
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class);
|
||||
return $this->belongsTo(Status::class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CuratedRegister extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_has_responded'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'autofollow_account_ids' => 'array',
|
||||
'admin_notes' => 'array',
|
||||
'email_verified_at' => 'datetime',
|
||||
'admin_notified_at' => 'datetime',
|
||||
'action_taken_at' => 'datetime',
|
||||
'user_has_responded' => 'boolean',
|
||||
'is_awaiting_more_info' => 'boolean',
|
||||
'is_accepted' => 'boolean',
|
||||
'is_rejected' => 'boolean',
|
||||
'is_closed' => 'boolean',
|
||||
];
|
||||
|
||||
public function adminStatusLabel()
|
||||
{
|
||||
if($this->user_has_responded) {
|
||||
return '<span class="border border-warning px-3 py-1 rounded text-white font-weight-bold">Awaiting Admin Response</span>';
|
||||
}
|
||||
if(!$this->email_verified_at) {
|
||||
return '<span class="border border-danger px-3 py-1 rounded text-white font-weight-bold">Unverified email</span>';
|
||||
}
|
||||
if($this->is_approved) {
|
||||
return '<span class="badge badge-success bg-success text-dark">Approved</span>';
|
||||
}
|
||||
if($this->is_rejected) {
|
||||
return '<span class="badge badge-danger bg-danger text-white">Rejected</span>';
|
||||
}
|
||||
if($this->is_awaiting_more_info ) {
|
||||
return '<span class="border border-info px-3 py-1 rounded text-white font-weight-bold">Awaiting User Response</span>';
|
||||
}
|
||||
if($this->is_closed ) {
|
||||
return '<span class="border border-muted px-3 py-1 rounded text-white font-weight-bold" style="opacity:0.5">Closed</span>';
|
||||
}
|
||||
|
||||
return '<span class="border border-success px-3 py-1 rounded text-white font-weight-bold">Open</span>';
|
||||
}
|
||||
|
||||
public function emailConfirmUrl()
|
||||
{
|
||||
return url('/auth/sign_up/confirm?sid=' . $this->id . '&code=' . $this->verify_code);
|
||||
}
|
||||
|
||||
public function emailReplyUrl()
|
||||
{
|
||||
return url('/auth/sign_up/concierge?sid=' . $this->id . '&code=' . $this->verify_code . '&sc=' . str_random(8));
|
||||
}
|
||||
|
||||
public function adminReviewUrl()
|
||||
{
|
||||
return url('/i/admin/curated-onboarding/show/' . $this->id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CuratedRegisterActivity extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'admin_notified_at' => 'datetime',
|
||||
'action_taken_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function application()
|
||||
{
|
||||
return $this->belongsTo(CuratedRegister::class, 'register_id');
|
||||
}
|
||||
|
||||
public function emailReplyUrl()
|
||||
{
|
||||
return url('/auth/sign_up/concierge?sid='.$this->register_id . '&id=' . $this->id . '&code=' . $this->secret_code);
|
||||
}
|
||||
|
||||
public function adminReviewUrl()
|
||||
{
|
||||
$url = '/i/admin/curated-onboarding/show/' . $this->register_id . '/?ah=' . $this->id;
|
||||
if($this->reply_to_id) {
|
||||
$url .= '&rtid=' . $this->reply_to_id;
|
||||
}
|
||||
return url($url);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CuratedRegisterTemplate extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'description', 'content', 'is_active', 'order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
|
@ -10,6 +10,8 @@ class ProfileAlias extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Profile;
|
||||
|
||||
class ProfileMigration extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id');
|
||||
}
|
||||
|
||||
public function target()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'target_profile_id');
|
||||
}
|
||||
}
|
|
@ -38,6 +38,10 @@ class StatusObserver
|
|||
*/
|
||||
public function updated(Status $status)
|
||||
{
|
||||
if(!in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config('instance.timeline.home.cached')) {
|
||||
Cache::forget('pf:timelines:home:' . $status->profile_id);
|
||||
}
|
||||
|
@ -55,6 +59,10 @@ class StatusObserver
|
|||
*/
|
||||
public function deleted(Status $status)
|
||||
{
|
||||
if(!in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config('instance.timeline.home.cached')) {
|
||||
Cache::forget('pf:timelines:home:' . $status->profile_id);
|
||||
}
|
||||
|
|
|
@ -23,8 +23,6 @@ class RouteServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
|
@ -36,10 +34,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
public function map()
|
||||
{
|
||||
$this->mapApiRoutes();
|
||||
|
||||
$this->mapWebRoutes();
|
||||
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,6 +46,18 @@ class RouteServiceProvider extends ServiceProvider
|
|||
*/
|
||||
protected function mapWebRoutes()
|
||||
{
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web-admin.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web-portfolio.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web-api.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web.php'));
|
||||
|
|
|
@ -11,38 +11,61 @@ use Illuminate\Http\Client\RequestException;
|
|||
|
||||
class ActivityPubFetchService
|
||||
{
|
||||
public static function get($url, $validateUrl = true)
|
||||
{
|
||||
public static function get($url, $validateUrl = true)
|
||||
{
|
||||
if($validateUrl === true) {
|
||||
if(!Helpers::validateUrl($url)) {
|
||||
return 0;
|
||||
}
|
||||
if(!Helpers::validateUrl($url)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
$baseHeaders = [
|
||||
'Accept' => 'application/activity+json, application/ld+json',
|
||||
];
|
||||
$baseHeaders = [
|
||||
'Accept' => 'application/activity+json, application/ld+json',
|
||||
];
|
||||
|
||||
$headers = HttpSignature::instanceActorSign($url, false, $baseHeaders, 'get');
|
||||
$headers['Accept'] = 'application/activity+json, application/ld+json';
|
||||
$headers['User-Agent'] = 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')';
|
||||
$headers = HttpSignature::instanceActorSign($url, false, $baseHeaders, 'get');
|
||||
$headers['Accept'] = 'application/activity+json, application/ld+json';
|
||||
$headers['User-Agent'] = 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')';
|
||||
|
||||
try {
|
||||
$res = Http::withOptions(['allow_redirects' => false])->withHeaders($headers)
|
||||
->timeout(30)
|
||||
->connectTimeout(5)
|
||||
->retry(3, 500)
|
||||
->get($url);
|
||||
} catch (RequestException $e) {
|
||||
return;
|
||||
} catch (ConnectionException $e) {
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
return;
|
||||
}
|
||||
if(!$res->ok()) {
|
||||
return;
|
||||
}
|
||||
return $res->body();
|
||||
}
|
||||
try {
|
||||
$res = Http::withOptions(['allow_redirects' => false])
|
||||
->withHeaders($headers)
|
||||
->timeout(30)
|
||||
->connectTimeout(5)
|
||||
->retry(3, 500)
|
||||
->get($url);
|
||||
} catch (RequestException $e) {
|
||||
return;
|
||||
} catch (ConnectionException $e) {
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$res->ok()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$res->hasHeader('Content-Type')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$acceptedTypes = [
|
||||
'application/activity+json; charset=utf-8',
|
||||
'application/activity+json',
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
];
|
||||
|
||||
$contentType = $res->getHeader('Content-Type')[0];
|
||||
|
||||
if(!$contentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!in_array($contentType, $acceptedTypes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $res->body();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,125 +2,129 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use Cache;
|
||||
use Config;
|
||||
use App\Models\ConfigCache as ConfigCacheModel;
|
||||
use Cache;
|
||||
|
||||
class ConfigCacheService
|
||||
{
|
||||
const CACHE_KEY = 'config_cache:_v0-key:';
|
||||
const CACHE_KEY = 'config_cache:_v0-key:';
|
||||
|
||||
public static function get($key)
|
||||
{
|
||||
$cacheKey = self::CACHE_KEY . $key;
|
||||
$ttl = now()->addHours(12);
|
||||
if(!config('instance.enable_cc')) {
|
||||
return config($key);
|
||||
}
|
||||
public static function get($key)
|
||||
{
|
||||
$cacheKey = self::CACHE_KEY.$key;
|
||||
$ttl = now()->addHours(12);
|
||||
if (! config('instance.enable_cc')) {
|
||||
return config($key);
|
||||
}
|
||||
|
||||
return Cache::remember($cacheKey, $ttl, function() use($key) {
|
||||
return Cache::remember($cacheKey, $ttl, function () use ($key) {
|
||||
|
||||
$allowed = [
|
||||
'app.name',
|
||||
'app.short_description',
|
||||
'app.description',
|
||||
'app.rules',
|
||||
$allowed = [
|
||||
'app.name',
|
||||
'app.short_description',
|
||||
'app.description',
|
||||
'app.rules',
|
||||
|
||||
'pixelfed.max_photo_size',
|
||||
'pixelfed.max_album_length',
|
||||
'pixelfed.image_quality',
|
||||
'pixelfed.media_types',
|
||||
'pixelfed.max_photo_size',
|
||||
'pixelfed.max_album_length',
|
||||
'pixelfed.image_quality',
|
||||
'pixelfed.media_types',
|
||||
|
||||
'pixelfed.open_registration',
|
||||
'federation.activitypub.enabled',
|
||||
'instance.stories.enabled',
|
||||
'pixelfed.oauth_enabled',
|
||||
'pixelfed.import.instagram.enabled',
|
||||
'pixelfed.bouncer.enabled',
|
||||
'pixelfed.open_registration',
|
||||
'federation.activitypub.enabled',
|
||||
'instance.stories.enabled',
|
||||
'pixelfed.oauth_enabled',
|
||||
'pixelfed.import.instagram.enabled',
|
||||
'pixelfed.bouncer.enabled',
|
||||
|
||||
'pixelfed.enforce_email_verification',
|
||||
'pixelfed.max_account_size',
|
||||
'pixelfed.enforce_account_limit',
|
||||
'pixelfed.enforce_email_verification',
|
||||
'pixelfed.max_account_size',
|
||||
'pixelfed.enforce_account_limit',
|
||||
|
||||
'uikit.custom.css',
|
||||
'uikit.custom.js',
|
||||
'uikit.show_custom.css',
|
||||
'uikit.show_custom.js',
|
||||
'about.title',
|
||||
'uikit.custom.css',
|
||||
'uikit.custom.js',
|
||||
'uikit.show_custom.css',
|
||||
'uikit.show_custom.js',
|
||||
'about.title',
|
||||
|
||||
'pixelfed.cloud_storage',
|
||||
'pixelfed.cloud_storage',
|
||||
|
||||
'account.autofollow',
|
||||
'account.autofollow_usernames',
|
||||
'config.discover.features',
|
||||
'account.autofollow',
|
||||
'account.autofollow_usernames',
|
||||
'config.discover.features',
|
||||
|
||||
'instance.has_legal_notice',
|
||||
'instance.avatar.local_to_cloud',
|
||||
'instance.has_legal_notice',
|
||||
'instance.avatar.local_to_cloud',
|
||||
|
||||
'pixelfed.directory',
|
||||
'app.banner_image',
|
||||
'pixelfed.directory.submission-key',
|
||||
'pixelfed.directory.submission-ts',
|
||||
'pixelfed.directory.has_submitted',
|
||||
'pixelfed.directory.latest_response',
|
||||
'pixelfed.directory.is_synced',
|
||||
'pixelfed.directory.testimonials',
|
||||
'pixelfed.directory',
|
||||
'app.banner_image',
|
||||
'pixelfed.directory.submission-key',
|
||||
'pixelfed.directory.submission-ts',
|
||||
'pixelfed.directory.has_submitted',
|
||||
'pixelfed.directory.latest_response',
|
||||
'pixelfed.directory.is_synced',
|
||||
'pixelfed.directory.testimonials',
|
||||
|
||||
'instance.landing.show_directory',
|
||||
'instance.landing.show_explore',
|
||||
'instance.admin.pid',
|
||||
'instance.banner.blurhash',
|
||||
'instance.landing.show_directory',
|
||||
'instance.landing.show_explore',
|
||||
'instance.admin.pid',
|
||||
'instance.banner.blurhash',
|
||||
|
||||
'autospam.nlp.enabled',
|
||||
// 'system.user_mode'
|
||||
];
|
||||
'autospam.nlp.enabled',
|
||||
|
||||
if(!config('instance.enable_cc')) {
|
||||
return config($key);
|
||||
}
|
||||
'instance.curated_registration.enabled',
|
||||
|
||||
if(!in_array($key, $allowed)) {
|
||||
return config($key);
|
||||
}
|
||||
'federation.migration',
|
||||
// 'system.user_mode'
|
||||
];
|
||||
|
||||
$v = config($key);
|
||||
$c = ConfigCacheModel::where('k', $key)->first();
|
||||
if (! config('instance.enable_cc')) {
|
||||
return config($key);
|
||||
}
|
||||
|
||||
if($c) {
|
||||
return $c->v ?? config($key);
|
||||
}
|
||||
if (! in_array($key, $allowed)) {
|
||||
return config($key);
|
||||
}
|
||||
|
||||
if(!$v) {
|
||||
return;
|
||||
}
|
||||
$v = config($key);
|
||||
$c = ConfigCacheModel::where('k', $key)->first();
|
||||
|
||||
$cc = new ConfigCacheModel;
|
||||
$cc->k = $key;
|
||||
$cc->v = $v;
|
||||
$cc->save();
|
||||
if ($c) {
|
||||
return $c->v ?? config($key);
|
||||
}
|
||||
|
||||
return $v;
|
||||
});
|
||||
}
|
||||
if (! $v) {
|
||||
return;
|
||||
}
|
||||
|
||||
public static function put($key, $val)
|
||||
{
|
||||
$exists = ConfigCacheModel::whereK($key)->first();
|
||||
$cc = new ConfigCacheModel;
|
||||
$cc->k = $key;
|
||||
$cc->v = $v;
|
||||
$cc->save();
|
||||
|
||||
if($exists) {
|
||||
$exists->v = $val;
|
||||
$exists->save();
|
||||
Cache::put(self::CACHE_KEY . $key, $val, now()->addHours(12));
|
||||
return self::get($key);
|
||||
}
|
||||
return $v;
|
||||
});
|
||||
}
|
||||
|
||||
$cc = new ConfigCacheModel;
|
||||
$cc->k = $key;
|
||||
$cc->v = $val;
|
||||
$cc->save();
|
||||
public static function put($key, $val)
|
||||
{
|
||||
$exists = ConfigCacheModel::whereK($key)->first();
|
||||
|
||||
Cache::put(self::CACHE_KEY . $key, $val, now()->addHours(12));
|
||||
if ($exists) {
|
||||
$exists->v = $val;
|
||||
$exists->save();
|
||||
Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12));
|
||||
|
||||
return self::get($key);
|
||||
}
|
||||
return self::get($key);
|
||||
}
|
||||
|
||||
$cc = new ConfigCacheModel;
|
||||
$cc->k = $key;
|
||||
$cc->v = $val;
|
||||
$cc->save();
|
||||
|
||||
Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12));
|
||||
|
||||
return self::get($key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class FetchCacheService
|
||||
{
|
||||
const CACHE_KEY = 'pf:fetch_cache_service:getjson:';
|
||||
|
||||
public static function getJson($url, $verifyCheck = true, $ttl = 3600, $allowRedirects = true)
|
||||
{
|
||||
$vc = $verifyCheck ? 'vc1:' : 'vc0:';
|
||||
$ar = $allowRedirects ? 'ar1:' : 'ar0';
|
||||
$key = self::CACHE_KEY.sha1($url).':'.$vc.$ar.$ttl;
|
||||
if (Cache::has($key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($verifyCheck) {
|
||||
if (! Helpers::validateUrl($url)) {
|
||||
Cache::put($key, 1, $ttl);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')',
|
||||
];
|
||||
|
||||
if ($allowRedirects) {
|
||||
$options = [
|
||||
'allow_redirects' => [
|
||||
'max' => 2,
|
||||
'strict' => true,
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$options = [
|
||||
'allow_redirects' => false,
|
||||
];
|
||||
}
|
||||
try {
|
||||
$res = Http::withOptions($options)
|
||||
->retry(3, function (int $attempt, $exception) {
|
||||
return $attempt * 500;
|
||||
})
|
||||
->acceptJson()
|
||||
->withHeaders($headers)
|
||||
->timeout(40)
|
||||
->get($url);
|
||||
} catch (RequestException $e) {
|
||||
Cache::put($key, 1, $ttl);
|
||||
|
||||
return false;
|
||||
} catch (ConnectionException $e) {
|
||||
Cache::put($key, 1, $ttl);
|
||||
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
Cache::put($key, 1, $ttl);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $res->ok()) {
|
||||
Cache::put($key, 1, $ttl);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $res->json();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Internal;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
|
||||
class SoftwareUpdateService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:software-update:';
|
||||
|
||||
public static function cacheKey()
|
||||
{
|
||||
return self::CACHE_KEY . 'latest:v1.0.0';
|
||||
}
|
||||
|
||||
public static function get()
|
||||
{
|
||||
$curVersion = config('pixelfed.version');
|
||||
|
||||
$versions = Cache::remember(self::cacheKey(), 1800, function() {
|
||||
return self::fetchLatest();
|
||||
});
|
||||
|
||||
if(!$versions || !isset($versions['latest'], $versions['latest']['version'])) {
|
||||
$hideWarning = (bool) config('instance.software-update.disable_failed_warning');
|
||||
return [
|
||||
'current' => $curVersion,
|
||||
'latest' => [
|
||||
'version' => null,
|
||||
'published_at' => null,
|
||||
'url' => null,
|
||||
],
|
||||
'running_latest' => $hideWarning ? true : null
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'current' => $curVersion,
|
||||
'latest' => [
|
||||
'version' => $versions['latest']['version'],
|
||||
'published_at' => $versions['latest']['published_at'],
|
||||
'url' => $versions['latest']['url'],
|
||||
],
|
||||
'running_latest' => strval($versions['latest']['version']) === strval($curVersion)
|
||||
];
|
||||
}
|
||||
|
||||
public static function fetchLatest()
|
||||
{
|
||||
try {
|
||||
$res = Http::withOptions(['allow_redirects' => false])
|
||||
->timeout(5)
|
||||
->connectTimeout(5)
|
||||
->retry(2, 500)
|
||||
->get('https://versions.pixelfed.org/versions.json');
|
||||
} catch (RequestException $e) {
|
||||
return;
|
||||
} catch (ConnectionException $e) {
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$res->ok()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $res->json();
|
||||
}
|
||||
}
|
|
@ -48,13 +48,16 @@ class LandingService
|
|||
->toArray() : [];
|
||||
});
|
||||
|
||||
$openReg = (bool) config_cache('pixelfed.open_registration');
|
||||
|
||||
$res = [
|
||||
'name' => config_cache('app.name'),
|
||||
'url' => config_cache('app.url'),
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'show_directory' => config_cache('instance.landing.show_directory'),
|
||||
'show_explore_feed' => config_cache('instance.landing.show_explore'),
|
||||
'open_registration' => config_cache('pixelfed.open_registration') == 1,
|
||||
'open_registration' => (bool) $openReg,
|
||||
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
|
||||
'version' => config('pixelfed.version'),
|
||||
'about' => [
|
||||
'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'),
|
||||
|
|
|
@ -2,298 +2,324 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline;
|
||||
use App\Notification;
|
||||
use App\Transformer\Api\NotificationTransformer;
|
||||
use Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\{
|
||||
Notification,
|
||||
Profile
|
||||
};
|
||||
use App\Transformer\Api\NotificationTransformer;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline;
|
||||
|
||||
class NotificationService {
|
||||
class NotificationService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:notifications:ids:';
|
||||
|
||||
const CACHE_KEY = 'pf:services:notifications:ids:';
|
||||
const EPOCH_CACHE_KEY = 'pf:services:notifications:epoch-id:by-months:';
|
||||
const ITEM_CACHE_TTL = 86400;
|
||||
const MASTODON_TYPES = [
|
||||
'follow',
|
||||
'follow_request',
|
||||
'mention',
|
||||
'reblog',
|
||||
'favourite',
|
||||
'poll',
|
||||
'status'
|
||||
];
|
||||
const EPOCH_CACHE_KEY = 'pf:services:notifications:epoch-id:by-months:';
|
||||
|
||||
public static function get($id, $start = 0, $stop = 400)
|
||||
{
|
||||
$res = collect([]);
|
||||
$key = self::CACHE_KEY . $id;
|
||||
$stop = $stop > 400 ? 400 : $stop;
|
||||
$ids = Redis::zrangebyscore($key, $start, $stop);
|
||||
if(empty($ids)) {
|
||||
$ids = self::coldGet($id, $start, $stop);
|
||||
}
|
||||
foreach($ids as $id) {
|
||||
$n = self::getNotification($id);
|
||||
if($n != null) {
|
||||
$res->push($n);
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
const ITEM_CACHE_TTL = 86400;
|
||||
|
||||
public static function getEpochId($months = 6)
|
||||
{
|
||||
$epoch = Cache::get(self::EPOCH_CACHE_KEY . $months);
|
||||
if(!$epoch) {
|
||||
NotificationEpochUpdatePipeline::dispatch();
|
||||
return 1;
|
||||
}
|
||||
return $epoch;
|
||||
}
|
||||
const MASTODON_TYPES = [
|
||||
'follow',
|
||||
'follow_request',
|
||||
'mention',
|
||||
'reblog',
|
||||
'favourite',
|
||||
'poll',
|
||||
'status',
|
||||
];
|
||||
|
||||
public static function coldGet($id, $start = 0, $stop = 400)
|
||||
{
|
||||
$stop = $stop > 400 ? 400 : $stop;
|
||||
$ids = Notification::where('id', '>', self::getEpochId())
|
||||
->where('profile_id', $id)
|
||||
->orderByDesc('id')
|
||||
->skip($start)
|
||||
->take($stop)
|
||||
->pluck('id');
|
||||
foreach($ids as $key) {
|
||||
self::set($id, $key);
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
public static function get($id, $start = 0, $stop = 400)
|
||||
{
|
||||
$res = collect([]);
|
||||
$key = self::CACHE_KEY.$id;
|
||||
$stop = $stop > 400 ? 400 : $stop;
|
||||
$ids = Redis::zrangebyscore($key, $start, $stop);
|
||||
if (empty($ids)) {
|
||||
$ids = self::coldGet($id, $start, $stop);
|
||||
}
|
||||
foreach ($ids as $id) {
|
||||
$n = self::getNotification($id);
|
||||
if ($n != null) {
|
||||
$res->push($n);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getMax($id = false, $start = 0, $limit = 10)
|
||||
{
|
||||
$ids = self::getRankedMaxId($id, $start, $limit);
|
||||
return $res;
|
||||
}
|
||||
|
||||
if(empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
public static function getEpochId($months = 6)
|
||||
{
|
||||
$epoch = Cache::get(self::EPOCH_CACHE_KEY.$months);
|
||||
if (! $epoch) {
|
||||
NotificationEpochUpdatePipeline::dispatch();
|
||||
|
||||
$res = collect([]);
|
||||
foreach($ids as $id) {
|
||||
$n = self::getNotification($id);
|
||||
if($n != null) {
|
||||
$res->push($n);
|
||||
}
|
||||
}
|
||||
return $res->toArray();
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public static function getMin($id = false, $start = 0, $limit = 10)
|
||||
{
|
||||
$ids = self::getRankedMinId($id, $start, $limit);
|
||||
return $epoch;
|
||||
}
|
||||
|
||||
if(empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
public static function coldGet($id, $start = 0, $stop = 400)
|
||||
{
|
||||
$stop = $stop > 400 ? 400 : $stop;
|
||||
$ids = Notification::where('id', '>', self::getEpochId())
|
||||
->where('profile_id', $id)
|
||||
->orderByDesc('id')
|
||||
->skip($start)
|
||||
->take($stop)
|
||||
->pluck('id');
|
||||
foreach ($ids as $key) {
|
||||
self::set($id, $key);
|
||||
}
|
||||
|
||||
$res = collect([]);
|
||||
foreach($ids as $id) {
|
||||
$n = self::getNotification($id);
|
||||
if($n != null) {
|
||||
$res->push($n);
|
||||
}
|
||||
}
|
||||
return $res->toArray();
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
public static function getMax($id = false, $start = 0, $limit = 10)
|
||||
{
|
||||
$ids = self::getRankedMaxId($id, $start, $limit);
|
||||
|
||||
public static function getMaxMastodon($id = false, $start = 0, $limit = 10)
|
||||
{
|
||||
$ids = self::getRankedMaxId($id, $start, $limit);
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if(empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
$res = collect([]);
|
||||
foreach ($ids as $id) {
|
||||
$n = self::getNotification($id);
|
||||
if ($n != null) {
|
||||
$res->push($n);
|
||||
}
|
||||
}
|
||||
|
||||
$res = collect([]);
|
||||
foreach($ids as $id) {
|
||||
$n = self::rewriteMastodonTypes(self::getNotification($id));
|
||||
if($n != null && in_array($n['type'], self::MASTODON_TYPES)) {
|
||||
if(isset($n['account'])) {
|
||||
$n['account'] = AccountService::getMastodon($n['account']['id']);
|
||||
}
|
||||
return $res->toArray();
|
||||
}
|
||||
|
||||
if(isset($n['relationship'])) {
|
||||
unset($n['relationship']);
|
||||
}
|
||||
public static function getMin($id = false, $start = 0, $limit = 10)
|
||||
{
|
||||
$ids = self::getRankedMinId($id, $start, $limit);
|
||||
|
||||
if(isset($n['status'])) {
|
||||
$n['status'] = StatusService::getMastodon($n['status']['id'], false);
|
||||
}
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$res->push($n);
|
||||
}
|
||||
}
|
||||
return $res->toArray();
|
||||
}
|
||||
$res = collect([]);
|
||||
foreach ($ids as $id) {
|
||||
$n = self::getNotification($id);
|
||||
if ($n != null) {
|
||||
$res->push($n);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getMinMastodon($id = false, $start = 0, $limit = 10)
|
||||
{
|
||||
$ids = self::getRankedMinId($id, $start, $limit);
|
||||
return $res->toArray();
|
||||
}
|
||||
|
||||
if(empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
public static function getMaxMastodon($id = false, $start = 0, $limit = 10)
|
||||
{
|
||||
$ids = self::getRankedMaxId($id, $start, $limit);
|
||||
|
||||
$res = collect([]);
|
||||
foreach($ids as $id) {
|
||||
$n = self::rewriteMastodonTypes(self::getNotification($id));
|
||||
if($n != null && in_array($n['type'], self::MASTODON_TYPES)) {
|
||||
if(isset($n['account'])) {
|
||||
$n['account'] = AccountService::getMastodon($n['account']['id']);
|
||||
}
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if(isset($n['relationship'])) {
|
||||
unset($n['relationship']);
|
||||
}
|
||||
$res = collect([]);
|
||||
foreach ($ids as $id) {
|
||||
$n = self::rewriteMastodonTypes(self::getNotification($id));
|
||||
if ($n != null && in_array($n['type'], self::MASTODON_TYPES)) {
|
||||
if (isset($n['account'])) {
|
||||
$n['account'] = AccountService::getMastodon($n['account']['id']);
|
||||
}
|
||||
|
||||
if(isset($n['status'])) {
|
||||
$n['status'] = StatusService::getMastodon($n['status']['id'], false);
|
||||
}
|
||||
if (isset($n['relationship'])) {
|
||||
unset($n['relationship']);
|
||||
}
|
||||
|
||||
$res->push($n);
|
||||
}
|
||||
}
|
||||
return $res->toArray();
|
||||
}
|
||||
if ($n['type'] === 'mention' && isset($n['tagged'], $n['tagged']['status_id'])) {
|
||||
$n['status'] = StatusService::getMastodon($n['tagged']['status_id'], false);
|
||||
unset($n['tagged']);
|
||||
}
|
||||
|
||||
public static function getRankedMaxId($id = false, $start = null, $limit = 10)
|
||||
{
|
||||
if(!$start || !$id) {
|
||||
return [];
|
||||
}
|
||||
if (isset($n['status'])) {
|
||||
$n['status'] = StatusService::getMastodon($n['status']['id'], false);
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, $start, '-inf', [
|
||||
'withscores' => true,
|
||||
'limit' => [1, $limit]
|
||||
]));
|
||||
}
|
||||
$res->push($n);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getRankedMinId($id = false, $end = null, $limit = 10)
|
||||
{
|
||||
if(!$end || !$id) {
|
||||
return [];
|
||||
}
|
||||
return $res->toArray();
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, '+inf', $end, [
|
||||
'withscores' => true,
|
||||
'limit' => [0, $limit]
|
||||
]));
|
||||
}
|
||||
public static function getMinMastodon($id = false, $start = 0, $limit = 10)
|
||||
{
|
||||
$ids = self::getRankedMinId($id, $start, $limit);
|
||||
|
||||
public static function rewriteMastodonTypes($notification)
|
||||
{
|
||||
if(!$notification || !isset($notification['type'])) {
|
||||
return $notification;
|
||||
}
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if($notification['type'] === 'comment') {
|
||||
$notification['type'] = 'mention';
|
||||
}
|
||||
$res = collect([]);
|
||||
foreach ($ids as $id) {
|
||||
$n = self::rewriteMastodonTypes(self::getNotification($id));
|
||||
if ($n != null && in_array($n['type'], self::MASTODON_TYPES)) {
|
||||
if (isset($n['account'])) {
|
||||
$n['account'] = AccountService::getMastodon($n['account']['id']);
|
||||
}
|
||||
|
||||
if($notification['type'] === 'share') {
|
||||
$notification['type'] = 'reblog';
|
||||
}
|
||||
if (isset($n['relationship'])) {
|
||||
unset($n['relationship']);
|
||||
}
|
||||
|
||||
return $notification;
|
||||
}
|
||||
if ($n['type'] === 'mention' && isset($n['tagged'], $n['tagged']['status_id'])) {
|
||||
$n['status'] = StatusService::getMastodon($n['tagged']['status_id'], false);
|
||||
unset($n['tagged']);
|
||||
}
|
||||
|
||||
public static function set($id, $val)
|
||||
{
|
||||
if(self::count($id) > 400) {
|
||||
Redis::zpopmin(self::CACHE_KEY . $id);
|
||||
}
|
||||
return Redis::zadd(self::CACHE_KEY . $id, $val, $val);
|
||||
}
|
||||
if (isset($n['status'])) {
|
||||
$n['status'] = StatusService::getMastodon($n['status']['id'], false);
|
||||
}
|
||||
|
||||
public static function del($id, $val)
|
||||
{
|
||||
Cache::forget('service:notification:' . $val);
|
||||
return Redis::zrem(self::CACHE_KEY . $id, $val);
|
||||
}
|
||||
$res->push($n);
|
||||
}
|
||||
}
|
||||
|
||||
public static function add($id, $val)
|
||||
{
|
||||
return self::set($id, $val);
|
||||
}
|
||||
return $res->toArray();
|
||||
}
|
||||
|
||||
public static function rem($id, $val)
|
||||
{
|
||||
return self::del($id, $val);
|
||||
}
|
||||
public static function getRankedMaxId($id = false, $start = null, $limit = 10)
|
||||
{
|
||||
if (! $start || ! $id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function count($id)
|
||||
{
|
||||
return Redis::zcount(self::CACHE_KEY . $id, '-inf', '+inf');
|
||||
}
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, $start, '-inf', [
|
||||
'withscores' => true,
|
||||
'limit' => [1, $limit],
|
||||
]));
|
||||
}
|
||||
|
||||
public static function getNotification($id)
|
||||
{
|
||||
$notification = Cache::remember('service:notification:'.$id, self::ITEM_CACHE_TTL, function() use($id) {
|
||||
$n = Notification::with('item')->find($id);
|
||||
public static function getRankedMinId($id = false, $end = null, $limit = 10)
|
||||
{
|
||||
if (! $end || ! $id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if(!$n) {
|
||||
return null;
|
||||
}
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, '+inf', $end, [
|
||||
'withscores' => true,
|
||||
'limit' => [0, $limit],
|
||||
]));
|
||||
}
|
||||
|
||||
$account = AccountService::get($n->actor_id, true);
|
||||
public static function rewriteMastodonTypes($notification)
|
||||
{
|
||||
if (! $notification || ! isset($notification['type'])) {
|
||||
return $notification;
|
||||
}
|
||||
|
||||
if(!$account) {
|
||||
return null;
|
||||
}
|
||||
if ($notification['type'] === 'comment') {
|
||||
$notification['type'] = 'mention';
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($n, new NotificationTransformer());
|
||||
return $fractal->createData($resource)->toArray();
|
||||
});
|
||||
if ($notification['type'] === 'share') {
|
||||
$notification['type'] = 'reblog';
|
||||
}
|
||||
|
||||
if(!$notification) {
|
||||
return;
|
||||
}
|
||||
if ($notification['type'] === 'tagged') {
|
||||
$notification['type'] = 'mention';
|
||||
}
|
||||
|
||||
if(isset($notification['account'])) {
|
||||
$notification['account'] = AccountService::get($notification['account']['id'], true);
|
||||
}
|
||||
return $notification;
|
||||
}
|
||||
|
||||
return $notification;
|
||||
}
|
||||
public static function set($id, $val)
|
||||
{
|
||||
if (self::count($id) > 400) {
|
||||
Redis::zpopmin(self::CACHE_KEY.$id);
|
||||
}
|
||||
|
||||
public static function setNotification(Notification $notification)
|
||||
{
|
||||
return Cache::remember('service:notification:'.$notification->id, self::ITEM_CACHE_TTL, function() use($notification) {
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($notification, new NotificationTransformer());
|
||||
return $fractal->createData($resource)->toArray();
|
||||
});
|
||||
}
|
||||
return Redis::zadd(self::CACHE_KEY.$id, $val, $val);
|
||||
}
|
||||
|
||||
public static function warmCache($id, $stop = 400, $force = false)
|
||||
{
|
||||
if(self::count($id) == 0 || $force == true) {
|
||||
$ids = Notification::where('profile_id', $id)
|
||||
->where('id', '>', self::getEpochId())
|
||||
->orderByDesc('id')
|
||||
->limit($stop)
|
||||
->pluck('id');
|
||||
foreach($ids as $key) {
|
||||
self::set($id, $key);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
public static function del($id, $val)
|
||||
{
|
||||
Cache::forget('service:notification:'.$val);
|
||||
|
||||
return Redis::zrem(self::CACHE_KEY.$id, $val);
|
||||
}
|
||||
|
||||
public static function add($id, $val)
|
||||
{
|
||||
return self::set($id, $val);
|
||||
}
|
||||
|
||||
public static function rem($id, $val)
|
||||
{
|
||||
return self::del($id, $val);
|
||||
}
|
||||
|
||||
public static function count($id)
|
||||
{
|
||||
return Redis::zcount(self::CACHE_KEY.$id, '-inf', '+inf');
|
||||
}
|
||||
|
||||
public static function getNotification($id)
|
||||
{
|
||||
$notification = Cache::remember('service:notification:'.$id, self::ITEM_CACHE_TTL, function () use ($id) {
|
||||
$n = Notification::with('item')->find($id);
|
||||
|
||||
if (! $n) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$account = AccountService::get($n->actor_id, true);
|
||||
|
||||
if (! $account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($n, new NotificationTransformer());
|
||||
|
||||
return $fractal->createData($resource)->toArray();
|
||||
});
|
||||
|
||||
if (! $notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($notification['account'])) {
|
||||
$notification['account'] = AccountService::get($notification['account']['id'], true);
|
||||
}
|
||||
|
||||
return $notification;
|
||||
}
|
||||
|
||||
public static function setNotification(Notification $notification)
|
||||
{
|
||||
return Cache::remember('service:notification:'.$notification->id, self::ITEM_CACHE_TTL, function () use ($notification) {
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($notification, new NotificationTransformer());
|
||||
|
||||
return $fractal->createData($resource)->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
public static function warmCache($id, $stop = 400, $force = false)
|
||||
{
|
||||
if (self::count($id) == 0 || $force == true) {
|
||||
$ids = Notification::where('profile_id', $id)
|
||||
->where('id', '>', self::getEpochId())
|
||||
->orderByDesc('id')
|
||||
->limit($stop)
|
||||
->pluck('id');
|
||||
foreach ($ids as $key) {
|
||||
self::set($id, $key);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,69 +2,95 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use Cache;
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Util\Webfinger\WebfingerUrl;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class WebfingerService
|
||||
{
|
||||
public static function lookup($query, $mastodonMode = false)
|
||||
{
|
||||
return (new self)->run($query, $mastodonMode);
|
||||
}
|
||||
public static function rawGet($url)
|
||||
{
|
||||
$n = WebfingerUrl::get($url);
|
||||
if (! $n) {
|
||||
return false;
|
||||
}
|
||||
$webfinger = FetchCacheService::getJson($n);
|
||||
if (! $webfinger) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function run($query, $mastodonMode)
|
||||
{
|
||||
if($profile = Profile::whereUsername($query)->first()) {
|
||||
return $mastodonMode ?
|
||||
AccountService::getMastodon($profile->id, true) :
|
||||
AccountService::get($profile->id);
|
||||
}
|
||||
$url = WebfingerUrl::generateWebfingerUrl($query);
|
||||
if(!Helpers::validateUrl($url)) {
|
||||
return [];
|
||||
}
|
||||
if (! isset($webfinger['links']) || ! is_array($webfinger['links']) || empty($webfinger['links'])) {
|
||||
return false;
|
||||
}
|
||||
$link = collect($webfinger['links'])
|
||||
->filter(function ($link) {
|
||||
return $link &&
|
||||
isset($link['rel'], $link['type'], $link['href']) &&
|
||||
$link['rel'] === 'self' &&
|
||||
in_array($link['type'], ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']);
|
||||
})
|
||||
->pluck('href')
|
||||
->first();
|
||||
|
||||
try {
|
||||
$res = Http::retry(3, 100)
|
||||
->acceptJson()
|
||||
->withHeaders([
|
||||
'User-Agent' => '(Pixelfed/' . config('pixelfed.version') . '; +' . config('app.url') . ')'
|
||||
])
|
||||
->timeout(20)
|
||||
->get($url);
|
||||
} catch (\Illuminate\Http\Client\ConnectionException $e) {
|
||||
return [];
|
||||
}
|
||||
return $link;
|
||||
}
|
||||
|
||||
if(!$res->successful()) {
|
||||
return [];
|
||||
}
|
||||
public static function lookup($query, $mastodonMode = false)
|
||||
{
|
||||
return (new self)->run($query, $mastodonMode);
|
||||
}
|
||||
|
||||
$webfinger = $res->json();
|
||||
if(!isset($webfinger['links']) || !is_array($webfinger['links']) || empty($webfinger['links'])) {
|
||||
return [];
|
||||
}
|
||||
protected function run($query, $mastodonMode)
|
||||
{
|
||||
if ($profile = Profile::whereUsername($query)->first()) {
|
||||
return $mastodonMode ?
|
||||
AccountService::getMastodon($profile->id, true) :
|
||||
AccountService::get($profile->id);
|
||||
}
|
||||
$url = WebfingerUrl::generateWebfingerUrl($query);
|
||||
if (! Helpers::validateUrl($url)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$link = collect($webfinger['links'])
|
||||
->filter(function($link) {
|
||||
return $link &&
|
||||
isset($link['rel'], $link['type'], $link['href']) &&
|
||||
$link['rel'] === 'self' &&
|
||||
in_array($link['type'], ['application/activity+json','application/ld+json; profile="https://www.w3.org/ns/activitystreams"']);
|
||||
})
|
||||
->pluck('href')
|
||||
->first();
|
||||
try {
|
||||
$res = Http::retry(3, 100)
|
||||
->acceptJson()
|
||||
->withHeaders([
|
||||
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')',
|
||||
])
|
||||
->timeout(20)
|
||||
->get($url);
|
||||
} catch (\Illuminate\Http\Client\ConnectionException $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$profile = Helpers::profileFetch($link);
|
||||
if(!$profile) {
|
||||
return;
|
||||
}
|
||||
return $mastodonMode ?
|
||||
AccountService::getMastodon($profile->id, true) :
|
||||
AccountService::get($profile->id);
|
||||
}
|
||||
if (! $res->successful()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$webfinger = $res->json();
|
||||
if (! isset($webfinger['links']) || ! is_array($webfinger['links']) || empty($webfinger['links'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$link = collect($webfinger['links'])
|
||||
->filter(function ($link) {
|
||||
return $link &&
|
||||
isset($link['rel'], $link['type'], $link['href']) &&
|
||||
$link['rel'] === 'self' &&
|
||||
in_array($link['type'], ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']);
|
||||
})
|
||||
->pluck('href')
|
||||
->first();
|
||||
|
||||
$profile = Helpers::profileFetch($link);
|
||||
if (! $profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $mastodonMode ?
|
||||
AccountService::getMastodon($profile->id, true) :
|
||||
AccountService::get($profile->id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use App\Models\ProfileMigration;
|
||||
use League\Fractal;
|
||||
|
||||
class Move extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(ProfileMigration $migration)
|
||||
{
|
||||
$objUrl = $migration->target->permalink();
|
||||
$id = $migration->target->permalink('#moves/'.$migration->id);
|
||||
$to = $migration->target->permalink('/followers');
|
||||
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $id,
|
||||
'actor' => $objUrl,
|
||||
'type' => 'Move',
|
||||
'object' => $objUrl,
|
||||
'target' => $migration->profile->permalink(),
|
||||
'to' => $to,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use Auth;
|
||||
use Cache;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use League\Fractal;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\PronounService;
|
||||
use App\User;
|
||||
use App\UserSetting;
|
||||
use Cache;
|
||||
use League\Fractal;
|
||||
|
||||
class AccountTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
|
@ -15,47 +16,77 @@ class AccountTransformer extends Fractal\TransformerAbstract
|
|||
// 'relationship',
|
||||
];
|
||||
|
||||
public function transform(Profile $profile)
|
||||
{
|
||||
if(!$profile) {
|
||||
return [];
|
||||
}
|
||||
public function transform(Profile $profile)
|
||||
{
|
||||
if (! $profile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$adminIds = Cache::remember('pf:admin-ids', 604800, function() {
|
||||
return User::whereIsAdmin(true)->pluck('profile_id')->toArray();
|
||||
});
|
||||
$adminIds = Cache::remember('pf:admin-ids', 604800, function () {
|
||||
return User::whereIsAdmin(true)->pluck('profile_id')->toArray();
|
||||
});
|
||||
|
||||
$local = $profile->private_key != null;
|
||||
$is_admin = !$local ? false : in_array($profile->id, $adminIds);
|
||||
$acct = $local ? $profile->username : substr($profile->username, 1);
|
||||
$username = $local ? $profile->username : explode('@', $acct)[0];
|
||||
return [
|
||||
'id' => (string) $profile->id,
|
||||
'username' => $username,
|
||||
'acct' => $acct,
|
||||
'display_name' => $profile->name,
|
||||
'discoverable' => true,
|
||||
'locked' => (bool) $profile->is_private,
|
||||
'followers_count' => (int) $profile->followers_count,
|
||||
'following_count' => (int) $profile->following_count,
|
||||
'statuses_count' => (int) $profile->status_count,
|
||||
'note' => $profile->bio ?? '',
|
||||
'note_text' => $profile->bio ? strip_tags($profile->bio) : null,
|
||||
'url' => $profile->url(),
|
||||
'avatar' => $profile->avatarUrl(),
|
||||
'website' => $profile->website,
|
||||
'local' => (bool) $local,
|
||||
'is_admin' => (bool) $is_admin,
|
||||
'created_at' => $profile->created_at->toJSON(),
|
||||
'header_bg' => $profile->header_bg,
|
||||
'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(),
|
||||
'pronouns' => PronounService::get($profile->id),
|
||||
'location' => $profile->location
|
||||
];
|
||||
}
|
||||
$local = $profile->private_key != null;
|
||||
$local = $profile->user_id && $profile->private_key != null;
|
||||
$hideFollowing = false;
|
||||
$hideFollowers = false;
|
||||
if ($local) {
|
||||
$hideFollowing = Cache::remember('pf:acct-trans:hideFollowing:'.$profile->id, 2592000, function () use ($profile) {
|
||||
$settings = UserSetting::whereUserId($profile->user_id)->first();
|
||||
if (! $settings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function includeRelationship(Profile $profile)
|
||||
{
|
||||
return $this->item($profile, new RelationshipTransformer());
|
||||
}
|
||||
return $settings->show_profile_following_count == false;
|
||||
});
|
||||
$hideFollowers = Cache::remember('pf:acct-trans:hideFollowers:'.$profile->id, 2592000, function () use ($profile) {
|
||||
$settings = UserSetting::whereUserId($profile->user_id)->first();
|
||||
if (! $settings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $settings->show_profile_follower_count == false;
|
||||
});
|
||||
}
|
||||
$is_admin = ! $local ? false : in_array($profile->id, $adminIds);
|
||||
$acct = $local ? $profile->username : substr($profile->username, 1);
|
||||
$username = $local ? $profile->username : explode('@', $acct)[0];
|
||||
$res = [
|
||||
'id' => (string) $profile->id,
|
||||
'username' => $username,
|
||||
'acct' => $acct,
|
||||
'display_name' => $profile->name,
|
||||
'discoverable' => true,
|
||||
'locked' => (bool) $profile->is_private,
|
||||
'followers_count' => $hideFollowers ? 0 : (int) $profile->followers_count,
|
||||
'following_count' => $hideFollowing ? 0 : (int) $profile->following_count,
|
||||
'statuses_count' => (int) $profile->status_count,
|
||||
'note' => $profile->bio ?? '',
|
||||
'note_text' => $profile->bio ? strip_tags($profile->bio) : null,
|
||||
'url' => $profile->url(),
|
||||
'avatar' => $profile->avatarUrl(),
|
||||
'website' => $profile->website,
|
||||
'local' => (bool) $local,
|
||||
'is_admin' => (bool) $is_admin,
|
||||
'created_at' => $profile->created_at->toJSON(),
|
||||
'header_bg' => $profile->header_bg,
|
||||
'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(),
|
||||
'pronouns' => PronounService::get($profile->id),
|
||||
'location' => $profile->location,
|
||||
];
|
||||
|
||||
if ($profile->moved_to_profile_id) {
|
||||
$mt = AccountService::getMastodon($profile->moved_to_profile_id, true);
|
||||
if ($mt) {
|
||||
$res['moved'] = $mt;
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function includeRelationship(Profile $profile)
|
||||
{
|
||||
return $this->item($profile, new RelationshipTransformer());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,78 +4,81 @@ namespace App\Transformer\Api;
|
|||
|
||||
use App\Notification;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\RelationshipService;
|
||||
use App\Services\StatusService;
|
||||
use League\Fractal;
|
||||
|
||||
class NotificationTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Notification $notification)
|
||||
{
|
||||
$res = [
|
||||
'id' => (string) $notification->id,
|
||||
'type' => $this->replaceTypeVerb($notification->action),
|
||||
'created_at' => (string) str_replace('+00:00', 'Z', $notification->created_at->format(DATE_RFC3339_EXTENDED)),
|
||||
];
|
||||
public function transform(Notification $notification)
|
||||
{
|
||||
$res = [
|
||||
'id' => (string) $notification->id,
|
||||
'type' => $this->replaceTypeVerb($notification->action),
|
||||
'created_at' => (string) str_replace('+00:00', 'Z', $notification->created_at->format(DATE_RFC3339_EXTENDED)),
|
||||
];
|
||||
|
||||
$n = $notification;
|
||||
$n = $notification;
|
||||
|
||||
if($n->actor_id) {
|
||||
$res['account'] = AccountService::get($n->actor_id);
|
||||
if($n->profile_id != $n->actor_id) {
|
||||
$res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id);
|
||||
}
|
||||
}
|
||||
if ($n->actor_id) {
|
||||
$res['account'] = AccountService::get($n->actor_id);
|
||||
if ($n->profile_id != $n->actor_id) {
|
||||
$res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id);
|
||||
}
|
||||
}
|
||||
|
||||
if($n->item_id && $n->item_type == 'App\Status') {
|
||||
$res['status'] = StatusService::get($n->item_id, false);
|
||||
}
|
||||
if ($n->item_id && $n->item_type == 'App\Status') {
|
||||
$res['status'] = StatusService::get($n->item_id, false);
|
||||
}
|
||||
|
||||
if($n->item_id && $n->item_type == 'App\ModLog') {
|
||||
$ml = $n->item;
|
||||
if($ml && $ml->object_uid) {
|
||||
$res['modlog'] = [
|
||||
'id' => $ml->object_uid,
|
||||
'url' => url('/i/admin/users/modlogs/' . $ml->object_uid)
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($n->item_id && $n->item_type == 'App\ModLog') {
|
||||
$ml = $n->item;
|
||||
if ($ml && $ml->object_uid) {
|
||||
$res['modlog'] = [
|
||||
'id' => $ml->object_uid,
|
||||
'url' => url('/i/admin/users/modlogs/'.$ml->object_uid),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if($n->item_id && $n->item_type == 'App\MediaTag') {
|
||||
$ml = $n->item;
|
||||
if($ml && $ml->tagged_username) {
|
||||
$res['tagged'] = [
|
||||
'username' => $ml->tagged_username,
|
||||
'post_url' => '/p/'.HashidService::encode($ml->status_id)
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($n->item_id && $n->item_type == 'App\MediaTag') {
|
||||
$ml = $n->item;
|
||||
if ($ml && $ml->tagged_username) {
|
||||
$np = StatusService::get($ml->status_id, false);
|
||||
if ($np && isset($np['id'])) {
|
||||
$res['tagged'] = [
|
||||
'username' => $ml->tagged_username,
|
||||
'post_url' => $np['url'],
|
||||
'status_id' => $ml->status_id,
|
||||
'profile_id' => $ml->profile_id,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function replaceTypeVerb($verb)
|
||||
{
|
||||
$verbs = [
|
||||
'dm' => 'direct',
|
||||
'follow' => 'follow',
|
||||
'mention' => 'mention',
|
||||
'reblog' => 'share',
|
||||
'share' => 'share',
|
||||
'like' => 'favourite',
|
||||
'group:like' => 'favourite',
|
||||
'comment' => 'comment',
|
||||
'admin.user.modlog.comment' => 'modlog',
|
||||
'tagged' => 'tagged',
|
||||
'story:react' => 'story:react',
|
||||
'story:comment' => 'story:comment',
|
||||
];
|
||||
public function replaceTypeVerb($verb)
|
||||
{
|
||||
$verbs = [
|
||||
'dm' => 'direct',
|
||||
'follow' => 'follow',
|
||||
'mention' => 'mention',
|
||||
'reblog' => 'share',
|
||||
'share' => 'share',
|
||||
'like' => 'favourite',
|
||||
'comment' => 'comment',
|
||||
'admin.user.modlog.comment' => 'modlog',
|
||||
'tagged' => 'tagged',
|
||||
'story:react' => 'story:react',
|
||||
'story:comment' => 'story:comment',
|
||||
];
|
||||
|
||||
if(!isset($verbs[$verb])) {
|
||||
return $verb;
|
||||
}
|
||||
if (! isset($verbs[$verb])) {
|
||||
return $verb;
|
||||
}
|
||||
|
||||
return $verbs[$verb];
|
||||
}
|
||||
return $verbs[$verb];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -315,6 +315,23 @@ class Helpers {
|
|||
return;
|
||||
}
|
||||
|
||||
if(config('autospam.live_filters.enabled')) {
|
||||
$filters = config('autospam.live_filters.filters');
|
||||
if(!empty($filters) && isset($res['content']) && !empty($res['content']) && strlen($filters) > 3) {
|
||||
$filters = array_map('trim', explode(',', $filters));
|
||||
$content = $res['content'];
|
||||
foreach($filters as $filter) {
|
||||
$filter = trim(strtolower($filter));
|
||||
if(!$filter || !strlen($filter)) {
|
||||
continue;
|
||||
}
|
||||
if(str_contains(strtolower($content), $filter)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($res['object'])) {
|
||||
$activity = $res;
|
||||
} else {
|
||||
|
@ -372,6 +389,10 @@ class Helpers {
|
|||
$idDomain = parse_url($id, PHP_URL_HOST);
|
||||
$urlDomain = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
if($idDomain && $urlDomain && strtolower($idDomain) !== strtolower($urlDomain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!self::validateUrl($id)) {
|
||||
return;
|
||||
}
|
||||
|
@ -455,14 +476,21 @@ class Helpers {
|
|||
|
||||
public static function storeStatus($url, $profile, $activity)
|
||||
{
|
||||
$originalUrl = $url;
|
||||
$id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($activity['url']);
|
||||
$url = isset($activity['url']) && is_string($activity['url']) ? self::pluckval($activity['url']) : self::pluckval($id);
|
||||
$idDomain = parse_url($id, PHP_URL_HOST);
|
||||
$urlDomain = parse_url($url, PHP_URL_HOST);
|
||||
$originalUrlDomain = parse_url($originalUrl, PHP_URL_HOST);
|
||||
if(!self::validateUrl($id) || !self::validateUrl($url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if( strtolower($originalUrlDomain) !== strtolower($idDomain) ||
|
||||
strtolower($originalUrlDomain) !== strtolower($urlDomain) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reply_to = self::getReplyTo($activity);
|
||||
|
||||
$ts = self::pluckval($activity['published']);
|
||||
|
@ -763,7 +791,11 @@ class Helpers {
|
|||
if(!$res || isset($res['id']) == false) {
|
||||
return;
|
||||
}
|
||||
$urlDomain = parse_url($url, PHP_URL_HOST);
|
||||
$domain = parse_url($res['id'], PHP_URL_HOST);
|
||||
if(strtolower($urlDomain) !== strtolower($domain)) {
|
||||
return;
|
||||
}
|
||||
if(!isset($res['preferredUsername']) && !isset($res['nickname'])) {
|
||||
return;
|
||||
}
|
||||
|
@ -831,6 +863,9 @@ class Helpers {
|
|||
|
||||
public static function sendSignedObject($profile, $url, $body)
|
||||
{
|
||||
if(app()->environment() !== 'production') {
|
||||
return;
|
||||
}
|
||||
ActivityPubDeliveryService::queue()
|
||||
->from($profile)
|
||||
->to($url)
|
||||
|
|
|
@ -197,6 +197,22 @@ class Inbox
|
|||
public function handleCreateActivity()
|
||||
{
|
||||
$activity = $this->payload['object'];
|
||||
if(config('autospam.live_filters.enabled')) {
|
||||
$filters = config('autospam.live_filters.filters');
|
||||
if(!empty($filters) && isset($activity['content']) && !empty($activity['content']) && strlen($filters) > 3) {
|
||||
$filters = array_map('trim', explode(',', $filters));
|
||||
$content = $activity['content'];
|
||||
foreach($filters as $filter) {
|
||||
$filter = trim(strtolower($filter));
|
||||
if(!$filter || !strlen($filter)) {
|
||||
continue;
|
||||
}
|
||||
if(str_contains(strtolower($content), $filter)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$actor = $this->actorFirstOrCreate($this->payload['actor']);
|
||||
if(!$actor || $actor->domain == null) {
|
||||
return;
|
||||
|
@ -407,7 +423,7 @@ class Inbox
|
|||
$status->uri = $activity['id'];
|
||||
$status->object_url = $activity['id'];
|
||||
$status->in_reply_to_profile_id = $profile->id;
|
||||
$status->saveQuietly();
|
||||
$status->save();
|
||||
|
||||
$dm = new DirectMessage;
|
||||
$dm->to_id = $profile->id;
|
||||
|
@ -1227,7 +1243,14 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
|
||||
$content = isset($this->payload['content']) ? Purify::clean($this->payload['content']) : null;
|
||||
$content = null;
|
||||
if(isset($this->payload['content'])) {
|
||||
if(strlen($this->payload['content']) > 5000) {
|
||||
$content = Purify::clean(substr($this->payload['content'], 0, 5000) . ' ... (truncated message due to exceeding max length)');
|
||||
} else {
|
||||
$content = Purify::clean($this->payload['content']);
|
||||
}
|
||||
}
|
||||
$object = $this->payload['object'];
|
||||
|
||||
if(empty($object) || (!is_array($object) && !is_string($object))) {
|
||||
|
@ -1243,7 +1266,7 @@ class Inbox
|
|||
|
||||
foreach($object as $objectUrl) {
|
||||
if(!Helpers::validateLocalUrl($objectUrl)) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
if(str_contains($objectUrl, '/users/')) {
|
||||
|
@ -1260,10 +1283,27 @@ class Inbox
|
|||
}
|
||||
}
|
||||
|
||||
if(!$accountId || !$objects->count()) {
|
||||
if(!$accountId && !$objects->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($objects->count()) {
|
||||
$obc = $objects->count();
|
||||
if($obc > 25) {
|
||||
if($obc > 30) {
|
||||
return;
|
||||
} else {
|
||||
$objLimit = $objects->take(20);
|
||||
$objects = collect($objLimit->all());
|
||||
$obc = $objects->count();
|
||||
}
|
||||
}
|
||||
$count = Status::whereProfileId($accountId)->find($objects)->count();
|
||||
if($obc !== $count) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$instanceHost = parse_url($id, PHP_URL_HOST);
|
||||
|
||||
$instance = Instance::updateOrCreate([
|
||||
|
|
|
@ -3,16 +3,28 @@
|
|||
namespace App\Util\Webfinger;
|
||||
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Services\InstanceService;
|
||||
|
||||
class WebfingerUrl
|
||||
{
|
||||
public static function get($url)
|
||||
{
|
||||
$n = Nickname::normalizeProfileUrl($url);
|
||||
if(!$n || !isset($n['domain'], $n['username'])) {
|
||||
return false;
|
||||
}
|
||||
if(in_array($n['domain'], InstanceService::getBannedDomains())) {
|
||||
return false;
|
||||
}
|
||||
return 'https://' . $n['domain'] . '/.well-known/webfinger?resource=acct:' . $n['username'] . '@' . $n['domain'];
|
||||
}
|
||||
|
||||
public static function generateWebfingerUrl($url)
|
||||
{
|
||||
$url = Nickname::normalizeProfileUrl($url);
|
||||
$domain = $url['domain'];
|
||||
$username = $url['username'];
|
||||
$path = "https://{$domain}/.well-known/webfinger?resource=acct:{$username}@{$domain}";
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"license": "AGPL-3.0-only",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": "^8.1|^8.2",
|
||||
"php": "^8.1|^8.2|^8.3",
|
||||
"ext-bcmath": "*",
|
||||
"ext-ctype": "*",
|
||||
"ext-curl": "*",
|
||||
|
@ -16,7 +16,7 @@
|
|||
"bacon/bacon-qr-code": "^2.0.3",
|
||||
"beyondcode/laravel-websockets": "^1.13",
|
||||
"brick/math": "^0.9.3",
|
||||
"buzz/laravel-h-captcha": "1.0.4",
|
||||
"buzz/laravel-h-captcha": "^1.0.4",
|
||||
"doctrine/dbal": "^3.0",
|
||||
"intervention/image": "^2.4",
|
||||
"jenssegers/agent": "^2.6",
|
||||
|
@ -47,6 +47,7 @@
|
|||
"require-dev": {
|
||||
"brianium/paratest": "^6.1",
|
||||
"fakerphp/faker": "^1.20",
|
||||
"laravel/pint": "^1.14",
|
||||
"laravel/telescope": "^4.14",
|
||||
"mockery/mockery": "^1.0",
|
||||
"nunomaduro/collision": "^6.1",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -33,5 +33,10 @@ return [
|
|||
'nlp' => [
|
||||
'enabled' => false,
|
||||
'spam_sample_limit' => env('PF_AUTOSPAM_NLP_SPAM_SAMPLE_LIMIT', 200),
|
||||
],
|
||||
|
||||
'live_filters' => [
|
||||
'enabled' => env('PF_AUTOSPAM_LIVE_FILTERS_ENABLED', false),
|
||||
'filters' => env('PF_AUTOSPAM_LIVE_FILTERS_CSV', ''),
|
||||
]
|
||||
];
|
||||
|
|
|
@ -74,7 +74,7 @@ return [
|
|||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'lock_connection' => 'default',
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
'client' => env('REDIS_CLIENT', 'predis'),
|
||||
|
||||
'default' => [
|
||||
'scheme' => env('REDIS_SCHEME', 'tcp'),
|
||||
|
|
|
@ -49,7 +49,7 @@ return [
|
|||
],
|
||||
|
||||
'network_timeline' => env('PF_NETWORK_TIMELINE', true),
|
||||
'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 2),
|
||||
'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 90),
|
||||
|
||||
'custom_emoji' => [
|
||||
'enabled' => env('CUSTOM_EMOJI', false),
|
||||
|
@ -57,4 +57,6 @@ return [
|
|||
// max size in bytes, default is 2mb
|
||||
'max_size' => env('CUSTOM_EMOJI_MAX_SIZE', 2000000),
|
||||
],
|
||||
|
||||
'migration' => env('PF_ACCT_MIGRATION_ENABLED', true),
|
||||
];
|
||||
|
|
|
@ -72,7 +72,7 @@ return [
|
|||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'visibility' => 'public',
|
||||
'visibility' => env('AWS_VISIBILITY', 'public'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
|
|
|
@ -29,11 +29,12 @@ return [
|
|||
],
|
||||
|
||||
'local' => [
|
||||
'cached' => env('INSTANCE_PUBLIC_TIMELINE_CACHED', false),
|
||||
'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false)
|
||||
],
|
||||
|
||||
'network' => [
|
||||
'cached' => env('PF_NETWORK_TIMELINE') ? env('INSTANCE_NETWORK_TIMELINE_CACHED', true) : false,
|
||||
'cached' => env('PF_NETWORK_TIMELINE') ? env('INSTANCE_NETWORK_TIMELINE_CACHED', false) : false,
|
||||
'cache_dropoff' => env('INSTANCE_NETWORK_TIMELINE_CACHE_DROPOFF', 100),
|
||||
'max_hours_old' => env('INSTANCE_NETWORK_TIMELINE_CACHE_MAX_HOUR_INGEST', 6)
|
||||
]
|
||||
|
@ -139,5 +140,40 @@ return [
|
|||
'max_children' => env('INSTANCE_PARENTAL_CONTROLS_MAX_CHILDREN', 1),
|
||||
'auto_verify_email' => true,
|
||||
],
|
||||
]
|
||||
],
|
||||
|
||||
'software-update' => [
|
||||
'disable_failed_warning' => env('INSTANCE_SOFTWARE_UPDATE_DISABLE_FAILED_WARNING', false)
|
||||
],
|
||||
|
||||
'notifications' => [
|
||||
'gc' => [
|
||||
'enabled' => env('INSTANCE_NOTIFY_AUTO_GC', false),
|
||||
'delete_after_days' => env('INSTANCE_NOTIFY_AUTO_GC_DEL_AFTER_DAYS', 365)
|
||||
]
|
||||
],
|
||||
|
||||
'curated_registration' => [
|
||||
'enabled' => env('INSTANCE_CUR_REG', false),
|
||||
|
||||
'resend_confirmation_limit' => env('INSTANCE_CUR_REG_RESEND_LIMIT', 5),
|
||||
|
||||
'captcha_enabled' => env('INSTANCE_CUR_REG_CAPTCHA', env('CAPTCHA_ENABLED', false)),
|
||||
|
||||
'state' => [
|
||||
'fallback_on_closed_reg' => true,
|
||||
'only_enabled_on_closed_reg' => env('INSTANCE_CUR_REG_STATE_ONLY_ON_CLOSED', true),
|
||||
],
|
||||
|
||||
'notify' => [
|
||||
'admin' => [
|
||||
'on_verify_email' => [
|
||||
'enabled' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY', false),
|
||||
'bundle' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_BUNDLE', false),
|
||||
'max_per_day' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_MPD', 10),
|
||||
],
|
||||
'on_user_response' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_USER_RESPONSE', false),
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -23,7 +23,7 @@ return [
|
|||
| This value is the version of your Pixelfed instance.
|
||||
|
|
||||
*/
|
||||
'version' => '0.11.11',
|
||||
'version' => '0.11.13',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
upstream fe {
|
||||
server 127.0.0.1:8080;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name real.domain;
|
||||
listen [::]:443 ssl ipv6only=on;
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/real.domain/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/real.domain/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
location / {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
|
||||
proxy_set_header X-Forwarded-Port $http_x_forwarded_port;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://fe/;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
if ($host = real.domain) {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name real.domain;
|
||||
return 404;
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
FROM php:8.1-apache-bullseye
|
||||
|
||||
ENV COMPOSER_MEMORY_LIMIT=-1
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
WORKDIR /var/www/
|
||||
|
||||
# Get Composer binary
|
||||
COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Install package dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
# && apt-get install -y --no-install-recommends apt-utils \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
## Standard
|
||||
locales \
|
||||
locales-all \
|
||||
git \
|
||||
gosu \
|
||||
zip \
|
||||
unzip \
|
||||
libzip-dev \
|
||||
libcurl4-openssl-dev \
|
||||
## Image Optimization
|
||||
optipng \
|
||||
pngquant \
|
||||
jpegoptim \
|
||||
gifsicle \
|
||||
## Image Processing
|
||||
libjpeg62-turbo-dev \
|
||||
libpng-dev \
|
||||
libmagickwand-dev \
|
||||
# Required for GD
|
||||
libxpm4 \
|
||||
libxpm-dev \
|
||||
libwebp7 \
|
||||
libwebp-dev \
|
||||
## Video Processing
|
||||
ffmpeg \
|
||||
## Database
|
||||
# libpq-dev \
|
||||
# libsqlite3-dev \
|
||||
mariadb-client \
|
||||
# Locales Update
|
||||
&& sed -i '/en_US/s/^#//g' /etc/locale.gen \
|
||||
&& locale-gen \
|
||||
&& update-locale \
|
||||
# Install PHP extensions
|
||||
&& docker-php-source extract \
|
||||
#PHP Imagemagick extensions
|
||||
&& pecl install imagick \
|
||||
&& docker-php-ext-enable imagick \
|
||||
# PHP GD extensions
|
||||
&& docker-php-ext-configure gd \
|
||||
--with-freetype \
|
||||
--with-jpeg \
|
||||
--with-webp \
|
||||
--with-xpm \
|
||||
&& docker-php-ext-install -j$(nproc) gd \
|
||||
#PHP Redis extensions
|
||||
&& pecl install redis \
|
||||
&& docker-php-ext-enable redis \
|
||||
#PHP Database extensions
|
||||
&& docker-php-ext-install pdo_mysql \
|
||||
#pdo_pgsql pdo_sqlite \
|
||||
#PHP extensions (dependencies)
|
||||
&& docker-php-ext-configure intl \
|
||||
&& docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl \
|
||||
#APACHE Bootstrap
|
||||
&& a2enmod rewrite remoteip \
|
||||
&& {\
|
||||
echo RemoteIPHeader X-Real-IP ;\
|
||||
echo RemoteIPTrustedProxy 10.0.0.0/8 ;\
|
||||
echo RemoteIPTrustedProxy 172.16.0.0/12 ;\
|
||||
echo RemoteIPTrustedProxy 192.168.0.0/16 ;\
|
||||
echo SetEnvIf X-Forwarded-Proto "https" HTTPS=on ;\
|
||||
} > /etc/apache2/conf-available/remoteip.conf \
|
||||
&& a2enconf remoteip \
|
||||
#Cleanup
|
||||
&& docker-php-source delete \
|
||||
&& apt-get autoremove --purge -y \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/cache/apt \
|
||||
&& rm -rf /var/lib/apt/lists/
|
||||
|
||||
# Use the default production configuration
|
||||
COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini"
|
||||
|
||||
COPY . /var/www/
|
||||
# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862
|
||||
RUN cp -r storage storage.skel \
|
||||
&& composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader \
|
||||
&& rm -rf html && ln -s public html \
|
||||
&& chown -R www-data:www-data /var/www
|
||||
|
||||
RUN php artisan horizon:publish
|
||||
|
||||
VOLUME /var/www/storage /var/www/bootstrap
|
||||
|
||||
CMD ["/var/www/contrib/docker/start.apache.sh"]
|
|
@ -1,90 +0,0 @@
|
|||
FROM php:8.1-fpm-bullseye
|
||||
|
||||
ENV COMPOSER_MEMORY_LIMIT=-1
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
WORKDIR /var/www/
|
||||
|
||||
# Get Composer binary
|
||||
COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Install package dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
# && apt-get install -y --no-install-recommends apt-utils \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
## Standard
|
||||
locales \
|
||||
locales-all \
|
||||
git \
|
||||
gosu \
|
||||
zip \
|
||||
unzip \
|
||||
libzip-dev \
|
||||
libcurl4-openssl-dev \
|
||||
## Image Optimization
|
||||
optipng \
|
||||
pngquant \
|
||||
jpegoptim \
|
||||
gifsicle \
|
||||
## Image Processing
|
||||
libjpeg62-turbo-dev \
|
||||
libpng-dev \
|
||||
libmagickwand-dev \
|
||||
# Required for GD
|
||||
libxpm4 \
|
||||
libxpm-dev \
|
||||
libwebp7 \
|
||||
libwebp-dev \
|
||||
## Video Processing
|
||||
ffmpeg \
|
||||
## Database
|
||||
# libpq-dev \
|
||||
# libsqlite3-dev \
|
||||
mariadb-client \
|
||||
# Locales Update
|
||||
&& sed -i '/en_US/s/^#//g' /etc/locale.gen \
|
||||
&& locale-gen \
|
||||
&& update-locale \
|
||||
# Install PHP extensions
|
||||
&& docker-php-source extract \
|
||||
#PHP Imagemagick extensions
|
||||
&& pecl install imagick \
|
||||
&& docker-php-ext-enable imagick \
|
||||
# PHP GD extensions
|
||||
&& docker-php-ext-configure gd \
|
||||
--with-freetype \
|
||||
--with-jpeg \
|
||||
--with-webp \
|
||||
--with-xpm \
|
||||
&& docker-php-ext-install -j$(nproc) gd \
|
||||
#PHP Redis extensions
|
||||
&& pecl install redis \
|
||||
&& docker-php-ext-enable redis \
|
||||
#PHP Database extensions
|
||||
&& docker-php-ext-install pdo_mysql \
|
||||
#pdo_pgsql pdo_sqlite \
|
||||
#PHP extensions (dependencies)
|
||||
&& docker-php-ext-configure intl \
|
||||
&& docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl \
|
||||
#Cleanup
|
||||
&& docker-php-source delete \
|
||||
&& apt-get autoremove --purge -y \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/cache/apt \
|
||||
&& rm -rf /var/lib/apt/lists/
|
||||
|
||||
# Use the default production configuration
|
||||
COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini"
|
||||
|
||||
COPY . /var/www/
|
||||
# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862
|
||||
RUN cp -r storage storage.skel \
|
||||
&& composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader \
|
||||
&& rm -rf html && ln -s public html \
|
||||
&& chown -R www-data:www-data /var/www
|
||||
|
||||
RUN php artisan horizon:publish
|
||||
|
||||
VOLUME /var/www/storage /var/www/bootstrap
|
||||
|
||||
CMD ["/var/www/contrib/docker/start.fpm.sh"]
|
|
@ -1,15 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Create the storage tree if needed and fix permissions
|
||||
cp -r storage.skel/* storage/
|
||||
chown -R www-data:www-data storage/ bootstrap/
|
||||
|
||||
# Refresh the environment
|
||||
php artisan config:cache
|
||||
php artisan storage:link
|
||||
php artisan horizon:publish
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
|
||||
# Finally run Apache
|
||||
apache2-foreground
|
|
@ -1,15 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Create the storage tree if needed and fix permissions
|
||||
cp -r storage.skel/* storage/
|
||||
chown -R www-data:www-data storage/ bootstrap/
|
||||
|
||||
# Refresh the environment
|
||||
php artisan config:cache
|
||||
php artisan storage:link
|
||||
php artisan horizon:publish
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
|
||||
# Finally run FPM
|
||||
php-fpm
|
|
@ -1,67 +0,0 @@
|
|||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name pixelfed.example; # change this to your fqdn
|
||||
root /home/pixelfed/public; # path to repo/public
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/server.crt; # generate your own
|
||||
ssl_certificate_key /etc/nginx/ssl/server.key; # or use letsencrypt
|
||||
|
||||
ssl_protocols TLSv1.2;
|
||||
ssl_ciphers EECDH+AESGCM:EECDH+CHACHA20:EECDH+AES;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
#add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
index index.php;
|
||||
|
||||
charset utf-8;
|
||||
client_max_body_size 15M;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
error_page 404 /index.php;
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||
fastcgi_param QUERY_STRING $query_string;
|
||||
fastcgi_param REQUEST_METHOD $request_method;
|
||||
fastcgi_param CONTENT_TYPE $content_type;
|
||||
fastcgi_param CONTENT_LENGTH $content_length;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
fastcgi_param DOCUMENT_URI $document_uri;
|
||||
fastcgi_param DOCUMENT_ROOT $document_root;
|
||||
fastcgi_param SERVER_PROTOCOL $server_protocol;
|
||||
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
|
||||
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
|
||||
fastcgi_param REMOTE_ADDR $remote_addr;
|
||||
fastcgi_param REMOTE_PORT $remote_port;
|
||||
fastcgi_param SERVER_ADDR $server_addr;
|
||||
fastcgi_param SERVER_PORT $server_port;
|
||||
fastcgi_param SERVER_NAME $server_name;
|
||||
fastcgi_param HTTPS $https if_not_empty;
|
||||
fastcgi_param REDIRECT_STATUS 200;
|
||||
fastcgi_param HTTP_PROXY "";
|
||||
}
|
||||
|
||||
location ~ /\.(?!well-known).* {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
server { # Redirect http to https
|
||||
server_name pixelfed.example; # change this to your fqdn
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('curated_registers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('email')->unique()->nullable()->index();
|
||||
$table->string('username')->unique()->nullable()->index();
|
||||
$table->string('password')->nullable();
|
||||
$table->string('ip_address')->nullable();
|
||||
$table->string('verify_code')->nullable();
|
||||
$table->text('reason_to_join')->nullable();
|
||||
$table->unsignedBigInteger('invited_by')->nullable()->index();
|
||||
$table->boolean('is_approved')->default(0)->index();
|
||||
$table->boolean('is_rejected')->default(0)->index();
|
||||
$table->boolean('is_awaiting_more_info')->default(0)->index();
|
||||
$table->boolean('is_closed')->default(0)->index();
|
||||
$table->json('autofollow_account_ids')->nullable();
|
||||
$table->json('admin_notes')->nullable();
|
||||
$table->unsignedInteger('approved_by_admin_id')->nullable();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->timestamp('admin_notified_at')->nullable();
|
||||
$table->timestamp('action_taken_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('curated_registers');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('curated_register_activities', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedInteger('register_id')->nullable()->index();
|
||||
$table->unsignedInteger('admin_id')->nullable();
|
||||
$table->unsignedInteger('reply_to_id')->nullable()->index();
|
||||
$table->string('secret_code')->nullable();
|
||||
$table->string('type')->nullable()->index();
|
||||
$table->string('title')->nullable();
|
||||
$table->string('link')->nullable();
|
||||
$table->text('message')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->boolean('from_admin')->default(false)->index();
|
||||
$table->boolean('from_user')->default(false)->index();
|
||||
$table->boolean('admin_only_view')->default(true);
|
||||
$table->boolean('action_required')->default(false);
|
||||
$table->timestamp('admin_notified_at')->nullable();
|
||||
$table->timestamp('action_taken_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('curated_register_activities');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\Models\CuratedRegisterActivity;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('curated_registers', function (Blueprint $table) {
|
||||
$table->boolean('user_has_responded')->default(false)->index()->after('is_awaiting_more_info');
|
||||
});
|
||||
|
||||
CuratedRegisterActivity::whereFromUser(true)->get()->each(function($cra) {
|
||||
$cr = CuratedRegister::find($cra->register_id);
|
||||
$cr->user_has_responded = true;
|
||||
$cr->save();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('curated_registers', function (Blueprint $table) {
|
||||
$table->dropColumn('user_has_responded');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('curated_register_templates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->text('content')->nullable();
|
||||
$table->boolean('is_active')->default(false)->index();
|
||||
$table->tinyInteger('order')->default(10)->unsigned()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('curated_register_templates');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('profile_migrations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('profile_id');
|
||||
$table->string('acct')->nullable();
|
||||
$table->unsignedBigInteger('followers_count')->default(0);
|
||||
$table->unsignedBigInteger('target_profile_id')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('profile_migrations');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
migrate:
|
||||
image: "secoresearch/rsync"
|
||||
entrypoint: ""
|
||||
working_dir: /migrate
|
||||
command: 'bash -c "exit 1"'
|
||||
restart: never
|
||||
volumes:
|
||||
################################
|
||||
# Storage volume
|
||||
################################
|
||||
# OLD
|
||||
- "app-storage:/migrate/app-storage/old"
|
||||
# NEW
|
||||
- "${DOCKER_APP_HOST_STORAGE_PATH}:/migrate/app-storage/new"
|
||||
|
||||
################################
|
||||
# MySQL/DB volume
|
||||
################################
|
||||
# OLD
|
||||
- "db-data:/migrate/db-data/old"
|
||||
# NEW
|
||||
- "${DOCKER_DB_HOST_DATA_PATH}:/migrate/db-data/new"
|
||||
|
||||
################################
|
||||
# Redis volume
|
||||
################################
|
||||
# OLD
|
||||
- "redis-data:/migrate/redis-data/old"
|
||||
# NEW
|
||||
- "${DOCKER_REDIS_HOST_DATA_PATH}:/migrate/redis-data/new"
|
||||
|
||||
# Volumes from the old [docker-compose.yml] file
|
||||
# https://github.com/pixelfed/pixelfed/blob/b1ff44ca2f75c088a11576fb03b5bad2fbed4d5c/docker-compose.yml#L72-L76
|
||||
volumes:
|
||||
db-data:
|
||||
redis-data:
|
||||
app-storage:
|
||||
app-bootstrap:
|
|
@ -1,82 +1,218 @@
|
|||
---
|
||||
version: '3'
|
||||
# Require 3.8 to ensure people use a recent version of Docker + Compose
|
||||
version: "3.8"
|
||||
|
||||
# In order to set configuration, please use a .env file in
|
||||
# your compose project directory (the same directory as your
|
||||
# docker-compose.yml), and set database options, application
|
||||
# name, key, and other settings there.
|
||||
# A list of available settings is available in .env.example
|
||||
#
|
||||
# The services should scale properly across a swarm cluster
|
||||
# if the volumes are properly shared between cluster members.
|
||||
###############################################################
|
||||
# Please see docker/README.md for usage information
|
||||
###############################################################
|
||||
|
||||
services:
|
||||
## App and Worker
|
||||
app:
|
||||
# Comment to use dockerhub image
|
||||
image: pixelfed/pixelfed:latest
|
||||
# HTTP/HTTPS proxy
|
||||
#
|
||||
# Sits in front of the *real* webserver and manages SSL and (optionally)
|
||||
# load-balancing between multiple web servers
|
||||
#
|
||||
# You can disable this service by setting [DOCKER_PROXY_PROFILE="disabled"]
|
||||
# in your [.env] file - the setting is near the bottom of the file.
|
||||
#
|
||||
# This also disables the [proxy-acme] service, if this is not desired, change the
|
||||
# [DOCKER_PROXY_ACME_PROFILE] setting to an empty string [""]
|
||||
#
|
||||
# See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs
|
||||
proxy:
|
||||
image: nginxproxy/nginx-proxy:1.4
|
||||
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy"
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env.docker
|
||||
profiles:
|
||||
- ${DOCKER_PROXY_PROFILE:-}
|
||||
environment:
|
||||
DOCKER_SERVICE_NAME: "proxy"
|
||||
volumes:
|
||||
- app-storage:/var/www/storage
|
||||
- app-bootstrap:/var/www/bootstrap
|
||||
- "./.env.docker:/var/www/.env"
|
||||
networks:
|
||||
- external
|
||||
- internal
|
||||
- "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/tmp/docker.sock:ro"
|
||||
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d"
|
||||
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/vhost.d:/etc/nginx/vhost.d"
|
||||
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs:/etc/nginx/certs"
|
||||
- "${DOCKER_ALL_HOST_DATA_ROOT_PATH}/proxy/html:/usr/share/nginx/html"
|
||||
ports:
|
||||
- "8080:80"
|
||||
- "${DOCKER_PROXY_HOST_PORT_HTTP}:80"
|
||||
- "${DOCKER_PROXY_HOST_PORT_HTTPS}:443"
|
||||
healthcheck:
|
||||
test: "curl --fail https://${APP_DOMAIN}/api/service/health-check"
|
||||
interval: "${DOCKER_PROXY_HEALTHCHECK_INTERVAL}"
|
||||
retries: 2
|
||||
timeout: 5s
|
||||
|
||||
# Proxy companion for managing letsencrypt SSL certificates
|
||||
#
|
||||
# You can disable this service by setting [DOCKER_PROXY_ACME_PROFILE="disabled"]
|
||||
# in your [.env] file - the setting is near the bottom of the file.
|
||||
#
|
||||
# See: https://github.com/nginx-proxy/acme-companion/tree/main/docs
|
||||
proxy-acme:
|
||||
image: nginxproxy/acme-companion
|
||||
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy-acme"
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- ${DOCKER_PROXY_ACME_PROFILE:-}
|
||||
environment:
|
||||
DEBUG: 0
|
||||
DEFAULT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}"
|
||||
NGINX_PROXY_CONTAINER: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy"
|
||||
depends_on:
|
||||
- proxy
|
||||
volumes:
|
||||
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy-acme:/etc/acme.sh"
|
||||
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs:/etc/nginx/certs"
|
||||
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d"
|
||||
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/vhost.d:/etc/nginx/vhost.d"
|
||||
- "${DOCKER_ALL_HOST_DATA_ROOT_PATH}/proxy/html:/usr/share/nginx/html"
|
||||
- "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock:ro"
|
||||
|
||||
web:
|
||||
image: "${DOCKER_APP_IMAGE}:${DOCKER_APP_TAG}"
|
||||
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-web"
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- ${DOCKER_WEB_PROFILE:-}
|
||||
build:
|
||||
target: ${DOCKER_APP_RUNTIME}-runtime
|
||||
cache_from:
|
||||
- "type=registry,ref=${DOCKER_APP_IMAGE}-cache:${DOCKER_APP_TAG}"
|
||||
args:
|
||||
APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}"
|
||||
PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}"
|
||||
PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}"
|
||||
PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}"
|
||||
PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}"
|
||||
PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}"
|
||||
environment:
|
||||
# Used by Pixelfed Docker init script
|
||||
DOCKER_SERVICE_NAME: "web"
|
||||
DOCKER_APP_ENTRYPOINT_DEBUG: ${DOCKER_APP_ENTRYPOINT_DEBUG:-0}
|
||||
ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-}
|
||||
# Used by [proxy] service
|
||||
LETSENCRYPT_HOST: "${DOCKER_PROXY_LETSENCRYPT_HOST:?error}"
|
||||
LETSENCRYPT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}"
|
||||
LETSENCRYPT_TEST: "${DOCKER_PROXY_LETSENCRYPT_TEST:-}"
|
||||
VIRTUAL_HOST: "${APP_DOMAIN}"
|
||||
VIRTUAL_PORT: "80"
|
||||
volumes:
|
||||
- "./.env:/var/www/.env"
|
||||
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/shared/proxy/conf.d"
|
||||
- "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache"
|
||||
- "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro"
|
||||
- "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage"
|
||||
labels:
|
||||
com.github.nginx-proxy.nginx-proxy.keepalive: 30
|
||||
com.github.nginx-proxy.nginx-proxy.http2.enable: true
|
||||
com.github.nginx-proxy.nginx-proxy.http3.enable: true
|
||||
ports:
|
||||
- "${DOCKER_WEB_PORT_EXTERNAL_HTTP}:80"
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
healthcheck:
|
||||
test: 'curl --header "Host: ${APP_DOMAIN}" --fail http://localhost/api/service/health-check'
|
||||
interval: "${DOCKER_WEB_HEALTHCHECK_INTERVAL}"
|
||||
retries: 2
|
||||
timeout: 5s
|
||||
|
||||
worker:
|
||||
image: pixelfed/pixelfed:latest
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env.docker
|
||||
volumes:
|
||||
- app-storage:/var/www/storage
|
||||
- app-bootstrap:/var/www/bootstrap
|
||||
networks:
|
||||
- external
|
||||
- internal
|
||||
image: "${DOCKER_APP_IMAGE}:${DOCKER_APP_TAG}"
|
||||
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-worker"
|
||||
command: gosu www-data php artisan horizon
|
||||
restart: unless-stopped
|
||||
stop_signal: SIGTERM
|
||||
profiles:
|
||||
- ${DOCKER_WORKER_PROFILE:-}
|
||||
build:
|
||||
target: ${DOCKER_APP_RUNTIME}-runtime
|
||||
cache_from:
|
||||
- "type=registry,ref=${DOCKER_APP_IMAGE}-cache:${DOCKER_APP_TAG}"
|
||||
args:
|
||||
APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}"
|
||||
PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}"
|
||||
PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}"
|
||||
PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}"
|
||||
PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}"
|
||||
PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}"
|
||||
environment:
|
||||
# Used by Pixelfed Docker init script
|
||||
DOCKER_SERVICE_NAME: "worker"
|
||||
DOCKER_APP_ENTRYPOINT_DEBUG: ${DOCKER_APP_ENTRYPOINT_DEBUG:-0}
|
||||
ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-}
|
||||
volumes:
|
||||
- "./.env:/var/www/.env"
|
||||
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/shared/proxy/conf.d"
|
||||
- "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache"
|
||||
- "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro"
|
||||
- "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage"
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
healthcheck:
|
||||
test: gosu www-data php artisan horizon:status | grep running
|
||||
interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL:?error}"
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
|
||||
## DB and Cache
|
||||
db:
|
||||
image: mariadb:jammy
|
||||
image: ${DOCKER_DB_IMAGE:?error}
|
||||
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-db"
|
||||
command: ${DOCKER_DB_COMMAND:-}
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- internal
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
env_file:
|
||||
- .env.docker
|
||||
profiles:
|
||||
- ${DOCKER_DB_PROFILE:-}
|
||||
environment:
|
||||
TZ: "${TZ:?error}"
|
||||
# MySQL (Oracle) - "Environment Variables" at https://hub.docker.com/_/mysql
|
||||
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD:?error}"
|
||||
MYSQL_USER: "${DB_USERNAME:?error}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD:?error}"
|
||||
MYSQL_DATABASE: "${DB_DATABASE:?error}"
|
||||
# MySQL (MariaDB) - "Start a mariadb server instance with user, password and database" at https://hub.docker.com/_/mariadb
|
||||
MARIADB_ROOT_PASSWORD: "${DB_PASSWORD:?error}"
|
||||
MARIADB_USER: "${DB_USERNAME:?error}"
|
||||
MARIADB_PASSWORD: "${DB_PASSWORD:?error}"
|
||||
MARIADB_DATABASE: "${DB_DATABASE:?error}"
|
||||
# PostgreSQL - "Environment Variables" at https://hub.docker.com/_/postgres
|
||||
POSTGRES_USER: "${DB_USERNAME:?error}"
|
||||
POSTGRES_PASSWORD: "${DB_PASSWORD:?error}"
|
||||
POSTGRES_DB: "${DB_DATABASE:?error}"
|
||||
volumes:
|
||||
- "db-data:/var/lib/mysql"
|
||||
- "${DOCKER_DB_HOST_DATA_PATH:?error}:${DOCKER_DB_CONTAINER_DATA_PATH:?error}"
|
||||
ports:
|
||||
- "${DOCKER_DB_HOST_PORT:?error}:${DOCKER_DB_CONTAINER_PORT:?error}"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"healthcheck.sh",
|
||||
"--su-mysql",
|
||||
"--connect",
|
||||
"--innodb_initialized",
|
||||
]
|
||||
interval: "${DOCKER_DB_HEALTHCHECK_INTERVAL:?error}"
|
||||
retries: 2
|
||||
timeout: 5s
|
||||
|
||||
redis:
|
||||
image: redis:5-alpine
|
||||
image: redis:${DOCKER_REDIS_VERSION}
|
||||
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-redis"
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env.docker
|
||||
command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'"
|
||||
profiles:
|
||||
- ${DOCKER_REDIS_PROFILE:-}
|
||||
environment:
|
||||
TZ: "${TZ:?error}"
|
||||
REDISCLI_AUTH: ${REDIS_PASSWORD:-}
|
||||
volumes:
|
||||
- "redis-data:/data"
|
||||
networks:
|
||||
- internal
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
redis-data:
|
||||
app-storage:
|
||||
app-bootstrap:
|
||||
|
||||
networks:
|
||||
internal:
|
||||
internal: true
|
||||
external:
|
||||
driver: bridge
|
||||
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/redis:/etc/redis"
|
||||
- "${DOCKER_REDIS_HOST_DATA_PATH}:/data"
|
||||
ports:
|
||||
- "${DOCKER_REDIS_HOST_PORT}:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-p", "6379", "ping"]
|
||||
interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL:?error}"
|
||||
retries: 2
|
||||
timeout: 5s
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Pixelfed + Docker + Docker Compose
|
||||
|
||||
Please see the [Pixelfed Docs (Next)](https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/) for current documentation on Docker usage.
|
||||
|
||||
The docs can be [reviewed in the pixelfed/docs-next](https://github.com/pixelfed/docs-next/pull/1) repository.
|
|
@ -0,0 +1,8 @@
|
|||
RemoteIPHeader X-Real-IP
|
||||
|
||||
# All private IPs as outlined in rfc1918
|
||||
#
|
||||
# See: https://datatracker.ietf.org/doc/html/rfc1918
|
||||
RemoteIPTrustedProxy 10.0.0.0/8
|
||||
RemoteIPTrustedProxy 172.16.0.0/12
|
||||
RemoteIPTrustedProxy 192.168.0.0/16
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/bash
|
||||
|
||||
declare service="${PF_SERVICE:=worker}"
|
||||
declare user="${PF_USER:=www-data}"
|
||||
|
||||
exec docker compose exec \
|
||||
--user "${user}" \
|
||||
--env TERM \
|
||||
--env COLORTERM \
|
||||
"${service}" \
|
||||
php artisan "${@}"
|
|
@ -0,0 +1,45 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e -o errexit -o nounset -o pipefail
|
||||
|
||||
declare project_root="${PWD}"
|
||||
declare user="${PF_USER:=www-data}"
|
||||
|
||||
if command -v git &>/dev/null; then
|
||||
project_root=$(git rev-parse --show-toplevel)
|
||||
fi
|
||||
|
||||
declare -r release="${DOTTIE_VERSION:-latest}"
|
||||
|
||||
declare -r update_check_file="/tmp/.dottie-update-check" # file to check age of since last update
|
||||
declare -i update_check_max_age=$((8 * 60 * 60)) # 8 hours between checking for dottie version
|
||||
declare -i update_check_cur_age=$((update_check_max_age + 1)) # by default the "update" event should happen
|
||||
|
||||
# default [docker run] flags
|
||||
declare -a flags=(
|
||||
--rm
|
||||
--interactive
|
||||
--tty
|
||||
--user "${user}"
|
||||
--env TERM
|
||||
--env COLORTERM
|
||||
--volume "${project_root}:/var/www"
|
||||
--workdir /var/www
|
||||
)
|
||||
|
||||
# if update file exists, find its age since last modification
|
||||
if [[ -f "${update_check_file}" ]]; then
|
||||
now=$(date +%s)
|
||||
changed=$(date -r "${update_check_file}" +%s)
|
||||
update_check_cur_age=$((now - changed))
|
||||
fi
|
||||
|
||||
# if update file is older than max allowed poll for new version of dottie
|
||||
if [[ $update_check_cur_age -gt $update_check_max_age ]]; then
|
||||
flags+=(--pull always)
|
||||
|
||||
touch "${update_check_file}"
|
||||
fi
|
||||
|
||||
# run dottie
|
||||
exec docker run "${flags[@]}" "ghcr.io/jippi/dottie:${release}" "$@"
|
|
@ -0,0 +1,2 @@
|
|||
fpm: php-fpm
|
||||
nginx: nginx -g "daemon off;"
|
|
@ -0,0 +1,49 @@
|
|||
server {
|
||||
listen 80 default_server;
|
||||
|
||||
server_name {{ getenv "APP_DOMAIN" }};
|
||||
root /var/www/public;
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
access_log /dev/stdout;
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
index index.html index.htm index.php;
|
||||
|
||||
charset utf-8;
|
||||
client_max_body_size {{ getenv "POST_MAX_SIZE" "61M" }};
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location = /favicon.ico {
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
location = /robots.txt {
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
error_page 404 /index.php;
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
|
||||
include fastcgi_params;
|
||||
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
|
||||
location ~ /\.(?!well-known).* {
|
||||
deny all;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
# This is changed from the original "nginx" in upstream to work properly
|
||||
# with permissions within pixelfed when serving static files.
|
||||
user www-data;
|
||||
|
||||
worker_processes auto;
|
||||
|
||||
# Ensure the PID is writable
|
||||
# Lifted from: https://hub.docker.com/r/nginxinc/nginx-unprivileged
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
# Write error log to stderr (/proc/self/fd/2 -> /dev/stderr)
|
||||
error_log /proc/self/fd/2 notice;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
# Write error log to stdout (/proc/self/fd/1 -> /dev/stdout)
|
||||
access_log /proc/self/fd/1 main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
|
||||
# Ensure all temp paths are in a writable by "www-data" user.
|
||||
# Lifted from: https://hub.docker.com/r/nginxinc/nginx-unprivileged
|
||||
client_body_temp_path /tmp/client_temp;
|
||||
proxy_temp_path /tmp/proxy_temp_path;
|
||||
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||
scgi_temp_path /tmp/scgi_temp;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
: "${ENTRYPOINT_ROOT:="/docker"}"
|
||||
|
||||
# shellcheck source=SCRIPTDIR/../helpers.sh
|
||||
source "${ENTRYPOINT_ROOT}/helpers.sh"
|
||||
|
||||
entrypoint-set-script-name "$0"
|
||||
|
||||
# Ensure the Docker volumes and required files are owned by the runtime user as other scripts
|
||||
# will be writing to these
|
||||
run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./.env"
|
||||
run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap/cache"
|
||||
run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage"
|
||||
run-as-current-user chown --verbose --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "./storage/docker"
|
||||
|
||||
# Optionally fix ownership of configured paths
|
||||
: "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS:=""}"
|
||||
|
||||
declare -a ensure_ownership_paths=()
|
||||
IFS=' ' read -ar ensure_ownership_paths <<<"${DOCKER_APP_ENSURE_OWNERSHIP_PATHS}"
|
||||
|
||||
if [[ ${#ensure_ownership_paths[@]} == 0 ]]; then
|
||||
log-info "No paths has been configured for ownership fixes via [\$DOCKER_APP_ENSURE_OWNERSHIP_PATHS]."
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for path in "${ensure_ownership_paths[@]}"; do
|
||||
log-info "Ensure ownership of [${path}] is correct"
|
||||
stream-prefix-command-output run-as-current-user chown --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "${path}"
|
||||
done
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
: "${ENTRYPOINT_ROOT:="/docker"}"
|
||||
|
||||
# shellcheck source=SCRIPTDIR/../helpers.sh
|
||||
source "${ENTRYPOINT_ROOT}/helpers.sh"
|
||||
|
||||
entrypoint-set-script-name "$0"
|
||||
|
||||
# Validating dot-env files for any issues
|
||||
for file in "${dot_env_files[@]}"; do
|
||||
if ! file-exists "${file}"; then
|
||||
log-warning "Could not source file [${file}]: does not exists"
|
||||
continue
|
||||
fi
|
||||
|
||||
# We ignore 'dir' + 'file' rules since they are validate *host* paths
|
||||
# which do not (and should not) exists inside the container
|
||||
#
|
||||
# We disable fixer since its not interactive anyway
|
||||
run-as-current-user dottie validate --file "${file}" --ignore-rule dir,file --exclude-prefix APP_KEY --no-fix
|
||||
done
|
|
@ -0,0 +1,33 @@
|
|||
#!/bin/bash
|
||||
|
||||
# NOTE:
|
||||
#
|
||||
# This file is *sourced* not run by the entrypoint runner
|
||||
# so any environment values set here will be accessible to all sub-processes
|
||||
# and future entrypoint.d scripts
|
||||
#
|
||||
# We also don't need to source `helpers.sh` since it's already available
|
||||
|
||||
entrypoint-set-script-name "${BASH_SOURCE[0]}"
|
||||
|
||||
load-config-files
|
||||
|
||||
: "${MAX_PHOTO_SIZE:=15000}"
|
||||
: "${MAX_ALBUM_LENGTH:=4}"
|
||||
|
||||
# We assign a 1MB buffer to the just-in-time calculated max post size to allow for fields and overhead
|
||||
: "${POST_MAX_SIZE_BUFFER:=1M}"
|
||||
log-info "POST_MAX_SIZE_BUFFER is set to [${POST_MAX_SIZE_BUFFER}]"
|
||||
buffer=$(numfmt --invalid=fail --from=auto --to=none --to-unit=K "${POST_MAX_SIZE_BUFFER}")
|
||||
log-info "POST_MAX_SIZE_BUFFER converted to KB is [${buffer}]"
|
||||
|
||||
# Automatically calculate the [post_max_size] value for [php.ini] and [nginx]
|
||||
log-info "POST_MAX_SIZE will be calculated by [({MAX_PHOTO_SIZE} * {MAX_ALBUM_LENGTH}) + {POST_MAX_SIZE_BUFFER}]"
|
||||
log-info " MAX_PHOTO_SIZE=${MAX_PHOTO_SIZE}"
|
||||
log-info " MAX_ALBUM_LENGTH=${MAX_ALBUM_LENGTH}"
|
||||
log-info " POST_MAX_SIZE_BUFFER=${buffer}"
|
||||
: "${POST_MAX_SIZE:=$(numfmt --invalid=fail --from=auto --from-unit=K --to=si $(((MAX_PHOTO_SIZE * MAX_ALBUM_LENGTH) + buffer)))}"
|
||||
log-info "POST_MAX_SIZE was calculated to [${POST_MAX_SIZE}]"
|
||||
|
||||
# NOTE: must export the value so it's available in other scripts!
|
||||
export POST_MAX_SIZE
|
|
@ -0,0 +1,60 @@
|
|||
#!/bin/bash
|
||||
: "${ENTRYPOINT_ROOT:="/docker"}"
|
||||
|
||||
# shellcheck source=SCRIPTDIR/../helpers.sh
|
||||
source "${ENTRYPOINT_ROOT}/helpers.sh"
|
||||
|
||||
entrypoint-set-script-name "$0"
|
||||
|
||||
# Show [git diff] of templates being rendered (will help verify output)
|
||||
: "${ENTRYPOINT_SHOW_TEMPLATE_DIFF:=1}"
|
||||
# Directory where templates can be found
|
||||
: "${ENTRYPOINT_TEMPLATE_DIR:=/docker/templates/}"
|
||||
# Root path to write template template_files to (default is '', meaning it will be written to /<path>)
|
||||
: "${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX:=}"
|
||||
|
||||
declare template_file relative_template_file_path output_file_dir
|
||||
|
||||
# load all dot-env config files
|
||||
load-config-files
|
||||
|
||||
# export all dot-env variables so they are available in templating
|
||||
#
|
||||
# shellcheck disable=SC2068
|
||||
export ${seen_dot_env_variables[@]}
|
||||
|
||||
find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r template_file; do
|
||||
# Example: template_file=/docker/templates/usr/local/etc/php/php.ini
|
||||
|
||||
# The file path without the template dir prefix ($ENTRYPOINT_TEMPLATE_DIR)
|
||||
#
|
||||
# Example: /usr/local/etc/php/php.ini
|
||||
relative_template_file_path="${template_file#"${ENTRYPOINT_TEMPLATE_DIR}"}"
|
||||
|
||||
# Adds optional prefix to the output file path
|
||||
#
|
||||
# Example: /usr/local/etc/php/php.ini
|
||||
output_file_path="${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX}/${relative_template_file_path}"
|
||||
|
||||
# Remove the file from the path
|
||||
#
|
||||
# Example: /usr/local/etc/php
|
||||
output_file_dir=$(dirname "${output_file_path}")
|
||||
|
||||
# Ensure the output directory is writable
|
||||
if ! is-writable "${output_file_dir}"; then
|
||||
log-error-and-exit "${output_file_dir} is not writable"
|
||||
fi
|
||||
|
||||
# Create the output directory if it doesn't exists
|
||||
ensure-directory-exists "${output_file_dir}"
|
||||
|
||||
# Render the template
|
||||
log-info "Running [gomplate] on [${template_file}] --> [${output_file_path}]"
|
||||
gomplate <"${template_file}" >"${output_file_path}"
|
||||
|
||||
# Show the diff from the envsubst command
|
||||
if is-true "${ENTRYPOINT_SHOW_TEMPLATE_DIFF}"; then
|
||||
git --no-pager diff --color=always "${template_file}" "${output_file_path}" || : # ignore diff exit code
|
||||
fi
|
||||
done
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue